虚拟持仓的值, 即空仓 if mp == 0: # 如果当前无持仓 if close_last > on_line: # 如果价格大于上轨 exchange.SetDirection(“buy”) # 设置交易方向和类型 exchange.Buy(close_new, 1) # 开多单 mp = 1 # 设置虚拟持仓的值, 即有多单 elif close_last < under_line: # 如果价格小于下轨 exchange.SetDirection(“sell”) # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值, 即有空单
### 6.6.5 完整策略代码
```python
mp = 0 # 定义全局变量,用于控制虚拟持仓
def onTick():
_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() # 删除数组最后一个数据
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 # 计算唐奇安中轨
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 # 设置虚拟持仓的值, 即空仓
if mp == 0: # 如果当前无持仓
if close_last > on_line: # 如果价格大于上轨
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(close_new, 1) # 开多单
mp = 1 # 设置虚拟持仓的值, 即有多单
elif close_last < under_line: # 如果价格小于下轨
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(close_new - 1, 1) # 开空单
mp = -1 # 设置虚拟持仓的值, 即有空单
# 程序入口
def main():
while True: # 进入无线循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
唐奇安通道之所以流传至今,一定有它独特的道理。但随着市场的转变,我们也要与时俱进而不是贸然使用。随着你对交易认知的提升,你会发现改进的方法非常多。这就是交易的魅力所在,每一位交易者都应该是一位探险者,大胆探索小心求证,长此以往就一定能有适合自己的交易方法。最后配合合理的风险管理,方能成为一位成功的交易者。
本节我们以唐奇安通道为例,讲解了通道类策略的基本原理,其实还有类似的通道策略,比如布林带策略,Dual Thrust 策略等,大家可以以此为依据,了解通道策略具体的使用方法。
在期货市场,价格呈现一切。几乎所有的技术分析,如均线、布林线、MACD、KDJ等等,这些都是以价格为基础,通过特定的方法计算。包括基本面分析也是如此,通过分析近期和远期价差、期货和现货升贴水、上下游库存等等数据,计算当前价格是否合理,并预估未来的价格。既然如此,为什么不直接研究价格呢?今天我们讲的菲阿里四价策略就是完全根据价格来做出卖决定。
菲阿里是一位日本的交易者,主要偏向于商品期货日内主观交易。其大名远扬是在2001年的罗宾斯(ROBBINS-TAICOM)期货冠军大赛中,以1098%的成绩获得冠军,并且在之后的两年里再以709%、1131%的成绩夺冠。从成绩就知道,菲阿里是一个非常优秀的交易者。幸运的是,菲阿里在《1000%的男人》这本书中详尽叙述了他的交易方法,菲阿里四价策略正是后人总结他的交易方法。虽然只是从外在形式加上自己的理解模仿了一部分,并不代表菲阿里全部交易精髓,但至少可以帮助我们在构建策略时拓展思路。
菲阿里四价策略是一种比较简单的趋势性日内交易策略,四价分别是指:昨天高点、昨天低点、昨日收盘价、今天开盘价。从书中的交易笔记来看,菲阿里不使用任何分析工具,而是大量应用阻溢线的概念,也就是通常我们所说的阻力线和支撑线。
注意:对于阻力线和支撑线的定义,他使用的是昨日最高价和昨日最低价,可以视为昨天价格的波动范围,这也意味着多头或空头只有足够的力量时,才会有效突破阻力线和支撑线。并且一旦有突破这个波动范围,则说明价格背后的动能较大,后续走势可能会沿最小阻力线运动的概率较大。
如果开盘价处于阻力线和支撑线之间,当价格向上突破阻力线就建立多头,当价格向下突破支撑线就建立空头。如果一切顺利,则一直持仓到收盘。这样做的好处是,符合了充分非必要条件,即突破不一定上涨/下跌,但上涨/下跌一定会突破,也就是始终守在行情发生的必经之路伺机而动,因为较大行情的上涨和下跌一旦出现,势必要突破阻力线和支撑线的。
当然这也是出错率最高的方法,因为很多时候价格只是暂时性的突破了关键位置,如果贸然开仓可能会面临价格随时反向运动的风险。这时就需要设置一些过滤条件,限制假突破造成的来回开平仓问题。另外在交易周期上也尽量避免波动过于混乱的 5 分钟周期以下 K 线。
但是开仓后,盈利了还好,如果遇到亏损,总不能从小亏损一直积累到大亏损,才在收盘时平仓吧,这样显然不合理。所以我们对于平仓有两种处理方式:收盘平仓和止损平仓。如果 K 线上破高点或下破低点后又回到原来的区间内,就要考虑止损了。
实际上菲阿里在主观交易中,还有很多交易方法,包括:开盘后先涨后跌,跌破开盘价做空,止损设在之前上涨的最高点;开盘后先跌后涨,突破开盘价做多,止损设在之前下跌的最低点。动手能力强的朋友可以在自己的策略中增改。
到这里你会发现,对于一天的价格走势来说,收盘价相对于开盘价的涨跌,其概率接近于 50%。菲阿里的交易方法在胜率上就利于不败之地,再加上行情顺利的时候一直持仓到收盘,在行情不符合自己的预期时及时止损。形成了截断亏损,让利润奔跑的正向交易方式,这也是长期交易下来积累利润的原因。
第 1 步:编写策略框架
# 策略主函数
def onTick():
pass
# 程序入口
def main():
while True: # 进入无限循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
第 2 步:导入库
import time # 用于转换时间格式
因为我们这个是日内策略,到时候需要判断 K 线时间戳,如果有持仓,并且临近收盘时平仓出局。那么我们就直接 import time 就可以了。
第 3 步:获取基础数据
_C(exchange.SetContractType, "rb888") # 订阅期货品种
bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日线数组
if len(bar_arr) < 2: # 如果小于 2 根 K 线
return # 返回继续等待数据
yh = bar_arr[-2]['High'] # 昨日最高价
yl = bar_arr[-2]['Low'] # 昨日最低价
today_open = bar_arr[-1]['Open'] # 当日开盘价
菲阿里四价需要用到四个数据:昨日最高价、昨日最低价、昨日收盘价、当日开盘价。因为这些都是日线级别的数据,所以我们在使用 GetRecords
的时候,可以直接传入 PERIOD_D1
参数,表明我们要获取的是日 K,这样无论你的策略加载的是哪个周期的数据,它始终获取的都是日线级别的数据。
另外,细心的朋友可能已经发现,为什么这一次在调用 GetRecords
的时候,代码的写法跟以前不一样?这次我们使用的是优宽量化平台内置的重试函数 _C()
。使用这个函数的好处是,该函数会一直调用指定函数到成功返回,这样可以避免直接使用 GetRecords
函数时,没有获取到数据导致报错的情况。
第 4 步:处理时间和获取最新价格
bar_arr = _C(exchange.GetRecords) # 获取当前设置周期 K 线数组
current = bar_arr[-1]['Time'] # 获取当前 K 线时间戳
local = time.localtime(current / 1000) # 处理时间戳
hour = int(time.strftime("%H", local)) # 格式化时间戳,并获取小时
minute = int(time.strftime("%M", local)) # 格式化时间戳,并获取分钟
price = bar_arr[-1]['Close'] # 获取最新价格
既然是获取当前的时间,那么肯定是使用获取当前设置周期的 K 线数据更为合适,所以需要重新使用一次 GetRecords
,这次我们同样也是使用 _C()
重试函数,在不传入参数的情况下,就是默认获取当前设置周期的 K 线数组。另外,获取最新价格的目的是,计算交易逻辑和下单。
第 5 步:处理时间
def trade_time(hour, minute):
minute = str(minute)
if len(minute) == 1:
minute = "0" + minute
return int(str(hour) + minute)
注意:之所以创建这个函数,是因为我们在开仓之前,需要判断当前时间,是否在我们规定的交易时间之内,以及有持仓的时候,当前时间是否临近收盘。在第 4 步中,我们已经获取到了当前 K 线小时和分钟,为了方便比较,我们采用小时加分钟的方法,比如:
trade_time
返回的结果就是 905trade_time
返回的结果就是 1430第 6 步:设置虚拟持仓
mp = 0
第 7 步:设置止损
# 设置多头止损
if today_open / yh > 1.005: # 如果当天开盘价大于昨天最高价
long_stop = yh # 设置多头止损价为昨天最高价
elif today_open / yh < 0.995: # 如果当天开盘价小于昨天最高价
long_stop = today_open # 设置多头止损价为当天开盘价
else: # 如果当天开盘价接近昨天最高价
long_stop = (yh + yl) / 2 # 设置多头止损为昨天中间价
# 设置空头止损
if today_open / yl < 0.995: # 如果当天开盘价小于昨天最低价
short_stop = yl # 设置空头止损价为昨天最低价
elif today_open / yl > 1.005: # 如果当天开盘价大于昨天最低价
short_stop = today_open # 设置空头止损价为当天开盘价
else: # 如果当天开盘价接近昨天最低价
short_stop = (yh + yl) / 2 # 设置空头止损为昨天中间价
在大多数情况下,价格突破阻力线和支撑线,就把止损设置到当前开盘价这个位置。但是这里面有个问题:如果当天开盘价大于阻力线,而价格往下走;或者当天开盘价小于支撑线,而价格
往上走,会造成逻辑错误,导致频繁开平仓。为了解决这个问题,我们需要根据当天开盘与阻力线和支撑线的位置关系,分别设置不同的止损价格。如果当天开盘价大于昨天最高价 0.5%,那么就把多头的止损设置在昨天的最高价;如果当天开盘价在阻力线和支撑线之间,那么多头的止损价格还是当天的开盘价;如果当天开盘价接近于昨天最高价,那么就把多头的止损设置在昨天的中间价。设置空头的止损也是根据这个道理。
第 8 步:下单交易
trading = trade_time(hour, minute)
if mp > 0: # 如果当前持有多单
# 如果当前价格小于多头止损线,或者超过规定的交易时间
if price < long_stop or trading > 1450:
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(price - 1, 1) # 平多单
mp = 0 # 重置虚拟持仓
if mp < 0: # 如果当前持有空单
# 如果当前价格大于空头止损线,或者超过规定的交易时间
if price > short_stop or trading > 1450:
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(price, 1) # 平空单
mp = 0 # 重置虚拟持仓
# 如果当前无持仓,并且在规定的交易时间内
if mp == 0 and 930 < trading < 1450:
if price > yh: # 如果当前价格大于昨天最高价
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(price, 1) # 开多单
mp = 1 # 重置虚拟持仓
elif price < yl: # 如果价格小于昨天最低价
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(price - 1, 1) # 开空单
mp = -1 # 重置虚拟持仓
注意:为了避免逻辑错误,最好是把平仓逻辑写到开仓逻辑的前面。
import time # 导入库, 用于转换时间格式
mp = 0 # 虚拟持仓
def trade_time(hour, minute):
minute = str(minute)
if len(minute) == 1:
minute = "0" + minute
return int(str(hour) + minute)
def onTick():
_C(exchange.SetContractType, "rb888") # 订阅期货品种
bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日线数组
if len(bar_arr) < 2: # 如果小于 2 根 K 线
return # 返回继续等待数据
yh = bar_arr[-2]['High'] # 昨日最高价
yl = bar_arr[-2]['Low'] # 昨日最低价
today_open = bar_arr[-1]['Open'] # 当日开盘价
cur_bar_arr = _C(exchange.GetRecords, PERIOD_M1) # 获取当前设置周期 K 线数组
current = cur_bar_arr[-1]['Time'] # 获取当前 K 线时间戳
local = time.localtime(current / 1000) # 处理时间戳
hour = int(time.strftime("%H", local)) # 格式化时间戳, 并获取小时
minute = int(time.strftime("%M", local)) # 格式化时间戳, 并获取分钟
price = bar_arr[-1]['Close'] # 获取最新价格
global mp
# 设置多头止损
if today_open / yh > 1.005: # 如果当天开盘价大于昨天最高价
long_stop = yh # 设置多头止损价为昨天最高价
elif today_open / yh < 0.995: # 如果当天开盘价小于昨天最高价
long_stop = today_open # 设置多头止损价为当天开盘价
else: # 如果当天开盘价接近昨天最高价
long_stop = (yh + yl) / 2 # 设置多头止损为昨天中间价
# 设置空头止损
if today_open / yl < 0.995: # 如果当天开盘价小于昨天最低价
short_stop = yl # 设置空头止损价为昨天最低价
elif today_open / yl > 1.005: # 如果当天开盘价大于昨天最低价
short_stop = today_open # 设置空头止损价为当天开盘价
else: # 如果当天开盘价接近昨天最低价
short_stop = (yh + yl) / 2 # 设置多头止损为昨天中间价
# 下单交易
trading = trade_time(hour, minute)
if mp > 0: # 如果当前持有多单
# 如果当前价格小于多头止损线, 或者超过规定的交易时间
if price < long_stop or (trading > 1450 and trading < 1500):
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(price - 1, 1) # 平多单
mp = 0 # 重置虚拟持仓
if mp < 0: # 如果当前持有空单
# 如果当前价格大于空头止损线, 或者超过规定的交易时间
if price > short_stop or (trading > 1450 and trading < 1500):
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(price, 1) # 平空单
mp = 0 # 重置虚拟持仓
# 如果当前无持仓, 并且在规定的交易时间内
if mp == 0 and (900 < trading < 1450 or 2130 < trading < 2300):
if price > yh: # 如果当前价格大于昨天最高价
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(price + 1, 1) # 开多单
mp = 1 # 重置虚拟持仓
elif price < yl: # 如果价格小于昨天最低价
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(price - 1, 1) # 开空单
mp = -1 # 重置虚拟持仓
def main():
while True: # 无限循环
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
虽然距离比赛结束已经有近 20 年之久了, 但以今天的眼光看那时的交易, 毫无过时感。但需要注意的是, 策略仅仅作为思路拓展, 不能直接用于实盘。对于菲阿里策略来说, 它提供了一个很好的入场参考工具, 我们可以根据自己对市场的认知做更深的开发。
在前面几节课程中,我们学习了基于指标来构建简单的策略,其中在计算指标时用到了 talib
库,大大简化了策略编写难度。但有时候我们写策略可能会用到 talib
库中没有的计算方法,那么今天我们就通过简易波动 EMV 策略,来学习下这种策略是如何实现的。
与其他技术指标不同,简易波动(Ease of Movement Value)反映的是价格、成交量、人气的变化,它是一种将价格与成交量变化相结合的技术,它通过衡量单位成交量的价格变动,形成一个价格波动指标。当市场人气聚集,交易活跃时提示买入信号;当成交量低迷,市场能量即将耗尽时提示卖出信号。
注意:简易波动 EMV 根据等量图和压缩图的原理设计而成,它的核心理念是:市场价格仅在发生趋势转折或即将转折时,才会消耗大量能量,外在表现就是成交量变大。当价格在上升的过程中,由于推波助澜的作用,不会消耗太多的能量。虽然这个理念与量价同升的观点相悖,但的确有其独特的地方。
第 1 步:计算 mov_mid
$\text{movmid} = \frac{{\text{TH} + \text{TL}}}{2} - \frac{{\text{TY} + \text{YL}}}{2}$
其中 TH 代表当天最高价, TL 代表当天最低价, YH 代表前日最高价, YL 代表前日最低价。那么如果 MID > 0 意味着今天的平均价高于昨天的平均价。
第 2 步:计算 ratio
$\text{ratio} = \frac{{\text{TVOL}/1000}}{{\text{TH} - \text{TL}}}$
其中 TVOL 代表当天交易量, TH 代表当天最高价, TL 代表当天最低价。
第 3 步:计算 emv
$\text{emv} = \frac{{\text{movmid}}}{{\text{ratio}}}$
EMV 的作者认为,巨量上涨伴随的是能量的快速枯竭,上涨往往不会持续太久;反而温和的成交量,能够保存一定的能量,往往使上涨持续更久。一旦上涨趋势形成,较少的成交量就能推动价格上涨,EMV 的数值就会升高。一旦下跌趋势行情形成,往往伴随的是无量或少量下跌,EMV 的数值就会下降。如果价格处于震荡行情或者价格上涨和下跌都伴随较大成交量时,EMV 的数值也会接近于零。因此你会发现,EMV 在大部分行情中都处于零轴下方,这也是这个指标的一大特色。站在另一个角度看,EMV 重视大趋势且能够产生足够利润的行情。
EMV 的用法相当简单,只要看 EMV 是否穿越零轴即可,当 EMV 在 0 以下时,代表市场弱市;当 EMV 在 0 以上时,代表市场强市。让 EMV 由负数转为正数时应该买进;当 EMV由正数转为负数时应该卖出。其特点是不仅能较好的避免市场中的震荡行情,而且还能在趋势行情启动的时候及时入场。但由于 EMV 反映的是价格在变动时的成交量的变化情况,所以仅对中长期走势有作用。对于短线或交易周期比较小的行情 EMV 的效果很差。
第 1 步:编写策略框架
# 策略主函数
def onTick():
pass
# 程序入口
def main():
while True: # 进入无限循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
优宽量化采用轮询模式,首先需要定义一个 main
函数和一个 onTick
函数,main
函数是策略的入口函数,程序会从 main
函数开始逐行执行代码。在 main
函数中,写入 while
循环,重复执行 onTick
函数,所有的策略核心代码都写在 onTick
函数中。
第 2 步:获取持仓数据
# 获取持仓数量
def get_position():
position = 0 # 赋值持仓数量为 0
position_arr = _C(exchange.GetPosition) # 获取持仓数组
if len(position_arr) > 0: # 如果持仓数组长度大于 0
for i in position_arr: # 遍历持仓数组
if i['ContractType'][:2] == 'IH': # 如果持仓品种等于订阅品种
if i['Type'] % 2 == 0: # 如果是多单
position = i['Amount'] # 赋值持仓数量为正数
else:
position = -i['Amount'] # 赋值持仓数量为负数
return position # 返回持仓量
因为在这个策略中,只使用了实时的持仓数量,为了方便维护,这里使用 get_position
封装了持仓量,如果当前持有多单就返回正数,如果当前持有空单就返回负数。
第 3 步:获取 K 线数据
# 获取数据
exchange.SetContractType('IH000') # 订阅期货品种
bars_arr = exchange.GetRecords() # 获取 K 线数组
if len(bars_arr) < 10: # 如果 K 线数量小于 10 根
return
在获取具体的 K 线数据之前,首先要先订阅具体的合约,使用优宽量化的 SetContractType
函数,并传入合约代码即可,如果想知道该合约的其他信息,也可以使用一个变量来接收这个数据。接着使用 GetRecords
函数就可以获取 K 线数据,因为返回的是一个数组,所以我们使用变量 bars_arr
来接受它。
第 4 步:计算 emv
# 计算 emv
bar1 = bars_arr[-2] # 获取上一根 K 线数据
bar2 = bars_arr[-3] # 获取前一根 K 线数据
# 计算 mov_mid 的值
mov_mid = (bar1['High'] + bar1['Low'])/2 - (bar2['High'] + bar2['Low'])/2
if bar1['High'] != bar1['Low']: # 如果被除数不为 0
# 计算 ratio 的值
ratio = (bar1['Volume'] / 10000) / (bar1['High'] - bar1['Low'])
else:
ratio = 0
# 如果 ratio 的值大于 0
if ratio > 0:
emv = mov_mid / ratio
else:
emv = 0
注意:在这里我们并没有使用最新的价格来计算 EMV 的值,而是采用相对滞后的当前 K 线出信号,下根 K 线发单的方法。这么做的目的是让回测更接近于实盘交易。我们知道,尽管现在量化交易软件已经非常先进了,但还是很难做到完全模拟真实的实盘 Tick 环境,特别是面对回测 Bar 级超长数据时,所以就采用这个折中的方法。
第 5 步:下单交易
# 下单交易
current_price = bars_arr[-1]['Close'] # 最新价格
position = get_position() # 获取最新持仓量
if position > 0: # 如果持有多单
if emv < 0: # 如果价格小于牙齿
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(round(current_price - 0.2, 2), 1) # 平多单
if position < 0: # 如果持有空单
if emv > 0: # 如果价格大于牙齿
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(round(current_price + 0.2, 2), 1) # 平空单
if position == 0: # 如果无持仓
if emv > 0: # 如果价格大于上唇
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(round(current_price + 0.2, 2), 1) # 开多单
if emv < 0: # 如果价格小于下巴
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(round(current_price - 0.2, 2), 1) # 开空单
在下单交易之前,我们需要先确定两个数据,一个是下单的价格,另一个是当前的持仓状态。下单的价格很简单,直接使用当前的收盘价加减品种的最小变动价位即可。由于我们之前已经使用 get_position
函数封装了持仓量,所以这里直接调用即可。最后就是根据 EMV 与零轴的位置关系开平仓了。
# 获取持仓数量
def get_position():
position = 0 # 赋值持仓数量为 0
position_arr = _C(exchange.GetPosition) # 获取持仓数组
if len(position_arr) > 0: # 如果持仓数组长度大于 0
for i in position_arr: # 遍历持仓数组
if i['ContractType'][:2] == 'IH': # 如果持仓品种等于订阅品种
if i['Type'] % 2 == 0: # 如果是多单
position = i['Amount'] # 赋值持仓数量为正数
else:
position = -i['Amount'] # 赋值持仓数量为负数
return position # 返回持仓量
# 策略主函数
def onTick():
exchange.SetContractType('IH000') # 订阅期货品种
bars_arr = exchange.GetRecords() # 获取 K 线数组
if len(bars_arr) < 10: # 如果 K 线数量小于 10 根
return
bar1 = bars_arr[-2] # 获取上一根 K 线数据
bar2 = bars_arr[-3] # 获取前一根 K 线数据
mov_mid = (bar1['High'] + bar1['Low']) / 2 - (bar2['High'] + bar2['Low']) / 2
if bar1['High'] != bar1['Low']: # 如果被除数不为 0
ratio = (bar1['Volume'] / 10000) / (bar1['High'] - bar1['Low'])
else:
ratio = 0
if ratio > 0: # 如果 ratio 的值大于 0
emv = mov_mid / ratio
else:
emv = 0
current_price = bars_arr[-1]['Close'] # 最新价格
position = get_position() # 获取最新持仓量
if position > 0: # 如果持有多单
if emv < 0: # 如果当前价格小于牙齿
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(round(current_price - 0.2, 2), 1) # 平多单
if position < 0: # 如果持有空单
if emv > 0: # 如果当前价格大于牙齿
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(round(current_price + 0.2, 2), 1) # 平空单
if position == 0: # 如果无持仓
if emv > 0: # 如果当前价格大于上唇
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(round(current_price + 0.2, 2), 1) # 开多单
if emv < 0: # 如果当前价格小于下巴
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(round(current_price - 0.2, 2), 1) # 开空单
# 程序入口函数
def main():
while True: # 循环
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
通过本节课程学习,可以看出 EMV 与普通交易者的看法相反,但却不无道理。由于 EMV 引入了成交量数据,因此比其他单纯用价格计算的技术指标,更能有效发现价格背后的东西。每一种策略都有着不同的特点,只有充分了解不同策略之间的优缺点,去其糟粕取其精华才能离成功更进一步。
趋势行情不会永远持续下去,事实上市场大部分时间都处于震荡行情,所以才会有人希望能得到一种交易策略,既可以用在趋势行情,也可以用在震荡行情。本节我们就用优宽量化交易平台,构建一个趋势和震荡行情通用的经典恒温器策略。
提到恒温器可能会有人想到汽车发动机与水箱之间的恒温器。当发动机温度低时,恒温器是关闭状态,此时发动机和水箱的水是不相通的,直到发动机温度升高,达到最佳机油润滑效果;当发动机温度升高到一定阈值时,节温器是开启状态,此时发动机和水箱的水形成循环,并流经风扇开启降温模式,直到达到发动机最佳工作温度。
那么恒温器策略也类似这个原理,并且延用了这个名字。它通过波动指数作为阈值,将市场分为趋势行情和震荡行情,自动对两种不同的行情使用对应的交易逻辑,有效弥补了趋势策略在震荡行情中的不适应。
如何把市场划分为趋势行情和震荡行情,也就成了这个策略的关键,恒温器策略引入了市场波动指数(Choppy Market Index),简称CMI。它是一个用来判断市场走势类型的技术指标。通过计算当前收盘价与N周期前收盘价的差值与这段时间内价格波动的范围的比值,来判断目前的价格走势是趋势还是震荡。
CMI 的计算公式为:
$CMI = \frac{{\text{{abs}}(\text{{Close}} - \text{{ref}}(\text{{Close}}, n-1)) \times 100}}{{\text{{HHV}}(\text{{High}}, n) - \text{{LLV}}(\text{{Low}}, n)}}$
其中,abs是绝对值,n是周期数。
一般来说CMI的值在0~100区间,值越大,趋势越强。当CMI的值小于20时,策略认为市场处于震荡模式;当CMI的值大于等于20时,策略认为市场处于趋势模式。
整个策略逻辑,可以简化的写成下面这样:
策略架构就是这么简单,剩下的就是把震荡策略的内容和趋势策略的内容,填充到这个框架里面。
依次打开:youquant.com网站 > 登录 > 控制中心 > 策略库 > 新建策略 > 点击右上角下拉菜单选择Python语言,开始编写策略,注意看下面代码中的注释。
第1步:编写策略框架
# 策略主函数
def onTick():
pass
# 程序入口
def main():
while True: # 进入无限循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠1秒
第2步:定义虚拟持仓变量
mp = 0 # 定义一个全局变量,用于控制虚拟持仓
第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
# 计算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) # 调用BOLL指标函数
up_line = boll[0][-1] # 获取上轨
mid_line = boll[1][-1] # 获取中轨
down_line = boll[2][-1] # 获取下柜
在处理趋势行情的进出场价格上,沿用了布林带策略,当价格向上突破布林带上轨时多头开仓,当价格向下突破布林带下轨时空头开仓,平仓方式则是以当前价格与布林中轨的位置关系来判断。
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线数组最后一个元素
# 计算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秒
趋势跟踪策略的表现形式是追涨杀跌,与之相反就是低买高卖的回归策略,也称均值回归策略。商品期货普遍存在一种规律,震荡行情远多于趋势行情,回归策略通过不断的低买高卖实现利润增长。
索罗斯在 1987 年撰写的《金融炼金术》一书中,曾经提出过一个重要的命题:I believe the market prices are always wrong in the sense that they present a biased view of the future.市场有效假说只是理论上的假设,实际上市场参与者并不总是理性的,并且在每一个时间点上,参与者不可能完全获取和客观解读所有的信息,再者就算是同样的信息,每个人的反馈都不尽相同。也就是说,价格本身就已经包含了市场参与者的错误预期,所以本质上市场价格总错误的。这或许是套利者的利润来源。
在一个非有效的期货市场中,不同时期交割合约之间受到市场影响也并不总是同步,其定价也并非完全有效的原因。那么,根据同一种交易标的的不同时期交割合约价格为基础,如果两个价格出现了较大的价差幅度,就可以同时买卖不同时期的期货合约,进行跨期套利。比如:螺纹钢 2010 合约和螺纹钢 2105 合约。
举个例子,假设螺纹钢 2010 和螺纹钢 2105 的价差长期维持在 5 左右。如果某一天价差达到 7,我们预计价差会在未来某段时间回归到 5。那么就可以卖出螺纹钢 2010,同时买入螺纹钢 2105,来做空这个价差。反之亦然。尽管这种价差是存在的,但是人工操作耗时、准确性差以及价格变化的影响,人工套利往往存在诸多不确定性。通过量化模型捕捉套利机会并制定套利交易策略,以及程序化算法自动向交易所下达交易订单,快速准确捕捉机会,高效稳定赚取收益,这就是量化套利的魅力所在。
'''backtest
start: 2024-04-15 09:00:00
end: 2024-04-22 15:00:00
period: 1h
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["SA","FG405"],["SB","FG409"]]
'''
class Hedge:
def __init__(self, q, e, initAccount, symbolA, symbolB, maPeriod, atrRatio, opAmount):
self.q = q
self.initAccount = initAccount
self.status = 0
self.symbolA = symbolA
self.symbolB = symbolB
self.e = e
self.isBusy = False
self.maPeriod = maPeriod
self.atrRatio = atrRatio
self.opAmount = opAmount
def poll(self):
if (self.isBusy or not exchange.IO("status")) or not ext.IsTrading(self.symbolA):
Sleep(1000)
return
exchange.SetContractType(self.symbolA)
recordsA = exchange.GetRecords()
exchange.SetContractType(self.symbolB)
recordsB = exchange.GetRecords()
if not recordsA or not recordsB:
return
if recordsA[-1]["Time"] != recordsB[-1]["Time"]:
return
minL, rA, rB = min(len(recordsA), len(recordsB)), recordsA.copy(), recordsB.copy()
rA.reverse()
rB.reverse()
arrDiff = []
for i in range(minL):
arrDiff.append(rB[i]["Close"] - rA[i]["Close"])
arrDiff.reverse()
if len(arrDiff) < self.maPeriod:
return
boll = TA.BOLL(arrDiff, self.maPeriod, self.atrRatio)
ext.PlotLine("上轨", boll[0][-2], recordsA[-2]["Time"])
ext.PlotLine("中轨", boll[1][-2], recordsA[-2]["Time"])
ext.PlotLine("下轨", boll[2][-2], recordsA[-2]["Time"])
ext.PlotLine("收盘价差价", arrDiff[-2], recordsA[-2]["Time"])
LogStatus(f"{_D()}\n 上轨: {boll[0][-1]}\n 中轨: {boll[1][-1]}\n 下轨: {boll[2][-1]}\n 当前收盘差价: {arrDiff[-1]}")
action = 0
if self.status == 0:
if arrDiff[-1] > boll[0][-1]:
Log("开仓 A 买 B 卖", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000")
action = 2
elif arrDiff[-1] < boll[2][-1]:
Log("开仓 A 卖 B 买", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000")
action = 1
elif self.status == 1 and arrDiff[-1] > boll[1][-1]:
Log("平仓 A 卖 B 买", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000")
action = 1
elif self.status == 2 and arrDiff[-1] < boll[1][-1]:
Log("平仓 A 买 B 卖", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000")
action = 2
if action == 0:
return
self.isBusy = True
tasks = []
if action == 1:
tasks.append([self.symbolA, "sell" if self.status == 0 else "closebuy"])
tasks.append([self.symbolB, "buy" if self.status == 0 else "closesell"])
elif action == 2:
tasks.append([self.symbolA, "buy" if self.status == 0 else "closesell"])
tasks.append([self.symbolB, "sell" if self.status == 0 else "closebuy"])
def callBack(task, ret):
self.isBusy = False
if task["action"] == "sell":
self.status = 2
elif task["action"] == "buy":
self.status = 1
else:
self.status = 0
account = _C(exchange.GetAccount)
LogProfit(account["Balance"] - self.initAccount["Balance"], account)
self.q.pushTask(self.e, tasks[1][0], tasks[1][1], self.opAmount, callBack)
self.q.pushTask(self.e, tasks[0][0], tasks[0][1], self.opAmount, callBack)
def main():
while not exchange.IO("status"):
Sleep(1000)
initAccount = _C(exchange.GetAccount)
q = ext.NewTaskQueue()
p = ext.NewPositionManager()
if CoverAll:
p.CoverAll()
t = Hedge(q, exchange, initAccount, SA, SB, MAPeriod, ATRRatio, OpAmount)
while True:
q.poll()
t.poll()
Sleep(1000)
策略参数设置如下:
如上面的代码,该策略首先订阅了近期合约SA和远期合约SB合约,并分别获取了它们的K线数据,然后计算两个合约的价差,接着以价差为数据计算布林带指标,最后实现当差价超过布林线上轨时正对冲,触碰下轨时反对冲。持仓时触碰布林中线平仓。这样就基本实现了一个跨期的对冲策略,策略逻辑还是比较简洁明朗的,大家可以根据自己中意的品种进行不同的尝试。
“套利”在现实生活中却很常见。比如:便利店老板从批发市场以0.5元买入一瓶矿泉水,然后在店里以1元的价格出售,最后赚取0.5元的差价。这个过程其实就类似套利。金融市场上的套利跟这个道理差不多,只不过套利的形式有多种多样。
在商品期货市场中,理论上5月份交割的苹果合约价格减去10月交割的苹果合约价格,其结果应该接近于0或者稳定在一定的价格区间内。但事实上由于受到天气、市场供需等诸多因素的原因,近期和远期合约价格在一段时间内会分别受到不同程度的影响,价差也会出现较大幅度的波动。
但无论如何,价差通常最终会回归到一定的价格区间内,那么如果价差大于这个区间,就卖5月份合约,同时买10月份合约,做空价差赚取利润;如果价差小于这个区间,就买5月份合约,同时卖10月份合约,做多价差赚取利润。这就是通过买卖同一个品种但不同交割月份的跨期套利。
除了跨期套利外,还有买入出口国大豆同时卖出进口国大豆,或者卖出出口国大豆同时买入进口国大豆的跨市场套利;买入上游原材料铁矿石同时卖出下游成品螺纹钢,或者卖出上游原材料铁矿石同时买入下游成品螺纹钢的跨品种套利等等。
但是上面这几种套利方法,虽然字面上是“套利”,并不属于纯粹意义上的套利,它们本质上还是属于有风险的投机,只不过这种投机的方式是做多或做空价差。虽然价差在大部分时间内趋于稳定,但也可能出现很长时间不回归的行情。
期现套利的核心原理是,同一个商品在同一个时间点只能有一个价格,期货到了交割时间就变成现货,所以在临近交割时会强制回归。这个与跨期套利完全不同,跨期套利是两个不同交割月的合约,到期时也就是两个不同月份的现货,当然可以是两个价格。
期现套利最大的特点是理论上无风险,主要是根据基差状态,计算利润区间。如果基差过大,就可以买入现货,同时做空期货,等待基差重新归零,就可以期货和现货双边平仓,赚取基差的利润。
因为增值税的存在(现货交易通常有 17%~20%的增值税),所以基差一般维持在正常的水平。我们可以计
by2022 企业微信加不上啊啊啊啊啊啊
雨幕(youquant) 您好,企业微信满了,您加这个微信: https://www.youquant.com/upload/asset/1780ac4e8b9064c9d7d9a.png