价格走势背道而驰,形成底背离,这将预示着价格可能即将上涨。同理,如果价格逐步上涨,但 MACD 并没有跟着上涨,则逐步下跌,与价格走势形成顶背离,这将预示着价格可能会下跌。
值得注意的是,上面的几种方法,虽然逻辑上能站得住脚,但在实际使用时也有反复打脸的时候。为什么有时候会不灵呢?这里面有个悖论,可以试着想一下,如果 MACD 一直有效,那么大家都会来用,那么大家的买卖点位相似的。比如 DIF 向上突破 DEA 时是买入信号,大家都在买,谁会卖出呢?
所以,最终的结果将会是,MACD 越有效,用的人也就越多,当越来越多人使用它的时候,它就会慢慢失效,直到大部分人都放弃使用它,它又重新变的有效。因为人才是金融市场的最终参与者,这既不像物理定律,也不是数学公式,这里面没有整齐划一的规律,这是人与人买卖博弈的最终结果。
注意:市场是人与人之间的博弈,也是交易策略与交易策略之间的博弈,没有恒久有效的策略,也没有恒久无效的策略。
所以,交易界有句俗语:大道至简,重剑无锋。意思就是越简单的东西,越没人信,用的人也就越少,其结果反而越有效,普适性越强。比如:少吃多动至今仍然是减肥的成功秘诀,但真正做的人很少,因为大部分人不相信或者半信半疑没有坚持下来。那么今天我们就用最简单的方法构建一个 MACD 策略。策略逻辑如下:
根据上面的策略逻辑,用 Python 实现交易策略。首先注册并登录 优宽量化 网站,依次点击控制中心 > 策略库 > 新建策略 > 点击右上角下拉菜单选择 Python 语言,开始编写策略,注意看下面代码中的注释。
第 1 步:编写策略框架
策略开发就像盖房子一样,先把地基和框架搭建好,再往里面填充东西。策略框架包含 main
函数和 onTick
函数,main
函数是策略的入口函数,程序先从 main
函数开始逐行执行代码。在 main
函数中是 while
无限循环,重复执行 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 步:计算 MACD
在优宽量化平台中内置了很多常用的指标函数,直接调用指标函数传入参数就可以计算结果,不需要再重新计算。MACD 计算过程是:订阅期货合约 >>> 获取 K 线数组 >>> 调用内置 TA.MACD
指标函数即可。
_C(exchange.SetContractType, "rb000") # 订阅期货品种
bar = _C(exchange.GetRecords) # 获取 K 线数组
if len(bar) < 100: # 如果 K 线数组长度太小就返回
return
macd = TA.MACD(bar, 5, 50, 15) # 计算 MACD 值
dif = macd[0][-2] # 获取 DIF 的值,返回一个数组
dea = macd[1][-2] # 获取 DEA 的值,返回一个数组
注意:在调用内置指标函数之前,需要先判断 K 线数组的长度,因为指标的计算依赖足够的 K 线数据,所以使用 if
语句进行判断,如果 K 线数组长度太小,不足以计算指标,就直接返回。另外由于在计算指标时使用了收盘价数据,在 K 线还没有走完的时候,所计算的结果也会跟着来回变化,直接使用会造成信号闪烁。所以为了解决这个问题,折中的方法是在开平仓条件成立后,在下一根 K 线下单交易。
第 4 步:获取最新价
获取最新价的目的是下单交易,在下单函数 exchange.Buy()
和 exchange.Sell()
中,需要有 2 个参数,第 1 个是下单价格,也就是说在开平仓时必须指定固定的价格,通过获取 K 线数组最后一个元素中的 'Close'
就可以获取最新的价格(卖一价)。
last_close = bar_arr[-1]['Close'] # 获取最新价格(卖价)
第 5 步:下单交易
在开平仓条件中,首先判断当前的持仓状态;然后再判断 DIF 与零轴的位置或 DIF 与 DEA 的交叉状态;接着如果条件成立,就设置交易方向和类型,即:开多、开空、平多、平空;最后使用 Buy
和 Sell
函数下单,下单之后重置虚拟持仓的状态。
global mp # 全局变量,用于控制虚拟持仓
if mp == 1 and dif < dea:
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == -1 and dif > dea:
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and dif > dea:
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
if mp == 0 and dif < dea:
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
# 定义一个全局变量,用于控制虚拟持仓
mp = 0
# 程序主函数
def onTick():
global mp # 使用全局变量 mp
# 订阅期货品种
exchange.SetContractType("rb888")
# 获取 K 线数组
bar = exchange.GetRecords()
# 如果 K 线数组长度太小就返回
if len(bar) < 100:
return
# 计算 MACD 值
macd = TA.MACD(bar, 5, 50, 15)
# 获取 DIF 和 DEA 的值
dif = macd[0][-2]
dea = macd[1][-2]
# 获取最新价格(卖价)
last_close = bar[-1]['Close']
# 根据 dif 和 dea 的关系进行交易操作
if mp == 1 and dif < dea:
# 平多单
exchange.SetDirection("closebuy")
exchange.Sell(last_close - 1, 1)
mp = 0
elif mp == -1 and dif > dea:
# 平空单
exchange.SetDirection("closesell")
exchange.Buy(last_close, 1)
mp = 0
elif mp == 0:
if dif > dea:
# 开多单
exchange.SetDirection("buy")
exchange.Buy(last_close, 1)
mp = 1
elif dif < dea:
# 开空单
exchange.SetDirection("sell")
exchange.Sell(last_close - 1, 1)
mp = -1
# 主函数
def main():
while True:
onTick()
Sleep(1000)
通过本节的学习,相信你已经对 MACD 原理及计算方法有了一定的了解,你可以参照本节中的代码,试着把策略临摹下来进行测试,也可以对策略加以升级改进。
“趋势是你的朋友”这是每一个交易者都耳熟能详的箴言。但做过交易的朋友可能会有体会,趋势总是在毫无预警地开始并突然结束。那么在 CTA 策略中,如何抓住趋势并过滤震荡行情,是许多主观和量化交易者孜孜不倦的追求。在本节课程中,我们将以平均趋向指数(ADX)为滤网,分析在它量化交易中的应用。
平均趋向指数是衡量趋势的技术工具,简称 ADX(average directional indicator),它是由韦尔斯·怀尔德在 1978 年提出。
注意:与其他技术分析工具不同的是,ADX 并不能判断多空方向,更不能提示精确的买卖点位,它只是衡量当前趋势的强弱。
ADX 的默认周期参数是 14,它的值是在 0~100 之间,数值越大说明上涨或下跌趋势越强力。通常当 ADX 的值大于 40 时,说明趋势强力,此时使用趋势交易才具有最大的回报潜力;当 ADX 的值小于 20 时,说明趋势疲软,并警告交易者不要使用趋势跟踪交易策略。
ADX 的计算方式比较复杂,它涉及到了:价格正向移动距离(+DM)、价格负向移动距离(-DM)、真是波动幅度(TR)、正向方向性指数(+DI),负向方向性指数(-DI)等很多中间变量:
计算动向变化:
计算真实波幅:
计算动向指数:
计算 ADX:
虽然 ADX 的计算比较复杂,但其逻辑还是比较清晰的:up 和 down 分别代表了价格正向和负向移动距离;+DI 和-DI 分别代表用波动率修正后上涨和下跌趋势。不管趋势是上涨还是下跌,只要存在明显的趋势行情,那么+DI 和-DI 中总有一个是较大的,因此 DX 的值会随着趋势的强弱指示在 0~100 之间;最后 ADX 则是 DX 的 14 天平均线。
当 +DI 高于 -DI 时,表明价格处于上升趋势。相反,当 -DI 高于 +DI 时,价格处于下降趋势。交易者可以通过检查同一时间点的 ADX 值来确定上升趋势或下降趋势的强度。
在之前章节中,我们使用 MACD 指标创建了一个简单的策略,虽然该策略在趋势行情中表现还可以,但是在震荡行情常常入不敷出,甚至在长期的震荡行情中资金回撤比较大。为了降低策略在震荡时期的试错成本,因此我们将在本节中将之前的 MACD 策略加入 ADX 滤网,我们来看下效果到底如何?
原策略逻辑:
改进后的策略逻辑:
我们在原策略逻辑基础之上,对开仓和平仓分别加入 ADX 滤网,控制行情进入震荡时期的开仓次数。开仓时 ADX 必须是上升的,一旦 ADX 下降就平仓出局。 注意:ADX 的加入,使策略设计成严进宽出模式,控制震荡时期的回撤幅度。
根据上面更改的策略逻辑,我们可以直接在原始策略基础上加入 ADX 滤网。虽然 ADX 计算复杂,但可以借助 talib 库,需要几行代码计算 ADX 值。计算 ADX 需要 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)) # 把列表转换为 numpy.array 类型数据
adx_arr = talib.ADX(np_arr[0], np_arr[1], np_arr[2], 14) # 计算 ADX 的值
策略逻辑中,判断 ADX 大小和上升下降。提取某一天 ADX 值,判断上升下降需要倒数第二根和第三根 K 线 ADX 值。
adx1 = adx_arr[-2] # 倒数第二根 K 线的 ADX 值
adx2 = adx_arr[-3] # 倒数第三根 K 线的 ADX 值
最后修改下单逻辑:
if mp == 1 and (dif < dea or adx1 < adx2):
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == -1 and (dif > dea or adx1 < adx2):
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and dif > dea and adx1 > adx2:
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
if mp == 0 and dif < dea and adx1 > adx2:
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
# 导入库
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 onTick():
_C(exchange.SetContractType, "rb000") # 订阅期货品种
bar = _C(exchange.GetRecords) # 获取 K 线数组
if len(bar) < 100: # 如果 K 线数组长度太小就返回
return
macd = TA.MACD(bar, 5, 50, 15) # 计算 MACD 值
dif = macd[0][-2] # 获取 DIF 的值,返回一个数组
dea = macd[1][-2] # 获取 DEA 的值,返回一个数组
np_arr = np.array(get_data(bar)) # 把列表转换为 numpy.array 类型数据
adx_arr = talib.ADX(np_arr[0], np_arr[1], np_arr[2], 14) # 计算 ADX 的值
adx1 = adx_arr[-2] # 倒数第二根 K 线的 ADX 值
adx2 = adx_arr[-3] # 倒数第三根 K 线的 ADX 值
last_close = bar[-1]['Close'] # 获取最新价格(卖价)
global mp # 全局变量,用于控制虚拟持仓
if mp == 1 and (dif < dea or adx1 < adx2):
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == -1 and (dif > dea or adx1 < adx2):
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and dif > dea and adx1 > adx2:
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
if mp == 0 and dif < dea and adx1 > adx2:
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
def main():
while True:
onTick()
Sleep(1000)
之前流行过一段话:站在风口猪都会飞,我们做交易也是一样。在大趋势面前,再笨的策略也能分一杯羹,所以我们要做的就是抓住大趋势并在震荡时期控制回撤。ADX 与 MACD 配合使用,可以帮助交易者确认差异,提高交易精度。
注意:MACD 是中长线趋势指标,在市场反复震荡时,可能出现错误信号。增加滤网的好处是,在震荡行情中降低风险,在趋势行情中增加盈利潜力,达到降低风险并最大化利润的目的。一句话:要想赚大钱,就一定不要与趋势为敌!
对于初学者来说,策略开发最好从临摹开始。本节我们将重温经典技术分析工具考夫曼均线,并根据其常用的使用方法来构建策略,深度解析每一个计算步骤,以及如何用 Python 和 talib 库去实现它。
我们知道价格变化的速度本身就在变化,传统简单均线受困于固定周期参数,这使得不论市场的走势如何,短期均线灵敏度高,更贴近价格走势,但在市场震荡时期反复转向,造成频繁发出错误开平仓信号;长期均线在趋势判断上更加可靠,但在市场加速上涨或下跌时反应迟钝,造成错过最佳的买卖点。因此虽然传统简单均线可以在一定程度适应行情,但是却很难根据市场变化去进行调整,进而更好的把握趋势。特别在长期震荡行情中,不仅得不到正收益而且付出高额的交易成本,为了解决这个问题,我们引入考夫曼创立的自适应均线。
在《精明交易者》中,作者考夫曼(Kaufman)提出了“自适应移动平均线”,简称 AMA。该均线考虑到了市场价格变化速率,在普通均线的基础上增加了平滑系数,并自适应动态调整均线的灵敏度,可以在慢速趋势和快速趋势之间自我调整。当市场出现盘整、趋势不明显时期,AMA 倾向于慢速移动平均线。当市场波动较大,趋势明显,价格沿一个方向快速移动时,AMA 倾向于快速移动平均线。考夫曼均线本质上是根据一段时间内的价格波动率进行调整,计算出了合适的入场阈值提供了最佳的买卖点位。也就是说,它分为两部分主逻辑,第二部分逻辑在波动率层面做了又一次自适应。从而反应市场真实的趋势,便于快速抓住趋势性上涨和下跌的时机,同时规避市场来回震荡的影响。
有经验的交易者都习惯于在趋势展开的行情中使用快速均线,在震荡较多的行情中使用慢速均线。但如何把这个方法数量化,让程序来区分这两种行情?这里就需要引入“效率”的概念。如果价格一致朝一个方向运行,每天收盘价的变化贡献于总的运行幅度,那么就被称之为高效率;如果价格涨涨跌跌,很多次收盘价的变化相互抵消,那么就被称为低效率。这类似于物理学中的位移,如果价格在10天内上涨了100个点,我们可称为高效率,如果价格在10天内上涨了10个点,我们可以称为低效率。
第 1 步:计算价格效率 价格效率是建立在市场移动的速度和方向以及市场中噪声量的基础之上的,假设价格效率是在0~1之间,0表示市场没有移动,只有噪声;1表示市场只有移动,没有噪声。如果价格在10天内上涨了100个点,每天移动10个点,其价格效率就是:100 / (10 * 10) = 1;如果价格在10天内上涨了10个点,但每天震荡10个点,其价格效率就是:10 / (10 * 10) = 0.1。其计算公式是:首先计算价格变动值,即当根K线价格与前N根K线的价格差的绝对值;然后计算价格波动值,即N根K线内,所有价格变动绝对值的总和;最后计算效率系数,即价格变动值除以价格波动值。
abs(价格 - n 日前价格)
sum(abs(价格 - 上一个交易日价格), n)
第 2 步:计算平滑系数 考夫曼用一系列的移动平均速度来描述平滑系数,其计算方式与EMA类似,根据价格所占权重,重新定义快速和慢速趋势速度系数,比如可以将2天的平均称为快速,30天的平均称为慢速。其中:
2 /(2 + 1) = 2 / 3 = 0.66667
2 /(30 + 1) = 2 / 31 = 0.06452
。它们的差值是:0.60215。2 / (n1 + 1)
2 / (n2 + 1)
上面公式中的n1和n2是交易周期数,并且n1小于n2。默认n1为2,n2为30。最后利用效率比率计算平滑系数,也就是:效率系数 * 0.60215 + 0.06452。可见,当市场波动越大,趋势明显时,平滑系数更加趋向于选择快速趋势系数;反之,在市场震荡盘整,趋势不明显时期,平滑系数更趋向于选择慢速趋势系数。
第 3 步:计算 AMA 值 因为在效率系数太低时,可能会取消交易,所以卡夫曼建议在计算 AMA 值之前,对最后的平滑系数再次乘方。
假设昨天的 AMA 值是 40,当前的价格是 47,它们之间有 7 个点的差值。那么在一个高效市场,其 AMA 值提高将近 3.1 个点,这几乎是差值的一半。在一个低效市场,这个差值几乎不会对 AMA 值产生影响。
根据考夫曼的观点,AMA 相当于平滑指数,如果其方向改变就应该立刻交易。换句话说就是 AMA 上升时应该买进,AMA 下降时应该卖出。不过如果贸然以此做交易信号,可能造成大量的无效信号,因此就需要增加一个合适的滤网,即增加另一根 AMA 均线,以双均线交叉的形式发出买卖信号。
按照以上策略逻辑,开始用代码实现出来。依次打开:控制中心>策略库>新建策略>点击右上角下拉菜单选择 Python 语言,开始编写策略,注意看下面代码中的注释。
第 1 步:抱着不重复造轮子的精神,我们在计算 AMA 的值时,直接使用之前介绍过的 talib 库。因为在使用 talib 计算 AMA 的时候需要用到 numpy.array 数据,所以这里也要导入 numpy 库。
# 导入库
import talib
import numpy as np
第 2 步:编写策略框架,这个在之前的章节已经学习过,一个是 onTick 函数,另一个是 main 函数,其中在 main 函数中无限循环执行 onTick 函数,如下:
# 策略主函数
def onTick():
pass
# 程序入口
def main():
while True: # 进入无限循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
第 3 步:定义虚拟持仓变量。
mp = 0 # 定义一个全局变量,用于控制虚拟持仓
第 4 步:计算 AMA,因为我们是使用 talib 计算 AMA 的值,所以需要用到收盘价的 numpy.array 数据,那么其流程是:订阅期货数据>>>获取 K 线数组>>>把 K 线数组转换为收盘价数组>>>把收盘价数组转换为 numpy.array 数据>>>使用 talib 计算 AMA 值。
# 把 K 线数组转换成收盘价数组,用于计算 AMA 的值
def get_close(r):
arr = []
for i in r:
arr.append(i['Close'])
return arr
_C(exchange.SetContractType, "rb000") # 订阅期货品种
bar_arr = _C(exchange.GetRecords) # 获取 K 线数组
if len(bar_arr) < 100: # 如果 K 线数组长度太小就返回
return
close_arr = get_close(bar_arr) # 把 K 线数组转换成收盘价数组
np_close_arr = np.array(close_arr) # 把列表转换为 numpy.array
ama1 = talib.KAMA(np_close_arr, 10).tolist() # 计算短期 AMA
ama2 = talib.KAMA(np_close_arr, 100).tolist() # 计算长期 AMA
请看上面代码所示,第 1~6 行是 get_close 函数,这个函数的作用是把 K 线数组转换成收盘价数组,主要用于计算 AMA。第 8 行~第 15 行是按照流程计算 AMA 值。注意:计算 AMA 需要一个周期参数,如果 K 线长度小于这个周期参数,就不能计算其 AMA 值。所以在第 10 行和第 11 行,我们加了一个判断 K 线数组的长度,也就是说如果 K 线数据不足以计算 AMA 值时直接跳过。
第 5 步:开平仓,首先获取当前最新价格,因为在使用下单接口函数时,必须指定交易价格。K 线数组最后一个数据的收盘价就是最新价格。然后指定交易的方向类型,即:开多、开空、平多、平空。调用 exchange.SetDirection()
函数,分别传入:“buy”、“sell”、“closebuy”、“closesell”。最后下单之后重置持仓状态 mp 的值。
last_close = close_arr[-1] # 获取最新价格
global mp # 全局变量,用于控制虚拟持仓
if mp == 1 and is_cross(ama2, ama1):
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == -1 and is_cross(ama1, ama2):
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and is_cross(ama1, ama2):
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
if mp == 0 and is_cross(ama2, ama1):
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
# 导入库
import talib
import numpy as np
mp = 0 # 定义一个全局变量,用于控制虚拟持仓
# 把 K 线数组转换成收盘价数组,用于计算 AMA 的值
def get_close(r):
arr = []
for i in r:
arr.append(i['Close'])
return arr
# 判断两根 AMA 交叉
def is_cross(arr1, arr2):
if arr1[-2] < arr2[-2] and arr1[-1] > arr2[-1]:
return True
# 程序主函数
def onTick():
_C(exchange.SetContractType, "rb000") # 订阅期货品种
bar_arr = _C(exchange.GetRecords) # 获取 K 线数组
if len(bar_arr) < 100: # 如果 K 线数组长度过小就直接返回
return
close_arr = get_close(bar_arr) # 把 K 线数组转换成收盘价数组
np_close_arr = np.array(close_arr) # 把列表转换为 numpy.array
ama1 = talib.KAMA(np_close_arr, 10).tolist() # 计算短期 AMA
ama2 = talib.KAMA(np_close_arr, 100).tolist() # 计算长期 AMA
last_close = close_arr[-1] # 获取最新价格
global mp # 全局变量,用于控制虚拟持仓
if mp == 1 and is_cross(ama2, ama1):
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == -1 and is_cross(ama1, ama2):
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and is_cross(ama1, ama2):
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(last_close, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
if mp == 0 and is_cross(ama2, ama1):
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(last_close - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
def main():
while True:
onTick()
Sleep(1000)
上面的代码构建了自适应双均线,并从细节上逐行阐述其中的原理和算法,最后又以双自适应均线创建一个简单的 CTA 策略。整体来看自适应双均线比普通均线更加稳定又不失灵活性。
注意:AMA 本意上是用来替代普通均线,以更好地拟合市场价格走势,单独一根 AMA 并没有质的提升,所以需要额外配置一个过滤器,这个过滤器的选择基于市场波动状况来选择。
之前听过一句话:要想赚大钱必须学会长线持仓,但如果要赚快钱就要学会日内交易。如今的量化交易范围之广令人惊叹,各种交易策略层出不穷,其中最为流行的就是日内交易策略。 日内交易是一种快进快出的交易方式,由于可以控制隔夜风险的特点,得到了很多交易者的推崇和接受。为了帮助大家了解日内交易,丰富策略仓库,本节我们将深入了解商品期货中最为流行的日内策略之一日内高低点突破策略。
日内交易的目的是以更小的损失,来获取当天市场微小的价格波动所带来的利润。它是指开仓和平仓在同一天内或同一交易时间段内完成的交易方式,开仓和平仓可以是单次,也可以是多次,只要是开平仓在同一个交易日前结束就行。 理论上日内交易不承担隔夜的跳空风险,相对来说是一种较完美的低风险交易策略,但实际上并非如此,虽然日内交易回避了跳空所带来的风险,同时也错失了跳空所带来的利润。但如果以正确的方式交易,通过配合不同的交易规则,日内交易往往也能产生丰厚的回报。
我们知道判断上涨趋势最简单的方法是,当前低点比前一个低点更高,当前高点也比前一个高点更高;同理下跌趋势最简单的方法是,当前低点比前一个低点更低,当前高点也比前一个高点更低。但如果仅仅以高低点的比较去判断趋势的涨跌,这未免太过简陋,因为价格可能在一个点上来回跳动几十次甚至上百次,从而导致交易过于频繁。 所以我们需要设定一个价格区间来过滤这些日常杂波,来对简单的高低点突破策略进行完善。我们可以根据历史行情所出现的最高价和最低价,组成一个包含上轨和下轨的通道。根据顺势交易的原则,当价格突破上轨时多头开仓,当价格突破下轨时空头开仓。
第 1 步:导入 time 库
import time
因为日内策略在编写的时候,要判断当前的时间来控制开平仓逻辑,这个策略在设计的时候是:只能在 9 点 30 分至 14 点 50 分之间开仓,14 点 50 分之后全部平仓,其余的时间都过滤掉了。所以就需要引入 time 时间库。
第 2 步:编写策略框架
# 策略主函数
def onTick():
pass
# 程序入口
def main():
while True: # 进入无限循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
第 3 步:设置全局变量
mp = on_line = under_line = 0
在全局变量中,mp 主要用于控制虚拟持仓,on_line 和 under_line 分别记录上轨和下轨。
第 4 步:处理时间
def can_time(hour, minute):
hour = str(hour)
minute = str(minute)
if len(minute) == 1:
minute = "0" + minute
return int(hour + minute)
_C(exchange.SetContractType, "AP888") # 订阅期货品种
bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日级别K线数组
if len(bar_arr) < 10:
return
minute_arr = _C(exchange.GetRecords, PERIOD_M1) # 获取分钟级别K线数组
time_new = minute_arr[-1]['Time'] # 获取当根K线的时间戳
time_local_new = time.localtime(time_new / 1000) # 处理时间戳
hour_new = int(time.strftime("%H", time_local_new)) # 格式化时间戳,并获取小时
minute_new = int(time.strftime("%M", time_local_new)) # 格式化时间戳,并获取分钟
day_new = int(time.strftime("%d", time_local_new)) # 格式化时间戳,并获取日期
time_previous = bar_arr[-2]['Time'] # 获取上根K线的时间戳
previous = time.localtime(time_previous / 1000) # 处理时间戳
day_previous = int(time.strftime("%d", previous)) # 格式化时间戳,并获取日期
处理时间一共用于两个地方:一个是判断当前时间是否在我们规定的交易时间内,如果当前是在这个时间之内,并且已经达到了开仓条件就开仓,如果不是在这个时间之内,并且当前有持仓就平掉所有持仓,达到收盘前平仓的目的。
另一个是判断当前 K 线是不是最新交易日的 K 线,因为我们的策略逻辑是每当新的一天 K 线出现时,就重置上下轨。通过对比两个 K 线的时间戳来重置 on_line 和 under_line 的值,也就是说上下轨通道是在不断变化的。
所以这里获取两个不同时间级别的K线,PERIOD_D1
代表日级别的,在同一个交易日内,K线 Time 属性不会改动;PERIOD_M1
代表分钟级别的,K线 Time 属性会随分钟级别进行变化。
第 5 步:计算高低点上下轨
global mp, on_line, under_line # 引入全局变量
high = bar_arr[-2]['High'] # 获取上根 K 线的最高价
low = bar_arr[-2]['Low'] # 获取上根 K 线的最低价
if day_new != day_previous: # 如果是最新一根 K 线
on_line = high * up # 重置上轨
under_line = low * down # 重置下轨
can_trade = can_time(hour_new, minute_new)
if can_trade < 930: # 如果不是在规定交易的时间内
if high > on_line: # 如果上根 K 线最高价大于上轨
on_line = high * up # 重置上轨
if low < under_line: # 如果上根 K 线最低价小于下轨
under_line = low * down # 重置上轨
if on_line - under_line < 10: # 如果上轨与下轨的差小于 10
return
计算高低点上下轨的逻辑其实非常简单:如果当前是第一根 K 线,那么 on_line 和 under_line 的值分别是最高价和最低价,如果当前 K 线是最新交易日的 K 线,就重置 on_line 和 under_line 的值为最高价和最低价;一旦在规定的交易时间内,on_line 和 under_line 的值就固定不变了,除非在这个时间之外并且如果上根 K 线最高价大于 on_line 就重置为最新的最高价;如果上根 K 线最低价小于 under_line 就重置为最新的最低价。
第 6 步:下单交易 在下单交易之前,我们先获取当前最新价格,因为在下单时需要在函数中传入下单价价格。然后使用 if 语句,根据之前设计的交易逻辑,先是判断当前的持仓状态,然后再判断当前时间状态,以及最新价格与上下轨的相互位置关系,最后下单交易并重置虚拟持仓状态。
close_new = bar_arr[-1]['Close'] # 获取最新价格(卖价),用于开平仓
# 如果持多单,并且价格小于下轨或者非规定的交易时间
if mp > 0 and (close_new < under_line or can_trade > 1450):
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(close_new - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
# 如果持空单,并且价格大于上轨或者非规定的交易时间
if mp < 0 and (close_new > on_line or can_trade > 1450):
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(close_new, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and 930 < can_trade < 1450: # 如果当前无持仓且在交易时间内
if close_new > on_line: # 如果价格大于上轨
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(close_new, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
elif close_new < under_line: # 如果价格小于下轨
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(close_new - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
预测今天下午的天气是很容易的,但是要想预测这个月内的天气却很难。日内交易不需要较长的持仓周期,所承受的市场波动风险较低,尽管这种交易方式不符合每个人的风格,但对于那些风险较为敏感的交易者来说,日内交易还是相当值得深入研究。
# 导入库
import time
# 定义全局变量:虚拟持仓、上轨、下轨
mp = on_line = under_line = 0
# 处理时间函数
def can_time(hour, minute):
hour = str(hour)
minute = str(minute)
if len(minute) == 1:
minute = "0" + minute
return int(hour + minute)
def onTick():
_C(exchange.SetContractType, "AP888") # 订阅期货品种
bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日级别K线数组
if len(bar_arr) < 10:
return
minute_arr = _C(exchange.GetRecords, PERIOD_M1) # 获取分钟级别K线数组
time_new = minute_arr[-1]['Time'] # 获取当根K线的时间戳
time_local_new = time.localtime(time_new / 1000) # 处理时间戳
hour_new = int(time.strftime("%H", time_local_new)) # 格式化时间戳,并获取小时
minute_new = int(time.strftime("%M", time_local_new)) # 格式化时间戳,并获取分钟
day_new = int(time.strftime("%d", time_local_new)) # 格式化时间戳,并获取日期
time_previous = bar_arr[-2]['Time'] # 获取上根K线的时间戳
previous = time.localtime(time_previous / 1000) # 处理时间戳
day_previous = int(time.strftime("%d", previous)) # 格式化时间戳,并获取日期
global mp, on_line, under_line # 引入全局变量
high = bar_arr[-2]['High'] # 获取上根 K 线的最高价
low = bar_arr[-2]['Low'] # 获取上根 K 线的最低价
if day_new != day_previous: # 如果是最新一根 K 线
on_line = high * up # 重置上轨
under_line = low * down # 重置下轨
can_trade = can_time(hour_new, minute_new)
if can_trade < 930: # 如果不是在规定交易的时间内
if high > on_line: # 如果上根 K 线最高价大于上轨
on_line = high * up # 重置上轨
if low < under_line: # 如果上根 K 线最低价小于下轨
under_line = low * down # 重置上轨
if on_line - under_line < 10: # 如果上轨与下轨的差小于 10
return
close_new = bar_arr[-1]['Close'] # 获取最新价格(卖价),用于开平仓
# 如果持多单,并且价格小于下轨或者非规定的交易时间
if mp > 0 and (close_new < under_line or can_trade > 1450):
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(close_new - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
# 如果持空单,并且价格大于上轨或者非规定的交易时间
if mp < 0 and (close_new > on_line or can_trade > 1450):
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(close_new, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and 930 < can_trade < 1450: # 如果当前无持仓且在交易时间内
if close_new > on_line: # 如果价格大于上轨
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(close_new, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
elif close_new < under_line: # 如果价格小于下轨
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(close_new - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
def main():
while True:
onTick()
Sleep(1000)
唐奇安通道(Donchian Channel)是一种技术分析工具,由理查德·唐奇安(Richard Donchian)在20世纪50年代发明。这种策略基于一个简单的概念:在一个上升趋势中,价格通常会在最近一段时间的最高价附近交易;而在下降趋势中,价格往往会接近最近一段时间的最低价。唐奇安通道通过计算一定时间范围内的最高价和最低价,来展示当前市场的趋势强度和潜在的支撑/阻力区域。
原始的唐奇安通道(Donchianchannel
)规则其实很简单,它先设置一条阻力线和一条支撑线,阻力线由过去N天的最高价的最大值形成;支撑线由过去N天的最低价的最小值形成。
唐奇安上阻力线
:由过去N天的当日最高价的最大值
唐奇安下支撑线
:由过去N天的当日最低价的最小值
注意:图中唐奇安通道阻力线和支撑线,在外观上与布林带比较相像,只不过布林带的波动比较灵敏,而唐奇安通道则是直上直下。唐奇安通道可以衡量市场的波动性,一般来说通道宽度越宽,市场的波动就越大,通道宽度越窄,市场的波动性也就越小。
除了具有衡量市场波动率这个功能外,它的主要作用是帮助交易者确定买入和卖出时机。因为唐奇安通道是根据最高价和最低价计算出来的,通道的宽窄又随着价格的变化自动调整,所以大多数时候价格是在通道之内运行,很少突破其上下轨道的。也就是说,价格并不会随意突破阻力线和支撑线,但如果有效突破,那就预示着大行情可能将会出现。此时交易者可以根据支撑和阻力线,确定买进或卖出的具体时机。比如:当价格向上突破阻力线就买入,当价格跌破支撑线就卖出。
之所以原始策略逻辑在早期的金融市场大行其道,是因为最初的市场和市场参与者不太成熟。现如今散户都已经用上了量化交易,策略的同质性,导致策略低效,也就是说如果一个策略使用的人越多,在市场上的效率就越低。所以我们有必要对原始策略逻辑加以改进,让策略更加与众不同。
我们分别从优化开仓方式和止盈止损这两个方面加以改进。首先是开仓方式,做过突破策略的交易者可能会有体会,行情突破阻力线,本来我们是要做多的,结果刚一入场,价格却急转直下,本来看着是一个很好的机会,最后弄了一个措手不及。
大家想一想假突破究竟是怎么来的,怎么总是那么巧合的发生,就好像庄家顶着自己的账号操纵市场一样。其实这是策略同质化的原因,因为前期高低点是固定的,大家都有目共睹,结果大家都等着价格向上突破时买进,该买的都已经买了,此时买力消失,价格自然而然下跌。另外大户也在盯着这个关键点,他也知道散户会在突破时买进,等散户买完不就可以做空割韭菜了么。
所以为了解决这个问题,我们在支撑线和阻力线分别增加一个系数,这样避免与大多数策略参数一致,造成的同质化现象,导致策略低效。另外我们知道,中国的期货市场总是涨的时候涨的缓,跌的时候跌的急,那么可以对支撑线和阻力线设置不同的系数,让策略更合理的适应当前市场环境。
唐奇安上轨
:由过去N天的最高价的最大值上涨系数
唐奇安下轨
:由过去N天的最低价的最小值下跌系数
唐奇安中轨
:(唐奇安上轨 + 唐奇安下轨) / 2
然后是改进止盈止损的方式,原始的唐奇安通道规则是,价格突破阻力线开多单,把止盈止损放在支撑线这个位置;价格跌破支撑线开空单,把止盈止损放在阻力线这个位置。但是这里面有一个问题,假如市场波动率比较大,唐奇安通道上轨与下轨的距离就会加宽,此时就会增加止损的成本和损失一部分浮盈。
那么折中的办法是,可以根据唐奇安通道的上轨和下轨,再计算出一条中轨,这样把止盈止损放在中轨的位置,无论是持有多单还是空单,只要价格反向突破中轨及时止盈止损,这样不仅可以减少止损时所付出的成本,同时保护你未平仓的利润免受重大不利价格波动的影响。
开多
:如果当前无持仓,并且价格突破唐奇安上轨
开空
:如果当前无持仓,并且价格跌破唐奇安下轨
平多
:如果当前持多单,并且价格跌破唐奇安中轨
平空
:如果当前持空单,并且价格突破唐奇安中轨
到目前为止,你应该很好地理解了原始唐奇安通道规则,以及我们将要改进它的方法。现在我们就用代码编写这个交易策略吧。
第 1 步:编写策略框架
策略框架其实就是两个函数,其中 main
函数是整个程序的入口函数,也就是说策略开始执行的时候,会先执行 main
函数;另外一个是 onTick
函数,onTick
只是一个函数的名字,当然你也可以自由命名,onTick
函数里面主要编写策略逻辑。整个框架其实就是在 main
函数中重复执行 onTick
函数。
# 策略主函数
def onTick():
pass
# 程序入口
def main():
while True: # 进入无限循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
第 2 步:定义全局变量 我们这个策略只需要一个控制虚拟持仓的全局变量,所谓的虚拟持仓指的是理论持仓而非真实持仓,无论开仓还是平仓,我们都假设订单已经完全成交。这么做的目的是简化初学者的入门门槛。
# 定义全局变量
mp = 0 # 用于控制虚拟持仓
第 3 步:处理 K 线数据
我们在前面已经定义过,上轨是过去 N 天的最高价的最大值,下轨是过去 N 天的最低价的最小值。要想计算这两个值,首先要先获取基础 K 线数据。但是在使用 GetRecords
方法获取完基础 K 线数据之后,先不要慌着计算上轨和下轨,而是先把数据处理一下。
这里有一点需要注意一下,因为我们在计算上轨和下轨的时候需要 N 个 K 线,如果 K 线数量太少就不能计算了,所以要加一个 if
条件,判断当前 K 线是否满足我们所需要的数量,如果不满足就直接返回,等待下一次循环。另外我们还需要从 K 线数组中提取当前最新价格和上根 K 线的收盘价,最新价格主要用于开平仓,上根 K 线收盘价主要用于判断开平仓信号。有的朋友可能会问,为什么不直接使用最新的价格来判断开平仓信号呢?这是因为如果使用最新价格来判断,就可能出现信号反复的问题,同时也为了规避未来函数和偷价这些常见的量化交易问题,所以我们的策略在设计上是:当前 K 线出信号,下根 K 线发单。
_C(exchange.SetContractType, "rb000") # 订阅期货品种
bar_arr = _C(exchange.GetRecords) # 获取 K 线数组
if len(bar_arr) < 60:
return
close_new = bar_arr[-1]['Close'] # 获取最新价格(卖价)
close_last = bar_arr[-2]['Close'] # 上根 K 线收盘价
bar_arr.pop() # 删除数组最后一个数据
第 4 步:计算上轨、下轨、中轨
在优宽量化交易平台中,已经内置了 talib
库中的 Highest
函数和 Lowest
函数,所以我们直接调用这两个函数就可以计算上轨和下轨的值。但因为我们是使用上根 K 线收盘价为基准,来判断它与上轨、下轨、中轨的位置关系来开平仓,所以在计算上轨和下轨之前需要先删除 K 线数组中的最后一个元素。
bar_arr.pop() # 删除数组最后一个数据
on_line = TA.Highest(bar_arr, 55, 'High') * 0.999 # 计算唐奇安上轨
under_line = TA.Lowest(bar_arr, 55, 'Low') * 1.001 # 计算唐奇安下轨
middle_line = (on_line + under_line) / 2 # 计算唐奇安中轨
第 5 步:下单交易
要想在函数内使用外部的全局变量,需要在使用这个变量之前,先用 global
关键字把变量引入。注意下面代码中的注释,整个代码流程是使用 if
语句,然后根据我们之前定义的策略逻辑来编写。有两个地方需要注意,一个是在下单之前需要先设置下单的类型方向,也就是先调用 SetDirection
函数。另一个是在下单之后,要把虚拟持仓变量 mp
重新赋值。
global mp # 引入全局变量
# 如果持多单, 并且价格小于下轨
if mp > 0 and close_last < middle_line:
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(close_new - 1, 1) # 平多单
mp = 0 # 设置虚拟持仓的值, 即空仓
# 如果持空单, 并且价格大于上轨
if mp < 0 and close_last > middle_line:
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(close_new, 1) # 平空单
mp = 0 # 设
by2022 企业微信加不上啊啊啊啊啊啊
雨幕(youquant) 您好,企业微信满了,您加这个微信: https://www.youquant.com/upload/asset/1780ac4e8b9064c9d7d9a.png