一个为莫斯科交易所期货开发的点差策略实例·进阶篇
(2/3)· 接上篇概念铺垫,本篇在双期货真实相关性上落地三种点差交易逻辑与回测优化
◍ 用回归斜率捕捉点差的趋势拐点
点差图上的线性回归写成 Y=A*X+B,A 就是那条拟合线的斜率。A>0 代表点差近段在扩张,A<0 则是收敛,拐点出现时前后两根柱的 A 值乘积会小于 0。 附件里的 EA 取最近 100 根柱做回归(LR_length=100),另用 500 根(Spread_length=500)算点差本身,默认跑在 M5 周期。它只在 BarsCalculated 与 Bars 对齐后才读缓冲,避免跨品种数据没刷完就误触发。 核心判断在 OnTick:CopyBuffer 取索引 1、2 的回归值,用 LRvalues[1]-LRvalues[0] 当当前 A,和历史 Spread_A_prev 相乘。若结果 <0,且无持仓就按 strategy 参数双向进场;已有持仓则反转。若方向没变,就盯着 profit/balance*100 是否越过 profit_percent(默认 10%),到了就双向平掉。 外汇与贵金属价差受流动性影响大,这种拐点策略在高波动时段可能频繁假突破,实盘前务必在 MT5 用历史数据步进回测。把 strategy 设成 SELL_AND_BUY_ON_UP 或 BUY_AND_SELL_ON_UP,直接 F5 调试就能看 PrintFormat 打的斜率翻转日志。
class="macro">#include <Trade\Trade.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 点差策略类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum SPREAD_STRATEGY { BUY_AND_SELL_ON_UP, class=class="str">"cmt">// 买入第一个,卖出第二个 SELL_AND_BUY_ON_UP, class=class="str">"cmt">// 卖出第一个,买入第二个 }; class=class="str">"cmt">//--- class="kw">input class="type">int LR_length = class="num">100 ; class=class="str">"cmt">// 用于计算点差回归的柱数 class="kw">input class="type">int Spread_length = class="num">500 ; class=class="str">"cmt">// 用于计算点差的柱数 class="kw">input ENUM_TIMEFRAMES period = PERIOD_M5 ; class=class="str">"cmt">// 时段 class="kw">input class="type">class="kw">string symbol1 = "Si-class="num">12.16" ; class=class="str">"cmt">// 配对的第一个交易品种 class="kw">input class="type">class="kw">string symbol2 = "RTS-class="num">12.16" ; class=class="str">"cmt">// 配对的第二个交易品种 class="kw">input class="type">class="kw">double profit_percent = class="num">10 ; class=class="str">"cmt">// 锁定的利润百分比 class="kw">input SPREAD_STRATEGY strategy =SELL_AND_BUY_ON_UP; class=class="str">"cmt">// 点差策略的类型 class=class="str">"cmt">//--- 指标句柄 class="type">int ind_spreadLR,ind,ind_2_symbols; class=class="str">"cmt">//--- 交易操作的类 CTrade trade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易订单函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- 线性回归在点差图表上的斜率系数A, Y(X)=A*X+B class="kw">static class="type">class="kw">double Spread_A_prev= class="num">0 ; if (isNewBar()) PrintFormat( "新柱 %s 在 %s 上开启" , _Symbol , TimeToString( TimeCurrent(), TIME_DATE | TIME_SECONDS )); class=class="str">"cmt">//--- 等待指标数据刷新,因为它要在两个交易品种上工作 if ( BarsCalculated(ind_spreadLR)== Bars( _Symbol , _Period )) { class=class="str">"cmt">//--- 在点差图表中获取索引为1和2(“昨天”和“前天”)的线性回归数值 class="type">class="kw">double LRvalues[]; class="type">class="kw">double Spread_A_curr; class="type">int copied= CopyBuffer(ind_spreadLR, class="num">1 , class="num">1 , class="num">2 ,LRvalues); if (copied!=- class="num">1 ) { class=class="str">"cmt">//--- 在最近关闭 ("昨天") 的柱上的线性回归系数 Spread_A_curr=LRvalues[ class="num">1 ]-LRvalues[ class="num">0 ]; class=class="str">"cmt">//--- 如果线性回归的斜率方向改变,则当前和前一个数值的乘积小于0 if (Spread_A_curr*Spread_A_prev< class="num">0 ) { PrintFormat( "LR 斜率放向已改变, Spread_A_curr=%.2f, Spread_A_prev=%.2f: %s" , Spread_A_curr,Spread_A_prev, TimeToString( TimeCurrent(), TIME_SECONDS )); class=class="str">"cmt">//--- 如果我们没有持仓,就在两个交易品种中进场 if ( PositionsTotal()== class="num">0 ) DoTrades(Spread_A_curr-Spread_A_prev> class="num">0 , strategy , symbol1 , class="num">1 , symbol2 , class="num">1 ); class=class="str">"cmt">//--- 如果有持仓,进行反转交易 else ReverseTrades( symbol1 , symbol2 ); } class=class="str">"cmt">//--- LR 倾斜方向没有变化,检查浮赢 - 是时候平仓吗? else { class="type">class="kw">double profit= AccountInfoDouble( ACCOUNT_PROFIT ); class="type">class="kw">double balance= AccountInfoDouble( ACCOUNT_BALANCE ); if (profit/balance* class="num">100 >= profit_percent ) { class=class="str">"cmt">//--- 已经达到了所需的浮赢,获利 trade.PositionClose( symbol1 ); trade.PositionClose( symbol2 ); } } class=class="str">"cmt">//--- 记住趋势的方向,用于和开启的新柱做比较 Spread_A_prev=Spread_A_curr; } } }
用可视化调试跑通第一套策略
MT5 的可视化测试最适合做 EA 调试而非实盘验证。在「工具-设置-调试」里要逐项指定:交易品种、时段、测试间隔、执行模式、存款额、分时生成模式。 对于外汇和贵金属这类场外品种,分时生成建议选「基于真实分时的每一分时」;交易所品种同理。该模式下 EA 会回放真实记录的历史分时,结果更接近真实交易条件,但外汇/贵金属仍属高风险,回测贴近不等于实盘概率可控。 MT5 交易服务器会自动收集并存储从交易所收到的全部分时,终端首次请求便下发完整订单分时历史,不需自己拼数据。 开启调试模式后可在可视化界面单步跑策略,必要时打断点看任意变量数值;EA 引用的指标会自动加载到图表,不用手动挂。借这个过程顺手把参数调一遍,比盲改高效得多。
「只调利润比例看两种点差策略成色」
Strategy1_AngleChange_EA.mq5 留了四个外部参数,黄色高亮那几个都能进优化器。本次只动 profit_percent,把 strategy 钉死,范围扫 0.2% 到 3.0%,目的不是找最优解,而是横向比两种斜率翻转规则的轮廓。 [CODE]input int LR_length = 100 ; // 点差回归计算中的柱数 input int Spread_length = 500 ; // 用于计算点差的柱数 input double profit_percent = 10 ; // 锁定的利润百分比 input SPREAD_STRATEGY strategy =SELL_AND_BUY_ON_UP; // 点差策略的类型[/CODE] 上面四行里,LR_length=100 决定回归线用最近 100 根柱算斜率,Spread_length=500 管点差统计窗口,profit_percent 初始给的 10 实际远超本次扫描上限,得手动压进 3% 以内。 BUY_AND_SELL_ON_UP(买一卖二)在斜率由负转正时进场,优化结果稀烂。两个月实测连亏多笔,这种翻转触发方式本身就不像能打的样子。 SELL_AND_BUY_ON_UP(卖一买二)稍体面:15 轮优化里 5 轮出了点利润。测试区间是 2016.8.1–9.30 的历史两根月线,外汇与贵金属价差策略在此样本高风险,两种规则整体都偏弱。 滞后嫌疑最大的是 LR_length=100 这根斜率线——百柱回归天然慢半拍,进场参数可能选错了。下一版直接换斜率算法。
class="kw">input class="type">int LR_length = class="num">100 ; class=class="str">"cmt">// 点差回归计算中的柱数 class="kw">input class="type">int Spread_length = class="num">500 ; class=class="str">"cmt">// 用于计算点差的柱数 class="kw">input class="type">class="kw">double profit_percent = class="num">10 ; class=class="str">"cmt">// 锁定的利润百分比 class="kw">input SPREAD_STRATEGY strategy =SELL_AND_BUY_ON_UP; class=class="str">"cmt">// 点差策略的类型
◍ 拿已完成柱的点差正负翻转做信号
这套打法只看已收盘的柱:新柱一开,才去比对前两根柱的点差符号。若前天柱点差为负的、昨天柱变成正的,就判定点差方向翻转向上;反过来亦成立。EA 里用 strategy 参数切换只做某一侧翻转,不写死方向。 下面这段来自实盘 EA 的核心 OnTick 逻辑,先把点差缓冲读出来再判符号。static 变量 Spread_prev 跨 tick 保留,等 isNewBar 后拿索引 1、2 的已完成柱值做乘积累积判断。 回测层面不乐观:在可视化测试里按 profit_percent 跑优化,'卖一买二'规则全程亏损;'买一卖二'在全部测试轮次里也是净亏。所以原文作者紧接着做了第三版策略。 外汇与贵金属点差受流动性跳变影响大,符号翻转只是概率性线索,实盘前务必在 MT5 策略测试器用历史数据复算,高杠杆品种回撤可能超预期。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 交易订单函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- 之前的点差值是 Symbol1 和 Y(Symbol2)=A*Symbol2+B 之间的差异 class="kw">static class="type">class="kw">double Spread_prev=class="num">0; if(isNewBar()) PrintFormat("新柱 %s 在 %s 开启",_Symbol,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)); class=class="str">"cmt">//--- 等待指标数据刷新,因为它工作于两个交易品种 if(BarsCalculated(ind_spreadLR)==Bars(_Symbol,_Period)) { class=class="str">"cmt">//--- 取得索引为1和2 ("昨天" 和 "前天") 的点差值 class="type">class="kw">double SpreadValues[]; class="type">int copied=CopyBuffer(ind_spreadLR,class="num">0,class="num">1,class="num">2,SpreadValues); class="type">class="kw">double Spread_curr=SpreadValues[class="num">1]; if(copied!=-class="num">1) { class=class="str">"cmt">//--- 如果点差符号改变,当前和之前数值相乘会小于0 if(Spread_curr*Spread_prev<class="num">0) { PrintFormat("点差符号改变, Spread_curr=%.2f, Spread_prev=%.2f: %s", Spread_curr,Spread_prev,TimeToString(TimeCurrent(),TIME_SECONDS)); class=class="str">"cmt">//--- 如果我们没有持仓,就在两个交易品种中进场 if(PositionsTotal()==class="num">0) DoTrades(Spread_curr>class="num">0,strategy,symbol1,class="num">1,symbol2,class="num">1); class=class="str">"cmt">//--- 有持仓,就反转交易 else ReverseTrades(symbol1,symbol2); } class=class="str">"cmt">//--- 点差符号改变,检查浮盈 - 是时候平仓吗? else { class="type">class="kw">double profit=AccountInfoDouble(ACCOUNT_PROFIT); class="type">class="kw">double balance=AccountInfoDouble(ACCOUNT_BALANCE); if(profit/balance*class="num">100>=profit_percent) { class=class="str">"cmt">//--- 达到了要求的浮盈水平,获利平仓 trade.PositionClose(symbol1); trade.PositionClose(symbol2); } } class=class="str">"cmt">//--- 记住点差值以在新柱开启时做比较 Spread_prev=Spread_curr; } } }
在当根柱内抓点差符号翻转
前两个策略都只在整根柱闭合后才动手,等于放弃了柱内行情。这一节把战场移到当前未完成柱:逐笔 tick 监控点差符号,只要当根柱的点差符号和上一根已完成柱相反,就先假定方向切换已经发生。 光靠一次翻转不够稳,原文用了一个外部参数 ticks_for_trade=10 做过滤——要求最近 10 个订单分时的点差符号一致地反向,才认定不是噪点。比如上一根柱点差为正、近 10 个 tick 点差持续为负,EA 才进场;无持仓则双向建仓,有持仓则反转。 EA 里挂了 ShowLastTicksComment() 把最近 N 个分时的双品种数值直接打印在图表上,你能以毫秒精度肉眼核对信号时刻,不必盲信回测。把这套逻辑接进 MT5 后,沿用前两步的“买一卖二 / 卖一买二”优化,实测结果并没有明显抬升——说明单纯靠点差符号翻转+10 tick 确认,在外汇与贵金属这类高杠杆品种上仍只是弱信号,实盘前务必用小资金验证风险。
class="type">void OnTick() { if(isNewBar()) PrintFormat("新柱 %s 在 %s 开启", _Symbol, TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS)); class=class="str">"cmt">//--- 等待指标数据的刷新,因为它工作于两个交易品种上 if(BarsCalculated(ind_spreadLR)==Bars(_Symbol,_Period)) { class=class="str">"cmt">//--- 取得当前(今天)和之前(昨天)的柱的点差值 class="type">class="kw">double SpreadValues[]; class="type">int copied=CopyBuffer(ind_spreadLR,class="num">0,class="num">0,class="num">2,SpreadValues); class="type">class="kw">double Spread_curr=SpreadValues[class="num">1]; class=class="str">"cmt">// 当前未完成的柱上的点差 class="type">class="kw">double Spread_prev=SpreadValues[class="num">0]; class=class="str">"cmt">// 之前完成柱上的点差 if(copied!=-class="num">1) { class=class="str">"cmt">//--- 如果点差符号改变在最近的 ticks_for_trade 分时中是稳定的 if(SpreadSignChanged(Spread_curr,Spread_prev,ticks_for_trade)) { PrintFormat("点差符号改变了,Spread_curr=%.2f, Spread_prev=%.2f: %s", Spread_curr,Spread_prev,TimeToString(TimeCurrent(),TIME_SECONDS)); class=class="str">"cmt">//--- 在图表上显示两个交易品种的最近 ticks_for_trade 的值 ShowLastTicksComment(ticks_for_trade); class=class="str">"cmt">//--- 如果我们没有持仓,在两个交易品种中进场 if(PositionsTotal()==class="num">0) DoTrades(Spread_curr>class="num">0, strategy, symbol1, class="num">1, symbol2, class="num">1); class=class="str">"cmt">//--- 有持仓,则反转交易 else ReverseTrades(Spread_curr>class="num">0,positionstype, symbol1, symbol2); } class=class="str">"cmt">//--- 点差符号没有改变,检查浮赢 - 是时候平仓吗? else { class="type">class="kw">double profit=AccountInfoDouble(ACCOUNT_PROFIT); class="type">class="kw">double balance=AccountInfoDouble(ACCOUNT_BALANCE); if(profit/balance*class="num">100 >= profit_percent) { class=class="str">"cmt">//--- 达到了所需的浮赢水平,获利 trade.PositionClose(symbol1); trade.PositionClose(symbol2); positionstype=class="num">0; } } } } }