Loading ...

利用平均趋向指数辅助MACD策略

Author: Hukybo, Created: 2019-11-16 13:09:45, Updated: 2019-11-20 18:57:57

前言

“趋势是你的朋友”这是每一个交易者都耳熟能详的箴言。但做过交易的朋友可能会有体会,趋势总是在毫无预警地开始并突然结束。那么在CTA策略中,如何抓住趋势并过滤震荡行情,是许多主观和量化交易者孜孜不倦的追求。在本节课程中,我们将以平均趋向指数(ADX)为滤网,分析在它量化交易中的应用。

什么是平均趋向指数

平均趋向指数是衡量趋势的技术工具,简称ADX(average directional indicator),它是由韦尔斯·怀尔德在1978年提出,与其他技术分析工具不同的是,ADX并不能判断多空方向,更不能提示精确的买卖点位,它只是衡量当前趋势的强弱。

img

ADX的默认周期参数是14,通常在K线图的副图中显示。它的值是在0~100之间,数值越大说明上涨或下跌趋势越强力,通常当ADX的值大于40时,说明趋势强力,此时使用趋势交易才具有最大的回报潜力;当ADX的值小于20时,说明趋势疲软,并警告交易者不要使用趋势跟踪交易策略。

ADX的计算方式

ADX的计算方式比较复杂,它涉及到了:价格正向移动距离(+DM)、价格负向移动距离(-DM)、真是波动幅度(TR)、正向方向性指数(+DI), 负向方向性指数(-DI)等很多中间变量:

计算动向变化

  • up:今天的最高价 – 昨天的最高价
  • down:昨天的最低价 – 今天的最低价
  • +DM:如果up大于max(down, 0),则+DM等于up,否则等于零
  • -DM:如果down大于max(up, 0),则-DM等于down,否则等于零

计算真实波幅

  • TR:max(今天最高价与今天最低价的差值,今天最高价与昨天收盘价差值的绝对值,今天最低价与昨天收盘价差值的绝对值)

计算动向指数

  • +DI(14):+DM(14)/TR(14)*100
  • -DI(14):-DM(14)/TR(14)*100

计算ADX

  • DX:((+DI14)- (-DI14)/(+DI14)+(-DI14))*100
  • ADX:MA(DX,14)

虽然ADX的计算比较复杂,但其逻辑还是比较清晰的:up和down分别代表了价格正向和负向移动距离;+DI和-DI分别代表用波动率修正后上涨和下跌趋势。不管趋势是上涨还是下跌,只要存在明显的趋势行情,那么+DI和-DI中总有一个是较大的,因此DX的值会随着趋势的强弱指示在0~100之间;最后ADX则是DX的14天平均线。

当+ DI高于-DI时,表明价格处于上升趋势。 相反,当-DI高于+ DI时,价格处于下降趋势。 交易者可以通过检查同一时间点的ADX值来确定上升趋势或下降趋势的强度。

策略逻辑

在前几节中,我们使用MACD指标创建了一个简单的策略,虽然该策略在趋势行情中表现还可以,但是在震荡行情入不敷出,甚至在长期的震荡行情中资金回撤比较大。因此我们将在本节中将之前的MACD策略加入ADX滤网,我们来看下效果到底如何?

原策略逻辑

  • 多头开仓:DIF大于零轴
  • 空头开仓:DIF小于零轴
  • 多头平仓:DIF向下突破DEA
  • 空头平仓:DIF向上突破DEA

改进后的策略逻辑

  • 多头开仓:DIF大于零轴,并且ADX大于20
  • 空头开仓:DIF小于零轴,并且ADX大于20
  • 多头平仓:DIF向下突破DEA,或者ADX下降
  • 空头平仓:DIF向上突破DEA,或者ADX下降

我们在原策略逻辑基础之上,对开仓和平仓分别加入ADX滤网,控制在行情进入震荡时期的开仓次数。在开仓的时候ADX的数值必须大于指定的数值;当开仓之后一旦ADX下降就平仓出局。整个策略逻辑就设计成一个严进宽出的模式,以此来控制震荡时期的回撤幅度。

策略编写

原始策略

