Loading ...

动态阶梯突破策略

Author: Hukybo, Created: 2019-11-02 14:18:51, Updated: 2019-11-04 10:51:57

摘要

在前面几节课程中,我们学习了基于指标来构建简单的策略 https://www.fmz.com/digest-topic/4503 ,其中在计算指标时用到了talib库,大大简化了策略编写难度。但有时候我们写策略可能会用到talib库中没有的计算方法,那么今天我们就通过动态阶梯突破策略,来学习下这种策略是如何实现的。

什么是突破策略

我们知道,期货市场的价格以趋势和震荡交替的方式演变,如果我们只使用一种方法抓住趋势,就能赚到趋势行情的钱。那么,用什么方式来抓住趋势呢?比较简单的一种方法就是用突破策略。通过设置价格的上下轨,或者支撑位、压力位,当价格超过上轨,我们认为行情即将启动,开仓做多,反之亦然。

市面上有很多不同种类的突破策略,大致可以分为:形态突破(包括:双肩型、头肩型、颈线、趋势线等等)、指标突破(均线、KDJ、ATR等等)、通道突破(新高和新低、支撑线和阻力线)、量能突破(成交量、能量潮)。其中在量化交易中,最常用的是:指标突破和通道突破。

突破策略理论

在逻辑学中,有一个“充分不必要条件”的概念,也就是说:如果有A不一定有B,但如果有B就必定有A。那么B就是A的充分而不必要的条件,即充分不必要条件,A是B的必要不充分条件。所以站在结果的角度讲,价格突破关键点位后未必形成趋势,但趋势上涨或下跌必然会突破其间的关键价格位置。

另外,从突破的原因上讲,市场涨跌取决于买卖双方实力对比。当价位冲破上一时段的最高点时,在上一时段任何价位做空头的人都无一例外被套牢,它当中肯定有一部分要认赔平仓出局,反过来又给升势推波助澜。相反,当行情跌破上一时段的最低价时,在上一时段任何一点做多头的统统都出现浮动亏损,其中一定有部分要止损作平仓卖出,正好对跌势落井下石。

策略逻辑

阶梯策略,这是一个比较土的名字,因为其在图表上的外形类似台阶而得名,最初的灵感来自于阶梯止损。相信有过实盘经验的人应该深有体会:当市场进行横向整理或者摇摆不定的时候,对交叉类系统的打击很大,往往会买在高点,卖在低点。如果行情一直持续,则会出现连续亏损,连续的亏损信号将对交易者造成严重的心理负担和资金回撤压力。如下图:

img

通过利用,通道技术则可以过滤或者减少价格反复缠绕,减少部分虚假信号,对于降低无效交易有巨大帮助。本策略并非传统的通道策略。而是根据前期最高价和最低价,反向建立自适应通道。这里提到的自适应是指回溯日期会根据我们的逻辑进行调整,具体来说本策略是由市场波动的变动率来变化。

大部分通道策略的组成是由两个因子决定,第一个就是中轨,然后再根据中轨算出通道宽度,即上下轨。比如常见的布林带通道( BollingerBand ),先是由一条均线当中心线,而通道宽度是由标准差所决定。阶梯策略的通道并不是以中轨得来,与之相反的是:先根据市场波动率计算出通道上下轨,然后再根据通道宽度算出中轨。

另外,在判定最高点和最低点的 K 线条数取决于我们愿意给交易多少变化空间,我们用来越多的 K 线条数来确定上下轨,我们给予程序化交易的变化空间越大,相应的,在触发止损前盈利回撤的幅度也会越大。使用越近的高点或低点,止损被触发的速度也越快。想让通道窄一点就设定小一点,想让通道宽一点就设定大一点。

img

设置通道

  • 上轨:如果当根K线最低价小于上根K线最低价,上轨就等于前N根K线最高价的最高价
  • 下轨:如果当根K线最高价大于上根K线最高价,下轨就等于前N根K线最低价的最低价
  • 中轨:上轨和下轨的平均值

入场条件

  • 多头入场:如果当前没有持仓,并且价格大于上轨,买入开仓。
  • 空头入场:如果当前没有持仓,并且价格小于下轨,卖出开仓。

出场条件

  • 多头出场:如果当前持有多单,并且价格小于中轨,卖出平仓。
  • 空头出场:如果当前持有空单,兵器价格大于中轨,买入平仓。

为了避免过度拟合,在设计策略的时候,只给定了一个参数。虽然仅有一个参数,但却不失策略在市场中的灵活性。不仅如此,阶梯策略既能适应国内外商品期货,还可以应用于 A 股 ETF ,包括外盘 ETF 和反向杠杆 ETF,以及外汇市场。当然只是适应部分品种,从全品种统计来看,这是一个普适性比较强的策略。

策略编写

