经典恒温器策略Python版

Author: 扫地僧, Created: 2019-12-21 17:54:53, Updated: 2023-12-07 20:17:00

img

摘要

趋势行情不会永远持续下去,事实上市场大部分时间都处于震荡行情,所以才会有人希望能得到一种交易策略,既可以用在趋势行情,也可以用在震荡行情。那么今天我们就用发明者量化交易平台,构建一个趋势和震荡行情通用的经典恒温器策略。

策略简介

提到恒温器可能会有人想到汽车发动机与水箱之间的恒温器。当发动机温度低时,恒温器是关闭状态,此时发动机和水箱的水是不相通的,直到发动机温度升高,达到最佳机油润滑效果;当发动机温度升高到一定阈值时,节温器是开启状态,此时发动机和水箱的水形成循环,并流经风扇开启降温模式,直到达到发动机最佳工作温度。

那么恒温器策略也类似这个原理,并且延用了这个名字。它通过波动指数作为阈值,将市场分为趋势行情和震荡行情,自动对两种不同的行情使用对应的交易逻辑,有效弥补了趋势策略在震荡行情中的不适应。

市场波动指数

如何把市场划分为趋势行情和震荡行情,也就成了这个策略的关键,恒温器策略引入了市场波动指数(Choppy Market Index),简称CMI。它是一个用来判断市场走势类型的技术指标。通过计算当前收盘价与N周期前收盘价的差值与这段时间内价格波动的范围的比值,来判断目前的价格走势是趋势还是震荡。

CMI的计算公式为:

CMI=(abs(Close-ref(close,(n-1)))*100/(HHV(high,n)-LLV(low,n))

其中,abs是绝对值,n是周期数。

策略逻辑

一般来说CMI的值在0~100区间,值越大,趋势越强。当CMI的值小于20时,策略认为市场处于震荡模式;当CMI的值大于等于20时,策略认为市场处于趋势模式。

整个策略逻辑,可以简化的写成下面这样:

  • 如果CMI < 20,执行震荡策略;
  • 如果CMI ≥ 20,执行趋势策略;

策略架构就是这么简单,剩下的就是把震荡策略的内容和趋势策略的内容,填充到这个框架里面。

策略编写

依次打开:fmz.cn网站 > 登录 > 控制中心 > 策略库 > 新建策略 > 点击右上角下拉菜单选择Python语言,开始编写策略,注意看下面代码中的注释。

第1步:编写策略框架 这个在之前的章节已经学习过,一个是onTick函数,另一个是main函数,其中在main函数中无限循环执行onTick函数,如下:

# 策略主函数
def onTick():
    pass


# 程序入口
def main():
    while True:  # 进入无限循环模式
        onTick()  # 执行策略主函数
        Sleep(1000)  # 休眠1秒

第2步:定义虚拟持仓变量

mp = 0  # 定义一个全局变量,用于控制虚拟持仓

虚拟持仓的作用主要是用来控制策略仓位,当策略运行之初默认是空仓mp=0,当开多单后把虚拟持仓重置为mp=1,当开空单后把虚拟持仓重置为,mp=-1,当平多单或空单后把虚拟持仓重置为mp=0。这样我们在判断构建逻辑获取仓位时,只需要判断mp的值就可以了。虚拟持仓的特点时编写简单,快速迭代策略更新,一般用于回测环境中,假设每一笔订单都完全成交,但在实际交易中常用的还是真实持仓。

第3步:获取基础数据

exchange.SetContractType("rb000")  # 订阅期货品种
bar_arr = exchange.GetRecords()  # 获取K线数组
if len(bar_arr) < 100:  # 如果K线少于100根
    return  # 直接返回
close0 = bar_arr[-1]['Close']  # 获取最新价格(卖价),用于开平仓
bar_arr.pop()  # 删除K线数组最后一个元素,策略采用开平仓条件成立,下根K线交易模式

首先使用发明者量化API中的SetContractType方法订阅期货品种。接着使用GetRecords方法获取K线数组,因为有时候K线数量太少,导致无法计算一些数据,所以我们判断如果K线少于100根,就直接返回等待下一次新数据。

然后我们从K线数组中获取最新的卖一价,这个主要用于使用开平仓函数时传入价格参数。最后因为我们的策略采用当前K线开平仓条件成立,在下根K线交易的模式,所以需要删除K线数组最后一个元素。这样做有2个好处:第1个可以使回测绩效更接近于实盘;第2个是避免未来函数和偷价这些常见的策略逻辑错误。

计算市场波动指数CMI

close1 = bar_arr[-1]['Close']  # 最新收盘价
close30 = bar_arr[-30]['Close']  # 前30根K线的收盘价
hh30 = TA.Highest(bar_arr, 30, 'High')  # 最近30根K线的最高价
ll30 = TA.Lowest(bar_arr, 30, 'Low')  # 最近30根K线的最低价
cmi = abs((close1 - close30) / (hh30 - ll30)) * 100  # 计算市场波动指数

根据CMI的计算公式,我们需要4个数据,分别是:最新收盘价、前30根K线的收盘价、最近30根K线的最高价、最近30根K线的最低价。前两个很简单,可以直接从K线数组中获取。最后两个则需要调用发明者量化内置的talib指标库TA.Highest和TA.Lowest,这两个指标函数需要传入三个参数,分别是:K线数据、周期、属性。最后当前收盘价与前30根K线的收盘价的差值与这段时间内价格波动的范围的比值就是市场波动指数CMI。

定义宜卖市和宜买市

high1 = bar_arr[-1]['High']  # 最新最高价
low1 = bar_arr[-1]['Low']  # 最新最低价
kod = (close1 + high1 + low1) / 3  # 计算关键价格

if close1 > kod:
    be = 1
    se = 0
else:
    be = 0
    se = 1

在震荡市场中,通常存在一种现象:如果今天价格上涨的话,那么明天的价格下跌的概率更大。而今天价格如果下跌的话,那么明天的价格上涨的概率更大,而这也正是震荡市场的特性。所以这里首先定义一个关键价格(最高价+最低价+收盘价的平均值)。这些数据都可以在K线数据中直接获取。如果当前价格大于关键价格,那么明天应该震荡看空。相反的,如果当前价格小于关键价格,那么明天应该震荡看多。

计算震荡行情的进出场价格

# 计算10根K线ATR指标
atr10 = TA.ATR(bar_arr, 10)[-1]

# 定义最高价与最低价3日均线
high2 = bar_arr[-2]['High']  # 上根K线最高价
high3 = bar_arr[-3]['High']  # 前根K线最高价
low2 = bar_arr[-2]['Low']  # 上根K线最低价
low3 = bar_arr[-3]['Low']  # 前根K线最低价
avg3high = (high1 + high2 + high3) / 3  # 最近3根K线最高价的均值
avg3low = (low1 + low2 + low3) / 3  # 最近3根K线最低价的均值

# 计算震荡行情的进出场价格
open1 = bar_arr[-1]['Open']  # 最新开盘价
if close1 > kod:  # 如果收盘价大于关键价格
    lep = open1 + atr10 * 3
    sep = open1 - atr10 * 2
else:
    lep = open1 + atr10 * 2
    sep = open1 - atr10 * 3
lep1 = max(lep, avg3high)  # 计算震荡市多头进场价格
sep1 = min(sep, avg3low)  # 计算震荡市空头进场价格

首先计算10根K线ATR指标,同样也是直接调用发明者量化的内置talib库中的TA.ATR即可。为了防止假突破,导致策略来回止损,因此加入了一个最高价与最低价3日均线滤网来避免这种情形,分别从K线数组中获取最近3根K线的值求其平均就可以了。

有了以上计算步骤,最后就可以计算震荡行情中的进出场价格了,其原理是以开盘价为中心,上下加减最近10根K线的真实波动幅度,形成一个开多和开空的价格通道。为了使策略更加符合市场走势,在做多和做空时分别设置了不同的空间。

在震荡行情中看多,只代表价格上涨的概率更大一些,并不是指价格一定就会上涨。所以把做多的阈值设置的比较低一点,把做空的阈值设置的比较高一点。同理在震荡行情中看空,只代表价格下跌的概率更大一些,并不是指价格一定就会下跌。所以把做空的阈值设置的比较低一点,把做多的阈值设置的比较高一点。

计算趋势行情的进场价格

boll = TA.BOLL(bar_arr, 50, 2)
up_line = boll[0][-1]
mid_line = boll[1][-1]
down_line = boll[2][-1]

在处理趋势行情的进出场价格上,沿用了布林带策略,当价格向上突破布林带上轨时多头开仓,当价格向下突破布林带下轨时空头开仓,平仓方式则是以当前价格与布林中轨的位置关系来判断。

完整策略代码

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

mp = 0  # 定义一个全局变量,用于控制虚拟持仓

# 策略主函数
def onTick():
    exchange.SetContractType("rb000")  # 订阅期货品种
    bar_arr = exchange.GetRecords()  # 获取K线数组
    if len(bar_arr) < 100:  # 如果K线少于100根
        return  # 直接返回
    close0 = bar_arr[-1]['Close']  # 获取最新价格(卖价),用于开平仓
    bar_arr.pop()  # 删除K线数组最后一个元素,策略采用开平仓条件成立,下根K线交易模式
    
    # 计算CMI指标用以区分震荡市与趋势市
    close1 = bar_arr[-1]['Close']  # 最新收盘价
    close30 = bar_arr[-30]['Close']  # 前30根K线的收盘价
    hh30 = TA.Highest(bar_arr, 30, 'High')  # 最近30根K线的最高价
    ll30 = TA.Lowest(bar_arr, 30, 'Low')  # 最近30根K线的最低价
    cmi = abs((close1 - close30) / (hh30 - ll30)) * 100  # 计算市场波动指数

    # 震荡市中收盘价大于关键价格为宜卖市,否则为宜买市
    high1 = bar_arr[-1]['High']  # 最新最高价
    low1 = bar_arr[-1]['Low']  # 最新最低价
    kod = (close1 + high1 + low1) / 3  # 计算关键价格
    if close1 > kod:
        be = 1
        se = 0
    else:
        be = 0
        se = 1
    
    # 计算10根K线ATR指标
    atr10 = TA.ATR(bar_arr, 10)[-1]

    # 定义最高价与最低价3日均线
    high2 = bar_arr[-2]['High']  # 上根K线最高价
    high3 = bar_arr[-3]['High']  # 前根K线最高价
    low2 = bar_arr[-2]['Low']  # 上根K线最低价
    low3 = bar_arr[-3]['Low']  # 前根K线最低价
    avg3high = (high1 + high2 + high3) / 3  # 最近3根K线最高价的均值
    avg3low = (low1 + low2 + low3) / 3  # 最近3根K线最低价的均值
    
    # 计算震荡行情的进场价格
    open1 = bar_arr[-1]['Open']  # 最新开盘价
    if close1 > kod:  # 如果收盘价大于关键价格
        lep = open1 + atr10 * 3
        sep = open1 - atr10 * 2
    else:
        lep = open1 + atr10 * 2
        sep = open1 - atr10 * 3
    lep1 = max(lep, avg3high)  # 计算震荡市多头进场价格
    sep1 = min(sep, avg3low)  # 计算震荡市空头进场价格
    
    # 计算趋势行情的进场价格
    boll = TA.BOLL(bar_arr, 50, 2)
    up_line = boll[0][-1]
    mid_line = boll[1][-1]
    down_line = boll[2][-1]
    
    global mp  # 引入全局变量
    if cmi < 20:  # 如果是震荡行情
        if mp == 0 and close1 >= lep1 and se:
            exchange.SetDirection("buy")  # 设置交易方向和类型
            exchange.Buy(close0, 1)  # 开多单
            mp = 1  # 设置虚拟持仓的值,即有多单
        if mp == 0 and close1 <= sep1 and be:
            exchange.SetDirection("sell")  # 设置交易方向和类型
            exchange.Sell(close0 - 1, 1)  # 开空单
            mp = -1  # 设置虚拟持仓的值,即有空单
        if mp == 1 and (close1 >= avg3high or be):
            exchange.SetDirection("closebuy")  # 设置交易方向和类型
            exchange.Sell(close0 - 1, 1)  # 平多单
            mp = 0  # 设置虚拟持仓的值,即空仓
        if mp == -1 and (close1 <= avg3low or se):
            exchange.SetDirection("closesell")  # 设置交易方向和类型
            exchange.Buy(close0, 1)  # 平空单
            mp = 0  # 设置虚拟持仓的值,即空仓
    else:  # 如果是趋势行情
        if mp == 0 and close1 >= up_line:
            exchange.SetDirection("buy")  # 设置交易方向和类型
            exchange.Buy(close0, 1)  # 开多单
            mp = 1  # 设置虚拟持仓的值,即有多单
        if mp == 0 and close1 <= down_line:
            exchange.SetDirection("sell")  # 设置交易方向和类型
            exchange.Sell(close0 - 1, 1)  # 开空单
            mp = -1  # 设置虚拟持仓的值,即有空单
        if mp == 1 and close1 <= mid_line:
            exchange.SetDirection("closebuy")  # 设置交易方向和类型
            exchange.Sell(close0 - 1, 1)  # 平多单
            mp = 0  # 设置虚拟持仓的值,即空仓
        if mp == -1 and close1 >= mid_line:
            exchange.SetDirection("closesell")  # 设置交易方向和类型
            exchange.Buy(close0, 1)  # 平空单
            mp = 0  # 设置虚拟持仓的值,即空仓
            
            
# 程序入口        
def main():
    while True:  # 进入无限循环模式
        onTick()  # 执行策略主函数
        Sleep(1000)  # 休眠1秒

点击复制完整策略源码 https://www.fmz.cn/strategy/179014 无需配置直接回测

策略回测

为了将回测结果尽量接近实盘交易,这里把手续费设置为交易所的2倍,开仓和平仓各加2跳的滑点,回测的数据品种为螺纹钢指数,交易品种为螺纹钢主力连续。固定1手开仓。以下是在1小时级别的初步回测绩效报告。 img img img img

结尾

从资金曲线和数据来看,该策略表现良好,在螺纹钢品种回测中,除了2017年下半年有较大回撤外,整体资金曲线是稳步向上的。综上,恒温器策略的自动调节交易方式,为大家应对震荡行情提供了一定的思路。感兴趣的读者,可以根据自己的理解适当修改,做进一步的深入研究。


相关内容

更多内容