# 回测配置
'''backtest
start: 2015-02-22 00:00:00
end: 2019-10-17 00:00:00
period: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''


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


# 判断数组是否上升
def is_up(arr):
    arr_len = len(arr)
    if arr[arr_len - 1] > arr[arr_len - 2] and arr[arr_len - 2] > arr[arr_len - 3]:
        return True     

    
# 判断数组是否下降
def is_down(arr):
    arr_len = len(arr)
    if arr[arr_len - 1] < arr[arr_len - 2] and arr[arr_len - 2] < arr[arr_len - 3]:
        return True

    
# 判断两根两个数组是否金叉
def is_up_cross(arr1, arr2):
    if arr1[len(arr1) - 2] < arr2[len(arr2) - 2] and arr1[len(arr1) - 1] > arr2[len(arr2) - 1]:
        return True

# 判断两根两个数组是否死叉
def is_down_cross(arr1, arr2):
    if arr1[len(arr1) - 2] > arr2[len(arr2) - 2] and arr1[len(arr1) - 1] < arr2[len(arr2) - 1]:
        return True    

    
# 程序主函数
def onTick():
    exchange.SetContractType("rb000")  # 订阅期货品种
    bar_arr = exchange.GetRecords()  # 获取K线数组
    if len(bar_arr) < long + m + 1:  # 如果K线数组长度太小,就不能计算MACD,所以直接返回跳过
        return
    all_macd = TA.MACD(bar_arr, short, long, m)  # 计算MACD值,返回的是一个二维数组
    dif = all_macd[0]  # 获取DIF的值,返回一个数组
    dif.pop()  # 删除DIF数组最后一个元素
    dea = all_macd[1]  # 获取DEA的值,返回一个数组
    dea.pop()  # 删除DEA数组最后一个元素
    last_close = bar_arr[len(bar_arr) - 1]['Close']  # 获取最新价格(卖价),用于开平仓
    global mp  # 全局变量,用于控制虚拟持仓
    
    # 开多单
    if mp == 0 and dif[len(dif) - 1] > 0:
        exchange.SetDirection("buy")  # 设置交易方向和类型
        exchange.Buy(last_close, 1)  # 开多单
        mp = 1  # 设置虚拟持仓的值,即有多单
    
    # 开空单
    if mp == 0 and dif[len(dif) - 1] < 0:
        exchange.SetDirection("sell")  # 设置交易方向和类型
        exchange.Sell(last_close - 1, 1)  # 开空单
        mp = -1  # 设置虚拟持仓的值,即有空单
        
    # 平多单
    if mp == 1 and is_down_cross(dif, dea):
        exchange.SetDirection("closebuy")  # 设置交易方向和类型
        exchange.Sell(last_close - 1, 1)  # 平多单
        mp = 0  # 设置虚拟持仓的值,即空仓
    
    # 平空单
    if mp == -1 and is_up_cross(dif, dea):
        exchange.SetDirection("closesell")  # 设置交易方向和类型
        exchange.Buy(last_close, 1)  # 平空单
        mp = 0  # 设置虚拟持仓的值,即空仓

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

根据上面更改的策略逻辑,我们可以直接在原始策略的基础之上把ADX滤网加入进去,虽然ADX的计算方法比较复杂,但可以借助talib库只需要几行代码就可以把ADX的值计算出来。因为计算ADX需要用带talib,而计算talib库又需要用到numpy.array数据类型,所以我们需要在代码开头导入talib库和numpy库。

import talib
import numpy as np

在使用talib库计算ADX的时候,一共需要4个参数:最高价、最低价、收盘价、周期参数。所以我们还需要写一个get_data函数,这个函数的目的是从K线数组中提取出最高价、最低价、收盘价。

# 把K线数组转换成最高价、最低价、收盘价数组,用于转换为numpy.array类型数据
def get_data(bars):
    arr = [[], [], []]
    for i in bars:
        arr[0].append(i['High'])
        arr[1].append(i['Low'])
        arr[2].append(i['Close'])
    return arr

然后我们使用numpy库把普通的数组转换为numpy.array类型数据,最后使用talib库就可以计算出ADX的值,具体的写法可以看下面代码中的注释:

np_arr = np.array(get_data(bar_arr)) # 把列表转换为numpy.array类型数据,用于计算ADX的值
adx = talib.ADX(np_arr[0], np_arr[1], np_arr[2], 20);  # 计算ADX的值

在策略逻辑中,需要判断ADX的大小和是否上升下降。判断大小很简单,只需要把ADX具体某一天的值提取出来就可以了,跟判断MACD一样,我们只取倒数第二根K线的ADX值;但判断时候上升下降则需要只取倒数第二根和第三根K线的ADX值。

adx1 = adx_arr[len(adx_arr) - 2]  # 倒数第二根K线的ADX值
adx2 = adx_arr[len(adx_arr) - 3]  # 倒数第三根K线的ADX值

最后修改下单逻辑:

# 开多单
if mp == 0 and dif[len(dif) - 1] > 0 and adx1 > 40:
    exchange.SetDirection("buy")  # 设置交易方向和类型
    exchange.Buy(last_close, 1)  # 开多单
    mp = 1  # 设置虚拟持仓的值,即有多单

# 开空单
if mp == 0 and dif[len(dif) - 1] < 0 and adx1 > 40:
    exchange.SetDirection("sell")  # 设置交易方向和类型
    exchange.Sell(last_close - 1, 1)  # 开空单
    mp = -1  # 设置虚拟持仓的值,即有空单
    
# 平多单
if mp == 1 and (is_down_cross(dif, dea) or adx1 < adx2):
    exchange.SetDirection("closebuy")  # 设置交易方向和类型
    exchange.Sell(last_close - 1, 1)  # 平多单
    mp = 0  # 设置虚拟持仓的值,即空仓

# 平空单
if mp == -1 and (is_up_cross(dif, dea) or adx1 < adx2):
    exchange.SetDirection("closesell")  # 设置交易方向和类型
    exchange.Buy(last_close, 1)  # 平空单
    mp = 0  # 设置虚拟持仓的值,即空仓

完整策略代码

# 回测配置
'''backtest
start: 2015-02-22 00:00:00
end: 2019-10-17 00:00:00
period: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''