根据上面的策略逻辑,我们可以在发明者量化交易平台上实现交易策略。依次打开:fmz.com > 登录 > 控制中心 > 策略库 > 新建策略 > 点击右上角下拉菜单选择Python语言,开始编写策略,注意看下面代码中的注释。

第1步:编写策略框架

这个在之前的章节已经学习过,一个是onTick函数,另一个是main函数,其中在main函数中无限循环执行onTick函数,如下:

def onTick():
    pass

def main():
    while True:
        onTick()
        Sleep(1000)

第2步:定义全局变量

首先定义策略中的上轨、下轨,因为我们的策略中上轨、下轨时是根据一定的条件来计算的,也就是说:当前的最高价大于前面K线的最高价时,才重新计算下轨;当前的最低价小于前面K线的最低价时,才重新计算上轨。所以我们必须把上轨和下轨变量定义在onTick主函数外面。

up_line = 0  # 上轨
under_line = 0  # 下轨
mp = 0  # 用于控制虚拟持仓

另外,我们还需要定义全局变量虚拟持仓mp,策略运行之初默认是空仓mp=0,当开多单后把虚拟持仓重置为mp=1,当开空单后把虚拟持仓重置为,mp=-1,当平多单或空单后把虚拟持仓重置为mp=0。这样我们在判断构建逻辑获取仓位时,只需要判断mp的值就可以了。

第3步:计算上轨、下轨、中轨

因为在计算这些数据之前,肯定要先获取历史的K线基础数据,这些基础数据的获取方式也很简单,首先订阅期货品种,然后调用发明者量化API中的GetRecords方法。接着因为在计算上轨和下轨的时候需要用到talib库中的Highest和Lowest方法,这两个方法都要传入周期参数,但如果K线数据不够的时候,就不能正常计算其值,所以在这里就要判断K线数据的长度,如果K线的长度不足以计算其值时,就直接返回跳过。

接着,我们分别获取当前K线和上根K线的最高价和最低价,通过对比当前K线最高价与上根K线最高价来定义下轨的值,如果当前的最高价大于前面K线的最高价时,就重新计算下轨;同理如果当前的最低价小于前面K线的最低价时,就重新计算上轨。最后上轨和下轨的平均值就是中轨。

exchange.SetContractType("rb000")  # 订阅期货品种
bars = exchange.GetRecords()  # 获取K线数组
if len(bars) < cycle_length + 1:  # 如果K线数组的长度太小,所以直接返回
    return
close0 = bars[len(bars) - 1].Close;  # 获取当根K线收盘价
high0 = bars[len(bars) - 1].High;  # 获取当根K线最高价
high1 = bars[len(bars) - 2].High;  # 获取上根K线最高价
low0 = bars[len(bars) - 1].Low;  # 获取当根K线最低价
low1 = bars[len(bars) - 2].Low;  # 获取上根K线最低价
highs = TA.Highest(bars, cycle_length, 'High');  # 获取前cycle_length根K线最高价的最高价
lows = TA.Lowest(bars, cycle_length, 'Low');  # 获取前cycle_length根K线最低价的最低价
global up_line, under_line, mp  # 使用全局变量
if high0 > high1:  # 如果当根K线最高价大于上根K线最高价
    under_line = lows  # 把下轨重新赋值为:前cycle_length根K线最低价的最低价
if low0 < low1:  # 如果当根K线最低价小于上根K线最低价
    up_line = highs  # 把上轨重新赋值为:前cycle_length根K线最高价的最高价
middle_line = (lows + highs) / 2;  # 计算中轨的值

这里有一个地方需要注意,可能细心的朋友已经发现了,我们在计算上轨和下轨的时候,用到了talib库中的Highest和Lowest函数,因为在发明者量化软件中已经内置了这两个常用的函数,所以我们不需要像前几节那样在策略开头导入talib库,并且在使用内置函数的时候,其写法也略有不同,具体可以查看下方的代码。

第4步:下单交易

有了上轨、下轨、中轨的值,就可以配合当前的最新价格开平仓交易了,我们可以回过头再看下之前设计的交易逻辑:如果当前没有持仓,并且价格大于上轨 * 1.05,买入开仓。如果当前没有持仓,并且价格小于下轨 * 0.95,卖出开仓。如果当前持有多单,并且价格小于中轨,卖出平仓。如果当前持有空单,兵器价格大于中轨,买入平仓。

if mp == 0 and close0 > up_line:  # 如果当前空仓,并且最新价大于上轨
    exchange.SetDirection("buy")  # 设置交易方向和类型
    exchange.Buy(close0, 1)  # 开多单
    mp = 1  # 设置虚拟持仓的值,即有多单
    
if mp == 0 and close0 < under_line:  # 如果当前空仓,并且最新价小于下轨
    exchange.SetDirection("sell")  # 设置交易方向和类型
    exchange.Sell(close0 - 1, 1)  # 开空单
    mp = -1  # 设置虚拟持仓的值,即有空单
    
