逆转形态:测试双顶/双底形态·综合运用
「确认周期与搜索窗口的刷新逻辑」
这段逻辑解决了一个实盘常见坑:多周期形态扫描时,若每次 tick 都重跑全历史,CPU 会被拖死。代码先用 SeriesInfoInteger 取确认周期(e_ConfirmationTF)最后一根 K 线时间,与上次记录的 Last_CfTF 比较,时间没推进就直接 return,避免重复计算。 对象数组 ar_Objects 从下标 2 开始遍历,遇到指针失效(POINTER_INVALID)或形态校验不通过(CheckPattern 返回 false)的就删掉,删除后 i 自减、total 减一,保证不漏扫。这种惰性清理能让数组只留有效形态实例。 搜索窗口靠 e_TimeFrame 的最后 BAR 时间驱动,Last_WT 记录上次触发点。start_search 取 iTime 在限制历史根数(i_MaxHistory 与 Bars 取小)对应的时间,防止回测时扫太深。CPattern 对象首次 invalid 才 new,并调 Create 用最小/最大回调参数(d_MinCorrection、d_MaxCorrection)初始化,失败即释放并返回。 主循环里 Pattern.Search 不断向前找,每次把 start_search 推到当前形态末端时间加一个周期秒数之后;若数组里已有相同形态(Compare==0)或 CheckPattern 不过,就跳过。找到合法新形态才 Add 进数组并 new 下一个 CPattern 继续。外汇与贵金属波动剧烈,这种周期门控在高杠杆下能显著降低 EA 卡顿概率,建议直接拷进 MT5 用 Print 打点验证触发频率。
class="type">class="kw">datetime series=(class="type">class="kw">datetime)SeriesInfoInteger(_Symbol,e_ConfirmationTF,SERIES_LASTBAR_DATE); if(Last_CfTF>=series) class="kw">return; Last_CfTF=series; class="type">int total=ar_Objects.Total(); for(class="type">int i=class="num">2;i<total;i++) { if(CheckPointer(ar_Objects.At(i))==POINTER_INVALID) if(ar_Objects.Delete(i)) { i--; total--; class="kw">continue; } class=class="str">"cmt">//--- if(!CheckPattern(ar_Objects.At(i))) { if(ar_Objects.Delete(i)) { i--; total--; class="kw">continue; } } } class="kw">static class="type">class="kw">datetime Last_WT=class="num">0; series=(class="type">class="kw">datetime)SeriesInfoInteger(_Symbol,e_TimeFrame,SERIES_LASTBAR_DATE); if(Last_WT>=series) class="kw">return; start_search=iTime(_Symbol,e_TimeFrame,fmin(i_MaxHistory,Bars(_Symbol,e_TimeFrame))); if(CheckPointer(Pattern)==POINTER_INVALID) { Pattern=new CPattern(); if(CheckPointer(Pattern)==POINTER_INVALID) class="kw">return; if(!Pattern.Create(ar_Objects.At(class="num">1),d_MinCorrection,d_MaxCorrection)) { class="kw">delete Pattern; class="kw">return; } } Last_WT=series; class="kw">while(!IsStopped() && Pattern.Search(start_search)) { start_search=fmax(start_search,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame)); class="type">bool found=class="kw">false; for(class="type">int i=class="num">2;i<ar_Objects.Total();i++) if(Pattern.Compare(ar_Objects.At(i),class="num">0)==class="num">0) { found=true; class="kw">break; } if(found) class="kw">continue; if(!CheckPattern(Pattern)) class="kw">continue; if(!ar_Objects.Add(Pattern)) class="kw">continue; Pattern=new CPattern(); if(CheckPointer(Pattern)==POINTER_INVALID) class="kw">break; if(!Pattern.Create(ar_Objects.At(class="num">1),d_MinCorrection,d_MaxCorrection)) {
信号触发后的分批止盈拆单逻辑
当 CheckPattern 拿到 signal 后,真正的下单动作在 switch 里完成。signal=1 走多,signal=-1 走空,两种分支对称处理,差别只在报价取自 SYMBOL_ASK 还是 SYMBOL_BID,以及止损加减点方向。 以多单为例,先取当前卖价作为入场价,然后清空 CLimitTakeProfit 的旧挂单池。tp1 与 tp2 两段止盈并非一次性挂死:若 tp1 距入场价大于经纪商 SYMBOL_TRADE_STOPS_LEVEL 最小止损距离,则按 (tp1-price)/_Point 的点数挂第一批,占用仓位 50 或 100(取决于 tp1、tp2 间距是否大于等于 1 点);剩余 to_close 初始为 100,减去已用部分后,若仍大于 0 且 tp2 也满足最小距离,则把余量挂到 tp2 处。 空单镜像处理:用买价入场,tp1/tp2 改为 price 减目标价算点数,止损落在 sl+i_SL*_Point。两个分支里只要 AddTakeProfit 返回失败或 Trade.Buy/Sell 返回真(即下单出错),函数立刻 return false,不再继续。 外汇与贵金属杠杆高,最小止损距离和滑点可能让 tp1 挂单直接被拒,实盘前应在 MT5 策略测试器里把 SYMBOL_TRADE_STOPS_LEVEL 打印出来核对,避免回测能跑实盘全废。
class="kw">delete Pattern; class="kw">break; } } class=class="str">"cmt">//--- class="kw">return; } class="type">bool CheckPattern(CPattern *pattern) { class="type">int signal=class="num">0; class="type">class="kw">double sl=-class="num">1, tp1=-class="num">1, tp2=-class="num">1; if(!pattern.CheckSignal(signal,sl,tp1,tp2)) class="kw">return class="kw">false; class="type">class="kw">double price=class="num">0; class="type">class="kw">double to_close=class="num">100; class=class="str">"cmt">//--- class="kw">switch(signal) { case class="num">1: price=SymbolInfoDouble(_Symbol,SYMBOL_ASK); CLimitTakeProfit::Clear(); if((tp1-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(CLimitTakeProfit::AddTakeProfit((class="type">uint)((tp1-price)/_Point),(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100))) to_close-=(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100); if(to_close>class="num">0 && (tp2-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(!CLimitTakeProfit::AddTakeProfit((class="type">uint)((tp2-price)/_Point),to_close)) class="kw">return class="kw">false; if(Trade.Buy(d_Lot,_Symbol,price,sl-i_SL*_Point,class="num">0,NULL)) class="kw">return class="kw">false; class="kw">break; case -class="num">1: price=SymbolInfoDouble(_Symbol,SYMBOL_BID); CLimitTakeProfit::Clear(); if((price-tp1)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(CLimitTakeProfit::AddTakeProfit((class="type">uint)((price-tp1)/_Point),(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100))) to_close-=(fabs(tp1-tp2)>=_Point ? class="num">50 : class="num">100); if(to_close>class="num">0 && (price-tp2)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(!CLimitTakeProfit::AddTakeProfit((class="type">uint)((price-tp2)/_Point),to_close)) class="kw">return class="kw">false; if(Trade.Sell(d_Lot,_Symbol,price,sl+i_SL*_Point,class="num">0,NULL)) class="kw">return class="kw">false; class="kw">break;
◍ 函数收尾的返回逻辑
上面这段代码是自定义指标或 EA 中某个校验函数的收尾部分。前面的循环或大段判断结束后,用 //--- 注释分隔,最后直接 return true 表示校验通过。
在 MT5 里这类写法很常见:只要前置条件都没触发 return false,流程走到末尾就默认放行。你可以把这段贴进 MetaEditor 随便建个脚本,在函数末尾打断点看调用栈,确认返回路径是否符合预期。
外汇与贵金属品种波动剧烈、杠杆风险高,任何校验逻辑都建议在真实账户前用策略测试器跑够样本。
} class=class="str">"cmt">//--- class="kw">return true; }
「用九年数据给 EA 做一次压力测试」
EA 写完后别急着挂真仓,先扔进 MT5 的策略测试器跑历史。我们取 EURUSD 在 2018 年共 9 个月的数据,形态扫描放在 M30 周期,而实际开仓信号放到 M5 周期去确认,这样能避开大周期噪音、抓小周期入场。 回测跑完一共成交 90 笔,其中 70 笔收利,胜率约 77.8%。盈利因子 2.02、恢复因子 4.77,说明策略在样本期内有正期望,且回撤恢复能力不弱。外汇及贵金属属高风险品种,历史表现不预示未来,真仓前建议先开模拟账户按同样周期验证一遍。
限价单替代止盈的落地方式
把双顶/双底当作趋势逆转信号写进 EA 后,历史测试显示结果处于可接受区间,盈利表现说明这类形态作为入场过滤具备一定概率优势,但外汇与贵金属杠杆高、回撤不可控,实盘仍需谨慎。 在 EA 源码里直接实现指标计算,可以避免依赖外部指标文件,减少加载延迟与信号漂移,MT5 策略测试器里能直接看到每根 K 线的形态判定。 用限价订单(Limit Order)挂出场替代传统止盈(Take Profit),核心好处是不动原始交易逻辑:只在订单管理模块里把平仓动作改成反向限价挂单,回测样本里滑点环境更贴近真实盘口。 打开 MT5 的 EA 源码,搜 OrderSend 的 TP 参数赋值处,改成用 OrderSend 发 LIMIT 单,先拿 2020—2023 年 XAUUSD 的 M15 跑一遍,对比原 TP 版本的净值曲线。
◍ 随包附带的代码模块
这套双顶/双底 EA 不是单文件裸写,而是拆成了 6 个可复用模块。ZigZag.mqh 负责之字折线识别,Trends.mqh 做趋势搜索,Pattern.mqh 处理形态判定,LimitTakeProfit.mqh 用限价单替掉传统止盈,Header.mqh 是 EA 头文件,DoubleTop.mq5 才是最终跑策略的智能交易系统。 压缩包 MQL5.zip 体积 183.66 KB,在 MT5 里解压后直接能编译 DoubleTop.mq5。有用户在 2022 年 5 月反馈:欧元兑美元 M30 和 M5 用默认设置跑年初至今的行情,说明这套东西至少能原地复现,不用自己拼装类库。 外汇和贵金属杠杆高、滑点跳空频繁,限价单替换止盈也只是降低部分成交不确定性,不保证每次都吃到理想价位。模块拿来改参数前,先在策略测试器用历史数据验一遍逻辑。