# 导入库
import talib
import numpy as np


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


# 把K线数组转换成最高价、最低价、收盘价数组,用于转换为numpy.array类型数据
def get_data(bars):
    arr = [[], [], []]
    for i in bars:
        arr[0].append(i['High'])
        arr[1].append(i['Low'])
        arr[2].append(i['Close'])
    return arr


# 判断两根两个数组是否金叉
def is_up_cross(arr1, arr2):
    if arr1[len(arr1) - 2] < arr2[len(arr2) - 2] and arr1[len(arr1) - 1] > arr2[len(arr2) - 1]:
        return True

# 判断两根两个数组是否死叉
def is_down_cross(arr1, arr2):
    if arr1[len(arr1) - 2] > arr2[len(arr2) - 2] and arr1[len(arr1) - 1] < arr2[len(arr2) - 1]:
        return True    

    
# 程序主函数
def onTick():
    exchange.SetContractType("rb000")  # 订阅期货品种
    bar_arr = exchange.GetRecords()  # 获取K线数组
    if len(bar_arr) < long + m + 1:  # 如果K线数组长度太小,就不能计算MACD,所以直接返回跳过
        return
    all_macd = TA.MACD(bar_arr, short, long, m)  # 计算MACD值,返回的是一个二维数组
    dif = all_macd[0]  # 获取DIF的值,返回一个数组
    dif.pop()  # 删除DIF数组最后一个元素
    dea = all_macd[1]  # 获取DEA的值,返回一个数组
    dea.pop()  # 删除DEA数组最后一个元素
    
    np_arr = np.array(get_data(bar_arr)) # 把列表转换为numpy.array类型数据,用于计算ADX的值
    adx_arr = talib.ADX(np_arr[0], np_arr[1], np_arr[2], 20);  # 计算ADX的值
    adx1 = adx_arr[len(adx_arr) - 2]  # 倒数第二根K线的ADX值
    adx2 = adx_arr[len(adx_arr) - 3]  # 倒数第三根K线的ADX值
    
    last_close = bar_arr[len(bar_arr) - 1]['Close']  # 获取最新价格(卖价),用于开平仓
    global mp  # 全局变量,用于控制虚拟持仓
    
    # 开多单
    if mp == 0 and dif[len(dif) - 1] > 0 and adx1 > 40:
        exchange.SetDirection("buy")  # 设置交易方向和类型
        exchange.Buy(last_close, 1)  # 开多单
        mp = 1  # 设置虚拟持仓的值,即有多单
    
    # 开空单
    if mp == 0 and dif[len(dif) - 1] < 0 and adx1 > 40:
        exchange.SetDirection("sell")  # 设置交易方向和类型
        exchange.Sell(last_close - 1, 1)  # 开空单
        mp = -1  # 设置虚拟持仓的值,即有空单
        
    # 平多单
    if mp == 1 and (is_down_cross(dif, dea) or adx1 < adx2):
        exchange.SetDirection("closebuy")  # 设置交易方向和类型
        exchange.Sell(last_close - 1, 1)  # 平多单
        mp = 0  # 设置虚拟持仓的值,即空仓
    
    # 平空单
    if mp == -1 and (is_up_cross(dif, dea) or adx1 < adx2):
        exchange.SetDirection("closesell")  # 设置交易方向和类型
        exchange.Buy(last_close, 1)  # 平空单
        mp = 0  # 设置虚拟持仓的值,即空仓

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

以上贴出本节已经改进过的策略完整源代码,或者你也可以到 https://www.fmz.com/strategy/174672 发明者量化官网的策略中复制下载,无需配置直接在线回测。

结尾

之前流行过一段话:站在风口猪都会飞,我们做交易也是一样。在大趋势面前,再笨的策略也能分一杯羹,所以我们要做的就是抓住大趋势并在震荡时期控制回撤。ADX与MACD配合使用,可以帮助交易者确认差异,从而提高交易的精度。可以在震荡行情中降低风险,又可以在趋势行情中增加你的盈利潜力,以达到最大限度地降低风险并最大化利润的目的。一句话:要想赚大钱,就一定不要与趋势为敌!


More