一、策略简介
动量交易策略源于股票或期货市场中的动量效应,所谓动量效应是指过去一段时间的收益较高的资产价格,那么,资产在未来一段时间内同样也能获得较高收益。
同样的,如果某一资产价格过去的波动越大,那么在未来的一段时间内这部分资产价格的波动同样也大,也就是惯性效应。因此,基于这样的逻辑开发策略,称作:动量策略。
在股票和期货市场都有这样的效应,那么下面在期货市场中实现基于这一逻辑的策略。
二、策略的原理
本文将利用海龟交易策略中的唐奇安通道,应用在这一逻辑中,用Python tqsdk 开发基于多品种的动量交易策略,品种排序的标准是根据平均真实波幅(ATR中的TR)进行一阶段差分后平均值,进行排序。
策略逻辑:
(1)在多个品种中选出波动率较大的品种,计算唐奇安通道。当价格突破上轨时,开多。跌破下轨时开空。
(2)当价格突破上轨时,平空。跌破下轨时平多。
其实我们可以看得出,策略的基本思想并不复杂,就是一个通道+动量效应。但就是因为是基于动量这一逻辑,才让一个平凡的策略拥有比较理想的收益。在量化程序化交易中获得很多交易者的亲睐!
三、代码编写
策略的实现过程,我将用四个部分来讲解:剔除交易量较小或上市短的品种、按照ATR平均真实波幅进行排序、将排序后的期货合约进行交易。
(一)首先,剔除交易量较小或上市短的品种。
1.导入模块。
from tqsdk import TqApifrom tqsdk.ta import ATR,MAfrom datetime import timedeltaimport pandas as pdimport seaborn as snsimport numpy as npimport mathimport matplotlib.pyplot as pltsns.set()
2.获取剔除后的品种。我们剔除了股指期货和成交量或上市日期较短的期货品种",存入symbol_Trade这个列表中,用后后续进行排序。
symbol_Trade = []def trade_symbol(api): "剔除股指期货和成交量或上市日期较短的期货品种" Continuous_contract = [v["underlying_symbol"] for k, v in api._data["quotes"].items() if k.startswith("KQ.m")] symbol_Index = [] a = '' for i in range(len(Continuous_contract)): if Continuous_contract[i].split('.')[0] == 'CFFEX': continue else: for j in range(len(Continuous_contract[i].split('.')[1])): if Continuous_contract[i].split('.')[1][j] in '0123456789': symbol_Index.append(Continuous_contract[i].split('.')[1][:j]) a = Continuous_contract[i].split('.')[1][:j] "剔除品种" if a not in ['WH', 'PM', 'RI', 'RS', 'JR', 'LR', 'bb', 'wr', 'fb','fu','sp','ss','eg','eb','pg', 'CJ','UR','SA','sc','CY','b','SF','rr','nr']: symbol_Trade.append("KQ.i@"+Continuous_contract[i].split('.')[0]+'.'+a) break break
run:
['KQ.i@SHFE.cu', 'KQ.i@SHFE.au', 'KQ.i@SHFE.ag', 'KQ.i@SHFE.zn', 'KQ.i@SHFE.al', 'KQ.i@SHFE.ru', 'KQ.i@SHFE.rb', 'KQ.i@SHFE.hc', 'KQ.i@SHFE.bu', 'KQ.i@SHFE.pb', 'KQ.i@SHFE.ni', 'KQ.i@SHFE.sn', 'KQ.i@DCE.a', 'KQ.i@DCE.c', 'KQ.i@DCE.cs', 'KQ.i@DCE.i', 'KQ.i@DCE.j', 'KQ.i@DCE.jd', 'KQ.i@DCE.jm', 'KQ.i@DCE.l', 'KQ.i@DCE.m', 'KQ.i@DCE.p', 'KQ.i@DCE.pp', 'KQ.i@DCE.v', 'KQ.i@DCE.y', 'KQ.i@CZCE.CF', 'KQ.i@CZCE.SR', 'KQ.i@CZCE.TA', 'KQ.i@CZCE.OI', 'KQ.i@CZCE.MA', 'KQ.i@CZCE.FG', 'KQ.i@CZCE.RM', 'KQ.i@CZCE.ZC', 'KQ.i@CZCE.SM', 'KQ.i@CZCE.AP']
(二)按照ATR平均真实波幅进行排序。
将所有品种计算,ATR指标中的TR再进行一阶差分,然后求差分后的均值,最后得到所有品种每一个时间点上的值,将值进行降序排列。
在下一步交易中我们就用波动率最大的品种进行交易。
1.计算品种ATR,一阶差分后求涨跌幅,并计算其60均值。将值以合约代码为列名存入df中,紧接着将进行排序。
def symbol_filtrate(api,data_length,period): "将期货品种全部按照ATR波动幅度进行降序排列" sort = {} df = pd.DataFrame() for i in range(len(symbol_Trade)): "计算品种ATR,一阶差分后求涨跌幅,并计算其60均值" klines = api.get_kline_serial(symbol_Trade[i], period, data_length) atr = ATR(klines,14) klines['tr'] = atr.tr klines['pct_change'] = abs(klines['tr'].pct_change()) for j in range(len(klines)): if math.isinf(klines['pct_change'].iloc[j]) == True: klines['pct_change'].iloc[j] = 0 klines['ATR'] = klines['pct_change'].rolling(60).mean() df[symbol_Trade[i]] = klines['ATR']
我们获取了所有品种日线的2000根k线,分别存入对应的列中。效果如下
run:
KQ.i@SHFE.cu KQ.i@SHFE.au ... KQ.i@CZCE.AP 0 NaN NaN ... NaN 1 NaN NaN ... NaN 2 NaN NaN ... NaN 3 NaN NaN ... NaN4 NaN NaN ... NaN ... ... ... ... ...1995 0.691679 0.698503 ... 0.6835861996 0.683936 0.765273 ... 0.6741241997 0.678784 0.754017 ... 0.6741091998 0.662733 0.767613 ... 0.6796141999 0.654829 0.779521 ... 0.673562
2.上面存入df中的数据,并未按波动率的大小来进行排序,下面这段代码将每一个时间节点上所有品种的ATR均值进行排序,并将每一行的排序结果用合约代码存入新增的trade_code列中。
我们可以看到,这个DataFrame的最后一列就是,当前K线ATR最大的品种,在下一个步骤中,我们将利用比较出来,波动率较大的品种进行开仓。
run:
KQ.i@SHFE.cu KQ.i@SHFE.au ... KQ.i@CZCE.AP trade_code0 NaN NaN ... NaN KQ.i@SHFE.cu1 NaN NaN ... NaN KQ.i@SHFE.cu2 NaN NaN ... NaN KQ.i@SHFE.cu3 NaN NaN ... NaN KQ.i@SHFE.cu4 NaN NaN ... NaN KQ.i@SHFE.cu ... ... ... ... ...1995 0.691679 0.698503 ... 0.683586 KQ.i@SHFE.zn1996 0.683936 0.765273 ... 0.674124 KQ.i@SHFE.zn1997 0.678784 0.754017 ... 0.674109 KQ.i@SHFE.zn1998 0.662733 0.767613 ... 0.679614 KQ.i@SHFE.zn1999 0.654829 0.779521 ... 0.673562 KQ.i@SHFE.zn
(三)将排序后的期货合约进行交易。开仓部分需要我们注意的是这两个变量,k和flag。
k是持仓标志,k==1多仓,k==-1空仓,k==0无持仓,而flag=0代表没有交易,或者第一笔没有平仓,flag=1的时候,也就是平仓时,我们将计算累计盈亏。
1.开仓部分代码:
2.下面是平仓部分的代码。每次平仓时,我们都会将持仓标志设置为0,代表已经平仓目前无持仓,等待执行开仓代码。
(1)平多代码:
"如果有仓位-平仓" if k != 0: if k == 1: "如果持有多单" klines = api.get_kline_serial(symbol_entry, period, data_length) lowest = klines.low.iloc[i - length+30:i].min() if klines.low.iloc[i]<lowest: "如果最低价小于唐奇安通道下轨-平多" print("平多时间:",pd.to_datetime(klines.datetime.iloc[i])+timedelta(hours=8)) exit_buy = lowest symbol_exit = symbol_entry profit_buy = exit_buy - entry_buy a = a + profit_buy statistic_profit.append(a) k = 0 print("开多品种: %s,平多品种:%s,开多价:%s,平多价:%s,多利润:%s"%( symbol_entry,symbol_exit,entry_buy,exit_buy,profit_buy)) print(statistic_profit)
(2)平空代码。flag 设置为1,代表一笔交易完成,在开仓代码中开始计算累计盈亏。
elif k == -1: "如果持有空单" klines = api.get_kline_serial(symbol_entry, period, data_length) highest = klines.high.iloc[i - length+30:i].max() if klines.high.iloc[i]>highest: "如果最高价大于唐奇安通道上轨-平空" print("平空时间:", pd.to_datetime(klines.datetime.iloc[i]) + timedelta(hours=8)) exit_sell = highest symbol_exit = symbol_entry profit_sell = entry_sell - exit_sell a = a + profit_sell statistic_profit.append(a) k = 0 print("开空品种: %s,平空品种:%s,开空价:%s,平空价:%s,空利润:%s"%( symbol_entry,symbol_exit,entry_sell,exit_sell,profit_sell)) print(statistic_profit) flag = 1 "可视化" fig,(ax1,ax2) = plt.subplots(2,1,figsize=(20,10)) ax1.plot(df[symbol_Trade]) ax2.plot(statistic_profit) ax2.legend('statistic_profit') plt.show()
(3)策略启动。
交易品种:全品种(剔除后的)
回测周期:日线
滑点手续费:无
开仓手数:1手
回测区间:2016-04-21 至 2020-02-04
def main(api,data_length,period): "调用所有函数" trade_symbol(api) df = symbol_filtrate(api,data_length,period) trade(api,data_length,period,df)if __name__ == '__main__': "设置参数" api = TqApi() data_length = 2000 period = 86400 "启动策略" main(api,data_length,period)
下面是日线周期的回测效果。
run:
四、策略回测分析。下面将对这个策略进行多周期的回测,分别是2小时、4小时、日线
(一)2小时周期
(二)4小时周期
(三)日线周期
由于只改动周期参数,并未改变其他参数,所以策略适应性不是很好,适应大周期。
总结:
由此可以看出,基于动量效应的量化策略,更具有适应性,也就是普适性。俗话说东方不亮西方亮,每个品种不可能每年都有大行情,因此我们只能守株待兔,跟随市场波动,谁波动大就做谁,这样才能保证不错过任何潜在的大行情,从而避免在波动率较小的品种上耗时间。