if mp > 0 and close0 < middle_line:  # 如果当前持有多单,并且最新价小于中轨
    exchange.SetDirection("closebuy")  # 设置交易方向和类型
    exchange.Sell(close0 - 1, 1)  # 平多单
    mp = 0  # 设置虚拟持仓的值,即空仓
    
if mp < 0 and close0 > middle_line:  # 如果当前持有空单,并且最新价大于中轨
    exchange.SetDirection("closesell")  # 设置交易方向和类型
    exchange.Buy(close0, 1)  # 平空单
    mp = 0  # 设置虚拟持仓的值,即空仓

下单交易使用if语句,如果条件为真,就先设置交易方向和类型,即:开多、开空、平多、平空。然后调用发明者量化软件中的Buy或Sell下单函数,最后下单之后重置虚拟持仓的状态。

完整策略代码

'''backtest
start: 2015-02-22 00:00:00
end: 2019-10-29 00:00:00
period: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''


# 外部参数
cycle_length = 100


# 定义全局变量
up_line = 0  # 上轨
under_line = 0  # 下轨
mp = 0  # 用于控制虚拟持仓


def onTick():
    exchange.SetContractType("rb000")  # 订阅期货品种
    bars = exchange.GetRecords()  # 获取K线数组
    if len(bars) < cycle_length + 1:  # 如果K线数组的长度太小,所以直接返回
        return
    close0 = bars[len(bars) - 1].Close;  # 获取当根K线收盘价
    high0 = bars[len(bars) - 1].High;  # 获取当根K线最高价
    high1 = bars[len(bars) - 2].High;  # 获取上根K线最高价
    low0 = bars[len(bars) - 1].Low;  # 获取当根K线最低价
    low1 = bars[len(bars) - 2].Low;  # 获取上根K线最低价
    highs = TA.Highest(bars, cycle_length, 'High');  # 获取前cycle_length根K线最高价的最高价
    lows = TA.Lowest(bars, cycle_length, 'Low');  # 获取前cycle_length根K线最低价的最低价
    global up_line, under_line, mp  # 使用全局变量
    if high0 > high1:  # 如果当根K线最高价大于上根K线最高价
        under_line = lows  # 把下轨重新赋值为:前cycle_length根K线最低价的最低价
    if low0 < low1:  # 如果当根K线最低价小于上根K线最低价
        up_line = highs  # 把上轨重新赋值为:前cycle_length根K线最高价的最高价
    middle_line = (lows + highs) / 2;  # 计算中轨的值
    
    if mp == 0 and close0 > up_line:  # 如果当前空仓,并且最新价大于上轨
        exchange.SetDirection("buy")  # 设置交易方向和类型
        exchange.Buy(close0, 1)  # 开多单
        mp = 1  # 设置虚拟持仓的值,即有多单
        
    if mp == 0 and close0 < under_line:  # 如果当前空仓,并且最新价小于下轨
        exchange.SetDirection("sell")  # 设置交易方向和类型
        exchange.Sell(close0 - 1, 1)  # 开空单
        mp = -1  # 设置虚拟持仓的值,即有空单
        
    if mp > 0 and close0 < middle_line:  # 如果当前持有多单,并且最新价小于中轨
        exchange.SetDirection("closebuy")  # 设置交易方向和类型
        exchange.Sell(close0 - 1, 1)  # 平多单
        mp = 0  # 设置虚拟持仓的值,即空仓
        
    if mp < 0 and close0 > middle_line:  # 如果当前持有空单,并且最新价大于中轨
        exchange.SetDirection("closesell")  # 设置交易方向和类型
        exchange.Buy(close0, 1)  # 平空单
        mp = 0  # 设置虚拟持仓的值,即空仓
    

def main():
    while True:
        onTick()
        Sleep(1000)

本文的策略代码已经发布到发明者量化策略中心,可以复制链接 https://www.fmz.com/strategy/172610 ,无需配置直接在线回测。

结尾

在量化投资领域,突破策略是技术分析中很重要的一部份,它的观念在于市场价格穿透了之前的价格压力或支撑,继而形成一股新的趋势,我们的目标就是在突破发生时能够确认并建立部位以获取趋势的利润。

通道策略有他的弹性,可顺可逆。把通道缩窄一点,就可以做顺势;或是把通道拉宽一点,就可以做逆势,或是再搭配其他指标写成其他的交易策略等。可以发现其实通道无所不在,只是通道的取法各有不同罢了。

换个角度来看,我们都只是在寻找两条线,支撑跟压力线来决定进场作多或作空,通道的上限与下限即是这种概念。总之,对量化交易而言,不管是什么策略,可以赚钱就是好策略!


More