量化选股——基于动量因子的行业风格轮动策略(第2部分—策略回测)

动量因子的概述与测算,阿隆指标测算请参考:https://blog.csdn.net/weixin_35757704/article/details/128767040

1. 交易策略

阿隆动量因子策略构建:

  1. 相同权重,满仓持有5个指数;每卖出1个或多个指数后,都会等权重用现金买入指数,始终保持5个指数的持仓
  2. 买入:AroonDown+AroonUp>50,且当AroonUp > AroonDown时买入
  3. 卖出:AroonDown+AroonUp>50,且当AroonDown > AroonUp时卖出
  4. 买入时:
    1. 计算【aroon差额】= AroonUp-AroonDown;得到上涨与下跌动量的差额
    2. 每个指数按照 aroon差额 从大到小的顺序排序;为了买入上涨动量最强,且下跌动量最弱的指数
    3. 如果多个指数的aroon差额值相同,则按照15年到20年测算的胜率高低,按照历史测算时综合胜率的先后关系排序
    4. 然后从上到下依次买入指数,最终保持始终持仓5个指数

以 1/1000 作为摩擦成本,不计算管理费

2. Backtrader回测程序

这里我们使用backtrader回测框架,回测的内容除了在 量化策略——准备3 数据、Backtrader回测框架与quantstats评价指标 中提供的一些方法外,核心的策略代码如下:

class AroonStrategy(TemplateStrategy):
    params = (("start_date", None), ('end_date', None),)

    def __init__(self):
        super().__init__()
        # 基本配置
        self.max_hold = 5
        self.this_month = self.params.start_date.month
        total_bond_code = []
        for this_data in self.datas:
            if type(this_data).__name__ == "StockData":
                total_bond_code.append(this_data._name)
        self.total_bond_code = total_bond_code
        self.vic_dict = {
    
    '801210.SI': 0, '801110.SI': 1, '801750.SI': 2, '801120.SI': 3, '801890.SI': 4, '801080.SI': 5,
                         '801200.SI': 6, '801140.SI': 7, '801160.SI': 8, '801730.SI': 9, '801010.SI': 10,
                         '801130.SI': 11, '801760.SI': 12, '801770.SI': 13, '801050.SI': 14, '801040.SI': 15,
                         '801180.SI': 16, '801720.SI': 17, '801710.SI': 18, '801030.SI': 19, '801880.SI': 20,
                         '801170.SI': 21, '801790.SI': 22, '801150.SI': 23, '801230.SI': 24, '801740.SI': 25,
                         '801950.SI': 26, '801780.SI': 27}

    def next(self):
        """最核心的触发策略"""
        hold_bond_name = [_p._name for _p in self.broker.positions if self.broker.getposition(_p).size > 0]  # 查看持仓
        # 计算指标
        _candidate_dict = {
    
    }
        for _candidate_code in self.total_bond_code:
            _candidate_dict[_candidate_code] = {
    
    
                "aroondown": self.getdatabyname(_candidate_code).aroondown[0],
                "aroonup": self.getdatabyname(_candidate_code).aroonup[0],
            }
        candidate_df = pd.DataFrame(_candidate_dict).T
        candidate_df['aroo_energy'] = candidate_df['aroondown'] + candidate_df['aroonup']
        candidate_df['aroo_mines'] = candidate_df['aroonup'] - candidate_df['aroondown']
        candidate_df = pd.merge(candidate_df, pd.DataFrame(self.vic_dict, index=['rank']).T,
                                left_index=True, right_index=True)
        candidate_df = candidate_df.sort_values(['aroo_mines', "rank"], ascending=[False, True])

        if candidate_df['aroo_energy'].sum() == 0:
            return
        if len(hold_bond_name) < self.max_hold:
            self.get_buy_bond(candidate_df, self.max_hold - len(hold_bond_name))
        # 卖出的逻辑
        for _index, _series in candidate_df.iterrows():
            if _index in hold_bond_name:
                if _series['aroonup'] < _series['aroondown']:
                    self.sell(data=_index, size=self.getpositionbyname(_index).size,
                              valid=self.getdatabyname(_index).datetime.date(1))

    def get_buy_bond(self, candidate_df, buy_num):
        hold_bond_name = [_p._name for _p in self.broker.positions if self.broker.getposition(_p).size > 0]
        for index, series in candidate_df.iterrows():
            if series["aroo_energy"] <= 50:  # 当 AroonDown + AroonUp > 50时才执行判操作
                continue
            if index in hold_bond_name:
                continue
            buy_data = self.getdatabyname(index)
            if len(buy_data) >= buy_data.buflen():
                continue
            if series['aroonup'] > series['aroondown']:
                buy_cost_value = self.broker.getcash() / (self.max_hold - len(hold_bond_name)) * (
                        1 - self.broker.comminfo[None].p.commission)
                buy_size = buy_cost_value / self.getdatabyname(index).close[0]
                self.buy(data=buy_data, size=buy_size, exectype=bt.Order.Limit,
                         price=buy_data.close[0],
                         valid=buy_data.datetime.date(1))
                logger.debug("买入 {} size:{} 预计费用:{}".format(index, buy_size, buy_cost_value))
                buy_num -= 1
            if buy_num == 0:
                break

    def stop(self):
        # 绘制净值曲线
        wealth_curve_data = {
    
    }
        for _k, _v in self.value_record.items():
            wealth_curve_data[_k] = _v / self.broker.startingcash
        self.plot_wealth_curve(wealth_curve_data, "arron_{}_{}".format(
            self.params.start_date.strftime("%Y-%m-%d"), self.params.end_date.strftime("%Y-%m-%d")))
        # 最终结果
        daily_return = cal_daily_return(pd.Series(self.value_record))
        _, record_dict = cal_rolling_feature(daily_return)
        print(record_dict)
        print('a')

在策略中,当【aroon差额】相同时,按照测算的胜率从大到小依次买入,下面的字典便是每个行业指数胜率从大到小的排名,用于辅助排序:

{
    
    '801210.SI': 0, '801110.SI': 1, '801750.SI': 2, '801120.SI': 3, '801890.SI': 4, '801080.SI': 5,
 '801200.SI': 6, '801140.SI': 7, '801160.SI': 8, '801730.SI': 9, '801010.SI': 10,
 '801130.SI': 11, '801760.SI': 12, '801770.SI': 13, '801050.SI': 14, '801040.SI': 15,
 '801180.SI': 16, '801720.SI': 17, '801710.SI': 18, '801030.SI': 19, '801880.SI': 20,
 '801170.SI': 21, '801790.SI': 22, '801150.SI': 23, '801230.SI': 24, '801740.SI': 25,
 '801950.SI': 26, '801780.SI': 27}

3. 回测效果

3.1 2020年1月1日 - 2021年1月1日

  • 最终净值:1.21
  • 复合年增长: 0.226
  • 夏普比率: 0.885
  • 索蒂诺: 1.167
  • omega: 1.175
  • 最大回撤: -0.173
  • 年波动率: 0.253
    在这里插入图片描述

3.2 2021年1月1日 — 2022年1月1日

  • 最终净值:0.909
  • 复合年增长: -0.092
  • 夏普比率: -0.481
  • 索蒂诺: -0.663
  • omega: 0.924
  • 最大回撤: -0.191
  • 年波动率: 0.205
    在这里插入图片描述

3.3 2022年1月1日 — 2023年1月1日

  • 最终净值:0.74
  • 复合年增长: -0.255
  • 夏普比率: -1.681
  • 索蒂诺: -2.073
  • omega: 0.741
  • 最大回撤: -0.258
  • 年波动率: 0.186

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_35757704/article/details/129237291