测试当交易货币对篮子时出现的形态第二部分·进阶篇
事件原因分支与自写均线函数
EA 在 OnChartEvent 里用 switch 接 REASON_* 常量,能把图表上发生的动作转成中文提示。下面这段 case 覆盖了品种切换、周期变更、图表关闭、参数改动、重编译、移除脚本、换模板七类事件,default 兜底其它原因,最后统一 PrintFormat 输出,方便在专家日志里追踪 EA 为什么被触发。 REASON_CHARTCHANGE 对应交易品种或者时段已经改变;REASON_CHARTCLOSE 是图表被关闭;REASON_PARAMETERS 指输入参数已经改变。REASON_RECOMPILE 和 REASON_REMOVE 都用 __FILE__ 宏拼出当前程序文件名,区别在“被重新编译”还是“被从图表上删除”;REASON_TEMPLATE 则是图表应用了新模板。 除事件分发外,这段代码还给了个轻量均线函数 GetMA。它接收 double 数组引用 arr、起点 index、均线周期 period 和精度 digit,用 for 循环把 index 到 index+period-1 的收盘价累加后除以 period,再 NormalizeDouble 到指定位数返回。 在 MT5 里新建脚本粘入下方代码,挂到 EURUSD 的 H1 图表,改一次参数或换模板,就能在日志看到对应中文原因——外汇与贵金属杠杆高,事件触发逻辑仅用于诊断,不构成任何方向判断。
case REASON_CHARTCHANGE: text="交易品种或者时段已经改变";class="kw">break; case REASON_CHARTCLOSE: text="图表被关闭";class="kw">break; case REASON_PARAMETERS: text="输入参数已经改变";class="kw">break; case REASON_RECOMPILE: text="程序 "+__FILE__+" 被重新编译";class="kw">break; case REASON_REMOVE: text="程序 "+__FILE__+" 被从图表上删除";class="kw">break; case REASON_TEMPLATE: text="图表上应用了新的模板";class="kw">break; class="kw">default:text="其它原因"; } PrintFormat("%s",text); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetMA(class="kw">const class="type">class="kw">double &arr[],class="type">int index,class="type">int period,class="type">int digit) { class="type">class="kw">double m=class="num">0; for(class="type">int j=class="num">0; j<period; j++) m+=arr[index+j]; m/=period; class="kw">return (NormalizeDouble(m,digit)); } class=class="str">"cmt">//+------------------------------------------------------------------+
「篮子EA回测先泼冷水」
拿附带的 testEAbasket.mq5 跑策略测试器,用去年数据分别压 H1、H4、D1 三个周期,标的锁 USD 货币对篮子(EURUSD、GBPUSD、AUDUSD、NZDUSD、USDCAD、USDCHF、USDJPY)。标准报告里结论很直白:现有参数下三个周期全亏,这符合预期——靠早期就定死的形态很难挤出厚利润。 根子在两处:均线形态按定义自带延迟;单看一条快均线就下单,在技术分析的框架里本来就不成立。EA 只按预设的“形态质量”阈值剔掉少量信号,而那个质量阈值本身是拍脑袋定的,不够用。外汇与贵金属杠杆高,这类粗糙逻辑实盘大概率继续吃亏。 但 H1 上并非全无亮光:余额曲线整体负,却出现过获利段,回撤谷陡但中间夹着连续盈利。说明若改代码、调 H1 的过滤与手数,可能把负期望扳回来。下面先拆初始化这段骨架。 [CODE] 段里 OnInit 加载了自定义指标 testWPR&MA,周期参数 wpr=20、ma=10,并建了 7 货币对的 bDirect 方向数组(CAD/CHF/JPY 对 USD 为 true 表示反向)。EventSetTimer(1) 开 1 秒定时器,g_state 初始 BCLOSE。OnTick 目前空跑,真正交易逻辑还没接。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| testEAbasket.mq5 | class=class="str">"cmt">//| Copyright class="num">2017, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Trade\Trade.mqh> class="macro">#define LG class="num">7 enum BSTATE { BCLOSE = class="num">0, BBUY = class="num">1, BSELL = class="num">2 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">int wpr = class="num">20; class="kw">input class="type">int ma = class="num">10; class="kw">input class="type">class="kw">double lt = class="num">0.01; class=class="str">"cmt">//lot class="type">class="kw">string pair[]={"EURUSD","GBPUSD","AUDUSD","NZDUSD","USDCAD","USDCHF","USDJPY"}; class="type">bool bDirect[]={class="kw">false,class="kw">false,class="kw">false,class="kw">false,true,true,true}; class="type">int h; class="type">class="kw">ulong Ticket[LG]; class="type">class="kw">double m[class="num">1],ml; class="type">class="kw">double w[class="num">1],wl; BSTATE g_state; class="type">class="kw">double g_dMinSize = class="num">5.0; class="type">int OnInit() { h = iCustom(NULL,class="num">0,"testWPR&MA",wpr,ma); if (h == INVALID_HANDLE) { Print("当创建 testWPReur 时出错"); class="kw">return (INIT_FAILED); } g_state = BCLOSE; EventSetTimer(class="num">1); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 交易终止函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { IndicatorRelease(h); EventKillTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易订单分时函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- }
◍ 用计时器捕捉均线交叉的实盘切换
这段逻辑跑在 OnTimer 里,靠定时器而非逐 tick 去轮询,能明显降低 CPU 占用。核心动作是:每根 K 线收线后把上一根快慢线值存进 wl / ml,再拷进当前 w[0] / m[0],用新旧两点的相对位置变化来判断交叉。 当 w[0] > m[0] 且 wl < ml,说明快线从下向上穿过慢线,倾向多头触发。代码还要求穿越幅度同时满足 w[0]-m[0] >= g_dMinSize 与 ml-wl >= g_dMinSize,两个阈值都过了才 EnterBuy,否则只看交叉容易被毛刺骗进场。 EnterBuy 里用 LG 次循环对 pair[i] 发单,bDirect[i] 决定同品种发 Buy 还是反向发 Sell,Ticket[i] 接回成交号。CloseAllPos 则无脑平掉 Ticket 数组里所有持仓并把 g_state 置回 BCLOSE,保证状态机不卡死。 外汇与贵金属杠杆高,这类交叉策略在震荡市可能连续止损,上手前请在 MT5 策略测试器用 2020–2023 年 H1 数据跑一遍,重点看 g_dMinSize 调成 0.0002 与 0.0005 时的回撤差异。
class="type">void OnTimer() { if(IsNewCandle()) { wl = w[class="num">0]; CopyBuffer(h,class="num">0,class="num">1,class="num">1,w); ml = m[class="num">0]; CopyBuffer(h,class="num">1,class="num">1,class="num">1,m); if ( w[class="num">0] > m[class="num">0] && wl < ml) { if (g_state != BCLOSE) CloseAllPos(); if ( w[class="num">0] - m[class="num">0] >= g_dMinSize && ml - wl >= g_dMinSize) { EnterBuy(lt); g_state = BBUY; } } if ( w[class="num">0] < m[class="num">0] && wl > ml) { if (g_state != BCLOSE) CloseAllPos(); if ( m[class="num">0] - w[class="num">0] >= g_dMinSize && wl - ml >= g_dMinSize) { EnterSell(lt); g_state = BSELL; } } } } class="type">void CloseAllPos() { CTrade Trade; Trade.LogLevel(LOG_LEVEL_NO); for(class="type">int i=class="num">0; i<LG; i++) { Trade.PositionClose(Ticket[i]); } g_state = BCLOSE; } class="type">void EnterBuy(class="type">class="kw">double lot) { CTrade Trade; Trade.LogLevel(LOG_LEVEL_NO); for(class="type">int i=class="num">0; i<LG; i++) { if(bDirect[i]) { class=class="str">"cmt">//发送买入订单 Trade.Buy(lot,pair[i]); Ticket[i]=Trade.ResultDeal(); } else { class=class="str">"cmt">//发送卖出订单 Trade.Sell(lot,pair[i]); Ticket[i]=Trade.ResultDeal(); } } g_state = BBUY; }
批量挂单与新K线判定的代码骨架
这段逻辑解决两个实战问题:一是在多品种上按方向批量开仓,二是在 H1/H4/D1 周期精准捕捉新蜡烛生成。外汇与贵金属杠杆高,批量下单若周期判断错位,可能在同一根蜡烛上重复触发,风险被成倍放大。 EnterSell 函数通过 for 循环遍历 LG 个品种,依据 bDirect[i] 布尔数组决定 Trade.Sell 或 Trade.Buy,并把成交单号写回 Ticket[i]。循环结束将全局状态 g_state 置为 BSELL,相当于给后续平仓逻辑一个明确旗标。 IsNewCandle 用静态变量 candle 存上一根的时间刻度,_Period 切换下分别取 Hour()、Hour4()、Day() 的返回值;只要当前刻度不等于缓存值就返回 true 并刷新。Hour4 直接拿 Hour()/4 做整数除,意味着 0/4/8/12/16/20 点换柱,Day 与 Hour 都靠 TimeCurrent 填 MqlDateTime 结构体取字段。 把这套直接丢进 MT5 的 EA 里,把 LG 设成你盯的品种数,就能验证新柱触发频率是否符合预期;贵金属 XAUUSD 在 H4 下每 4 小时才一次信号,比手动刷图表省事得多。
class="type">void EnterSell(class="type">class="kw">double lot) { CTrade Trade; Trade.LogLevel(LOG_LEVEL_NO); for(class="type">int i=class="num">0; i<LG; i++) { if(bDirect[i]) { class=class="str">"cmt">//发送卖出订单 Trade.Sell(lot,pair[i]); Ticket[i]=Trade.ResultDeal(); } else { class=class="str">"cmt">//发送买入订单 Trade.Buy(lot,pair[i]); Ticket[i]=Trade.ResultDeal(); } } g_state = BSELL; } class="type">bool IsNewCandle() { class="kw">static class="type">int candle=-class="num">1; class="type">int t1=class="num">0; class="kw">switch(_Period) { case PERIOD_H1: t1 = Hour(); class="kw">break; case PERIOD_H4: t1 = Hour4(); class="kw">break; case PERIOD_D1: t1 = Day(); class="kw">break; } if(t1 != candle) {candle=t1; class="kw">return(true);} class="kw">return (class="kw">false); } class="type">int Hour4(){class="kw">return((class="type">int)Hour()/class="num">4);} class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int Day() { class="type">MqlDateTime tm; TimeCurrent(tm); class="kw">return(tm.day); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int Hour() { class="type">MqlDateTime tm; TimeCurrent(tm); class="kw">return(tm.hour); }
「给形态尺寸套上上限」
最初形态判定用的 Delta1、Delta2 阈值是 5%,数值越小 EA 进出越频繁,越大则信号更“健壮”但入场变少。直接拿这个参数去优化很容易在某个货币对、某时段上过拟合,换环境就失效,所以这组值先不动,但得防住“过大”的坑。 逻辑上 Delta 太大会让形态转向过猛,价格反而容易进入平盘或反转,而且此时组合 WPR 已逼近反向边界,顺着原趋势进场位置很差。把 Delta1 和 Delta2 单值上限锁在 10%,合计形态尺寸 20%,算是兼顾灵敏与安全的折中。 H1 上挂修改版 EA 回测(testEAbasket1.zip),净值仍为负但亏损绝对额明显收窄,原先一路吐的曲线变成深回撤后有所修复——说明上限过滤确实剔掉了一批垃圾信号。外汇与贵金属杠杆高,这类回测不等于实盘胜率,只能当作参数边界的参考。 下面这段代码就是计时器里加的闸门:先定义 g_dMaxSize=10.0,再在 OnTimer 里算 d1、d2 绝对值,只有同时满足 ≥下限且 ≤上限才放行下单。 别把 5% 当金标准 Delta 原值不优化不代表最优,切到别的时段或货币篮子时,记得回头调 Min/Max 双边界,不然过滤可能过严漏单或过松接飞刀。
<span class="keyword">class="type">class="kw">double</span> g_dMaxSize = <span class="number">class="num">10.0</span>; <span class="keyword">class="type">void</span> <span class="functions">OnTimer</span>() { <span class="keyword">if</span>(IsNewCandle()) { wl=w[<span class="number">class="num">0</span>]; <span class="functions">CopyBuffer</span>(h,<span class="number">class="num">0</span>,<span class="number">class="num">1</span>,<span class="number">class="num">1</span>,w); ml=m[<span class="number">class="num">0</span>]; <span class="functions">CopyBuffer</span>(h,<span class="number">class="num">1</span>,<span class="number">class="num">1</span>,<span class="number">class="num">1</span>,m); <span class="keyword">class="type">class="kw">double</span> d1 = <span class="functions">MathAbs</span>(w[<span class="number">class="num">0</span>] - m[<span class="number">class="num">0</span>]); <span class="keyword">class="type">class="kw">double</span> d2 = <span class="functions">MathAbs</span>(ml - wl); <span class="keyword">if</span>(w[<span class="number">class="num">0</span>]>m[<span class="number">class="num">0</span>] && wl<ml) { <span class="keyword">if</span>(g_state!=BCLOSE) CloseAllPos(); <span class="keyword">if</span>(d1 >= g_dMinSize && d2 >= g_dMinSize && d1 <= g_dMaxSize && d2 <= g_dMaxSize) { EnterBuy(lt); g_state=BBUY; } } <span class="keyword">if</span>(w[<span class="number">class="num">0</span>]<m[<span class="number">class="num">0</span>] && wl>ml) { <span class="keyword">if</span>(g_state!=BCLOSE) CloseAllPos(); <span class="keyword">if</span>(d1 >= g_dMinSize && d2 >= g_dMinSize && d1 <= g_dMaxSize && d2 <= g_dMaxSize) { EnterSell(lt); g_state=BSELL; } } } }
◍ 用组合WPR边界卡掉极端区入场
组合 WPR 取值在 -100% 到 +100% 之间,但曲线通常只会逼近边界而不会真正触顶触底,所以不能直接拿满值当信号。前面已经测出超买区落在 60%–70%、超卖区落在 -60%–-70%,这里取较低边界 60% 与 -60% 作为第二道过滤器:买篮子的前提是组合 WPR 低于 60%,卖篮子则要求它高于 -60%,借此避开趋势反转或转平盘概率较高的极端区。 把这条规则写进 EA 后,在先前选定的时间段与 H1 周期回测,结果档案在 testEAbasket2.zip 里。和未加过滤的版本比,差异不大,只是稍微好一点——可能源于超买超卖水平没校准准,或者过滤掉的无效交易还不够多,两者可能并存。 不能因为短期样本就判定这个过滤器没用。要下结论,得把它铺到全部货币篮子、所有选定周期,且样本时间段明显超过一年才行。当前先保留在代码里,尽管它的边际贡献还存疑,外汇与贵金属本身属高风险品种,这类过滤只降低暴露而非消除风险。 下面这段是处理入场条件的核心判断:先比组合 WPR 与均线位置,再叠加尺寸区间与边界过滤。
if(w[class="num">0]>m[class="num">0] && wl<ml) { if(g_state!=BCLOSE) CloseAllPos(); if(d1 >= g_dMinSize && d2 >= g_dMinSize && d1 <= g_dMaxSize && d2 <= g_dMaxSize && w[class="num">0] < class="num">60) { EnterBuy(lt); g_state=BBUY; } } if(w[class="num">0]<m[class="num">0] && wl>ml) { if(g_state!=BCLOSE) CloseAllPos(); if(d1 >= g_dMinSize && d2 >= g_dMinSize && d1 <= g_dMaxSize && d2 <= g_dMaxSize && w[class="num">0] > -class="num">60) { EnterSell(lt); g_state=BSELL; } }