导
读
关注小黎的朋友们大家好!本期是“基于tqsdk(天勤)编译的程序化交易策略”系列第十二期,本文正文内容近3200字,阅读用时约15分钟。对之前内容感兴趣的朋友可以点击下方超链接或查看历史推送。
“基于均线交叉与通道突破相结合的交易系统”/“市场情绪日内交易策略”/“做市商与逆向选择”/“鳄鱼交易系统”/“短线突破交易系统”/“逆势交易“/“AMA自适应均线”
本文源自:濮元恺的《量化投资技术分析实战:解码股票与期货交易模型》(电子工业出版社)中《CCI通道+自适应系统,驯服商品期货波动》一文。
领取本文py源代码方式(任选其一)
1.加入VIP
2.转发本文至朋友圈,扫描文末二维码添加小黎为好友发送截图
3.转发本文至朋友圈,将截图发送至本公众号后台
CCI指标基本思路解析
1
CCI(CommodityChannellndex,可译为商品通道指数)顺势指标本体结构非常简单,它是由唐纳德·R·兰伯特DonaldLambert所创,原意是用指标值+阈值线的方法,表示股价是否已超出常态分布范围,这个阈值线一般是正负100。它属于超买、超卖类指标中较特殊的一种,波动于正无限大和负无限小之间。但实际上我们可以将它作为一个趋势突破类系统,因为在中长期市场上,动量效应比反转效应更加明显(特指期货市场),所以如果能够识别到一个显著的上升/下跌趋势开始,然后介入,还是有较为可观的收益的。CCI指标在这里可以起到一些作用,我们计算出CCI值之后,再结合两个阈值线,即可得到做空信号。- def CCI(df, n):
- """
- 顺势指标
- Args:
- df (pandas.DataFrame): Dataframe格式的K线序列
- n (int): 周期n
- Returns:
- pandas.DataFrame: 返回的DataFrame包含1列, 是"cci", 代表计算出来的CCI值
- Example::
- # 获取 CFFEX.IF1903 合约的顺势指标
- from tqsdk import TqApi, TqSim
- from tqsdk.ta import CCI
- api = TqApi(TqSim())
- klines = api.get_kline_serial("CFFEX.IF1903", 24 * 60 * 60)
- cci = CCI(klines, 14)
- print(list(cci["cci"]))
- # 预计的输出是这样的:
- [..., 98.13054698810375, 93.57661788413617, 77.8671380173813, ...]
- """
- typ = (df["high"] + df["low"] + df["close"]) / 3
- ma = tafunc.ma(typ, n)
- def mad(x):
- return np.fabs(x - x.mean()).mean()
- md = typ.rolling(window=n).apply(mad, raw=True) # 平均绝对偏差
- new_df = pd.DataFrame(data=list((typ - ma) / (md * 0.015)), columns=["cci"])
- return new_d
复制代码 CCI原来的公式第一步是计算typ(typicalprice)价格,这里需要计算“最高价+最低价+收盘价”,意义是考虑到了最高价和最低价,而实际上这完全是在引入噪声,没有任何实际意义。要知道任何价格都不可能比close有效,其次是open价格包含的信息有一定含金量,可以作为一个日内基准价格,除此之外随意引入high和low价格,然后加以处理大都属于画蛇添足。所以我们改为引用close,在不改动模型公式的情况下,以最简单的方法修改TP的计算方式。如下图所示。
CCI系统和价格关系
紧接着就是用公式计算其他变量,你可以理解为:通过计算得到价格n周期均值,再计算价格和均值的累计离差值绝对值np.fabs(x - x.mean()).mean(),然后再获得每周期离差值,typ.rolling(window=n).apply(mad, raw=True);最后得到一个以0轴为中心,大致在正负100,有时在正负200波动的指标值。这就是按窗口期归一化的效果,可以方便我们使用阈值线构成交易条件。CCI本身并不神秘,我们对它做的改进有一定创新意义。
ATR风险头寸自适应调整
2
- atr = ATR(klines5,20).iloc[-1].atr
- atr_short = ATR(klines5,5).iloc[-1].atr
- atr_long = ATR(klines5,60).iloc[-1].atr
- atr_ratio = atr_short/atr_long
复制代码 atr_ratio是一个在1附近波动的值,它用于判断当前价格波动在于一个什么样的状态,根据价格波动的状态来调整持仓手数。- money = 20000 #初始金额
- margin = 0.07 #保证金率
- #头寸
- lots = int(int(money/(margin*klinesday.iloc[-1].open*quote.volume_multiple))/atr_ratio)
复制代码
ATR_Ratio和收盘价对比效果(以沪深300指数为例)
通过构建短期ATR指标来表示短期波动率,构建长期ATR指标表示长期波动率,用短期除以长期,获得短期的相对波动强度ATR_Ratio。这个相对强度是高度回复性的变量,所以它应该和交易头寸呈反比(负相关)。用这种简单的方式即可实现对于波动率的捕捉,并构建波动率和开仓头寸的负相关。有很多交易者认为,通过ATR调节头寸是至关重要的,而且几乎是程序程序化交易系统的灵魂所在,这决定了我们无论在什么时候出入场,都可以获得一个适合该点的风险头寸,让交易头寸再也不是冷冰冰的固定值,而是一个风险调节值。我们通过一个固定参数面(追踪止损参数是1~10的范围,0.5为步长)测试了该模块是否存在,对于单品种的意义如表3-1所示。我们同样也在商品期货多品种上进行了全品种组合测试,这个组合包括28个品种,得到效果如表3-2所示。
可以看到,ATR调整头寸牺牲了一些净利润,但是换来了夏普比率提升和平均回撤下降,考虑到该模块如此有效且简单,建议投资者在资金足够切分不同份额的情况下,保留模型中的该模块。
ER效率系数自适应调整突破阈值
3
ER效率系数Efficiency_Ratio是方向性和波动率的比值。ER较高时意味着趋势较为显著,它不仅考虑方向,还考虑波动,当波动率过大时,ER系数相应降低,对于趋势的强度描述性也随之下降。- def efficiency_ratio(klines,n=20):
- '''
- 传入一个K线对象,返回ER系数,参数默认20
- '''
- direction = klines.iloc[-1].close - klines.iloc[-n].close
- volatility = sum(abs(klines.iloc[-22:-1].close.diff().dropna(axis=0,how='all')))
- ER = abs(direction/volatility)
- return ER
复制代码 我们的设计思路是:在ER系数过大的时候对系统预警,让突破阈值线变得更加困难,降低开仓的可能性,当ER系数较小的时候,让突破变得更加容易。需要注意的是ER系数调节仅在做多和平空时起效,经过初步测试确认了这种特性,这说明上波动和下波动不同,上波动在过于流畅的时候,往往就是一轮升势结束的时候,下波动在ER效率系数这套系统中未见这样的显著调节效果,所以它在空头模型中,只能用于平空仓,而不能调节开仓,但平仓有时候比开仓更加有效。如下图所示。
ER效率系数与收盘价对比效果(以螺纹钢指数合约为例)
我们仅仅做了这样简单的分段调整,并不十分精确,但是在多头和空头都起到了显著作用。我们可以这样理解该模块的作用:既然ER效率系数表示上涨强度的数据,过大的ER系数必然意味着风险,特别是在商品期货领域这一现象更加显著。因为ER效率系数也和ATR_Ratio一样是一个回复性的值,所以我们可以赌其突破失败,随着ER系数的上升给CCI值的突破设定更加困难的障碍。我做了一些测试来证明空头部分有显著的性能改善,而这个测试的方法不是针对改进前和改进后,而是针对ER效率系数和平空条件是正相关还是负相关,得到结论如下图所示。
模块调节结论
参考股票多因子模型的对比方法,如果要证明一个因子是否有显著收益,应该对其进行分组,然后观察分组后收益率或夏普比率是否呈现单调性(分组资金曲线分层效果)。为了证明A方法有效,可以证明与A方向相反的方法会产生负面效果,以证明A方法有显著作用。与ER正相关的是平空条件随着ER增加而设置阈值线为20、30、40的效果,负相关相反阈值线为40、30、20,ER不启用为:30、30、30(即在任何情况下都以30作为平空仓的阈值线)。出场点和入场点一样重要,在商品期货市场上,因为入场点的争夺导致同质化,而出场点的自适应调节此时甚至可以发挥出更大效用。总体来看,以动量方式应用CCI系统比Aberration系统效果略好一些,主要体现在收益风险比方面,该系统值得读者们关注,并加以正确的迭代方式。
回测效果图:
基于tqsdk(天勤)编译的CCI通道+自适应系统策略源码(回测模式):- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- __author__ = 'XIAOLI'
- from tqsdk import TqApi,TargetPosTask,TqBacktest,TqSim
- from talib import CCI
- from tqsdk.tafunc import time_to_datetime
- from tqsdk.ta import ATR,EMA
- from datetime import date
- def efficiency_ratio(klines,n=20):
- '''
- 传入一个K线对象,返回ER系数,参数默认20
- '''
- direction = klines.iloc[-1].close - klines.iloc[-n].close
- volatility = sum(abs(klines.iloc[-22:-1].close.diff().dropna(axis=0,how='all')))
- ER = abs(direction/volatility)
- return ER
- def cross_up(serise,k):
- '''
- 判断上穿函数,ticks:serise,k:int
- '''
- if serise.iloc[-2] k:
- return True
- else:
- return False
- def cross_down(serise,k):
- '''
- 判断下穿函数,ticks:serise,k:int
- '''
- if serise.iloc[-2] >= k and serise.iloc[-1] < k:
- return True
- else:
- return False
- #Params
- symbol = 'SHFE.rb2001' #投资标的
- NATRstop = 5 #N倍ATR硬止损
- api = TqApi(account=TqSim(init_balance=50000),backtest=TqBacktest(start_dt=date(2019, 5, 1), end_dt=date(2019, 12, 31)),web_gui='192.168.31.229:80')
- klines5 = api.get_kline_serial(symbol=symbol,duration_seconds=300) #5分钟K线
- klinesday = api.get_kline_serial(symbol=symbol,duration_seconds=86400) #日K线
- #ticks = api.get_tick_serial(symbol=symbol) #tick序列
- position = api.get_position(symbol=symbol) #持仓
- quote = api.get_quote(symbol=symbol) #盘口
- target_pos = TargetPosTask(api=api,symbol=symbol) #调整持仓
- counter = 0 #计数器
- is_stop = False #止损标识符
- money = 20000 #初始金额
- margin = 0.07 #保证金率
- #头寸
- lots = int(int(money/(margin*klinesday.iloc[-1].open*quote.volume_multiple))/(ATR(klinesday,5).iloc[-1].atr/ATR(klinesday,60).iloc[-1].atr))
- #index
- cci = CCI(klines5.iloc[:-1].close,klines5.iloc[:-1].close,klines5.iloc[:-1].close,100) #CCI
- ER = efficiency_ratio(klines5.iloc[:-1]) #ER
- atr = ATR(klines5.iloc[:-1],14).iloc[-1].atr #ATR
- Highest_high_20 = max(klines5.iloc[-21:-1].high) #20周期高点
- Lowest_low_20 = min(klines5.iloc[-21:-1].low) #20周期低点
- while True:
- api.wait_update()
- if api.is_changing(klinesday.iloc[-1],'datetime'):
- #更新头寸
- lots = int(int(money/(margin*klinesday.iloc[-1].open*quote.volume_multiple))/(ATR(klinesday,5).iloc[-1].atr/ATR(klinesday,60).iloc[-1].atr)) #ATR比率分配头寸
- print('今天合适开仓手数:',lots)
- if api.is_changing(klines5.iloc[-1],'datetime'):
- #更新指标
- cci = CCI(klines5.close,klines5.close,klines5.close,100) #CCI
- ER = efficiency_ratio(klines5) #ER
- atr = ATR(klines5.iloc[:-1],14).iloc[-1].atr #ATR
- Highest_high_20 = max(klines5.iloc[-21:-1].high) #20周期高点
- Lowest_low_20 = min(klines5.iloc[-21:-1].low) #20周期低点
- print('CCI:',round(cci.iloc[-1],3),'ER:',abs(round(ER,3)),'ATR:',round(atr,3),'20周期高点:',Highest_high_20,'20周期低点:',Lowest_low_20)
- #计数器控制循环,止损后30根K线内不开仓,30根后止损标识符复位
- if is_stop:
- counter += 1
- if counter > 30:
- is_stop = False
- counter = 0
- if api.is_changing(quote):
- now = time_to_datetime(quote.datetime)
- #确保策略运行在可交易的时间内,请自行设置
- if now.hour > 9 and now.hour < 23:
- #无持仓且未止损过
- if position.pos_long == 0 and position.pos_short == 0 and is_stop == False:
- #最新价大于20周期高点
- if quote.last_price > Highest_high_20:
- #CCI多头组合ER系数进行判断
- #多头强势时,需要自适应增加入场难度,因为此多为行情衰竭点
- if (abs(ER) > 0.8 and cross_up(cci,120)) or ((abs(ER) < 0.8 or abs(ER) > 0.4) and cross_up(cci,100)) or (abs(ER) < 0.4 and cross_up(cci,80)):
- target_pos.set_target_volume(lots)
- myhigh = quote.last_price #记录开仓后的最高价用于跟踪止损
- #最新价小于20周期低点
- elif quote.last_price < Lowest_low_20:
- #CCI下穿,开空,这里没有自适应
- if cross_down(cci,-90):
- target_pos.set_target_volume(-lots)
- mylow = quote.last_price #记录开仓后的最低价用于跟踪止损
- #多头持仓
- if position.pos_long != 0:
- #更新持仓后记录的最高价
- if quote.last_price > myhigh:
- myhigh = quote.last_price
- print('最高价更新:',myhigh)
- #多头跟踪止损
- if myhigh - position.open_price_long > NATRstop * atr:
- if quote.last_price NATRstop * atr:
- if quote.last_price >= mylow + NATRstop * atr:
- target_pos.set_target_volume(0)
- is_stop = True
- #CCI上穿平仓,存在自适应
- if (abs(ER) > 0.8 and cross_up(cci,30)) or ((abs(ER) < 0.8 or abs(ER) > 0.4) and cross_up(cci,40)) or (abs(ER) < 0.4 and cross_up(cci,50)):
- #不是背离
- if quote.last_price > Lowest_low_20:
- target_pos.set_target_volume(0)
- #反转
- if quote.last_price > Highest_high_20:
- if (abs(ER) > 0.8 and cross_up(cci,120)) or ((abs(ER) < 0.8 or abs(ER) > 0.4) and cross_up(cci,100)) or (abs(ER) < 0.4 and cross_up(cci,80)):
- target_pos.set_target_volume(lots)
- myhigh = quote.last_price
复制代码
领取本文py源代码方式(任选其一)
1.加入VIP
2.转发本文至朋友圈,扫描文末二维码添加小黎为好友发送截图
3.转发本文至朋友圈,将截图发送至本公众号后台您不知道如何在电脑上部署实现天勤量化的Python环境?想获取完整版的py代码文件?您有交易经验和想法却又不知道如何实现?想学习Python量化投资课程?或者您有各种迫切的需求,欢迎联系小黎
本文代码为“小黎说财经”原创,转载需获得同意,并在显要位置标明文章来源。
作者:小黎说财经
公众号设计/排版:宁晴
点“在看”给排版小妹一朵小黄花
|
|