文章目录
- 1. 交易策略
- 2. Backtrader回测程序
- 3. 回测效果
- 3.1 2020年1月1日 - 2021年1月1日
- 3.2 2021年1月1日 — 2022年1月1日
- 3.3 2022年1月1日 — 2023年1月1日
动量因子的概述与测算,阿隆指标测算请参考:https://blog.csdn.net/weixin_35757704/article/details/128767040
1. 交易策略
阿隆动量因子策略构建:
- 相同权重,满仓持有5个指数;每卖出1个或多个指数后,都会等权重用现金买入指数,始终保持5个指数的持仓
- 买入:AroonDown+AroonUp>50,且当AroonUp > AroonDown时买入
- 卖出:AroonDown+AroonUp>50,且当AroonDown > AroonUp时卖出
- 买入时:
- 计算【aroon差额】= AroonUp-AroonDown;得到上涨与下跌动量的差额
- 每个指数按照 aroon差额 从大到小的顺序排序;为了买入上涨动量最强,且下跌动量最弱的指数
- 如果多个指数的aroon差额值相同,则按照15年到20年测算的胜率高低,按照历史测算时综合胜率的先后关系排序
- 然后从上到下依次买入指数,最终保持始终持仓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