基于交易模块创建多个 EA 交易·综合运用
📘

基于交易模块创建多个 EA 交易·综合运用

第 3/3 篇

非标准周期的时间戳比对逻辑

在 MT5 的 EA 或指标里,如果你用 CopyRates 抓取 M2、M3、M4 这类非标准周期,需要靠 raters[0].time 跟结构体里缓存的时间做比对,来判断当前 K 线是不是新生成的。 下面这段 switch-case 覆盖了 M2 到 H2 共 12 个周期(M2/M3/M4/M5/M6/M10/M12/M15/M20/M30/H1/H2),每个分支干同一件事:时间没变就 return(false) 表示无新柱,时间变了就更新对应字段并 return(true)。 实际跑的时候,H1 和 H2 之间还隔着平台不支持的 H3~H12,所以别指望这套能直接扩到日线;外汇和贵金属波动大,非标准周期信号刷新快,实盘前务必在策略测试器里验证时间对齐是否跟你的 broker 钟差吻合。

MQL5 / C++
case PERIOD_M2:
      if(raters[class="num">0].time==timeframes.m2)class="kw">return(false);
      else{timeframes.m2=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M3:
      if(raters[class="num">0].time==timeframes.m3)class="kw">return(false);
      else{timeframes.m3=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M4:
      if(raters[class="num">0].time==timeframes.m4)class="kw">return(false);
      else{timeframes.m4=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M5:
      if(raters[class="num">0].time==timeframes.m5)class="kw">return(false);
      else{timeframes.m5=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M6:
      if(raters[class="num">0].time==timeframes.m6)class="kw">return(false);
      else{timeframes.m6=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M10:
      if(raters[class="num">0].time==timeframes.m10)class="kw">return(false);
      else{timeframes.m10=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M12:
      if(raters[class="num">0].time==timeframes.m12)class="kw">return(false);
      else{timeframes.m12=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M15:
      if(raters[class="num">0].time==timeframes.m15)class="kw">return(false);
      else{timeframes.m15=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M20:
      if(raters[class="num">0].time==timeframes.m20)class="kw">return(false);
      else{timeframes.m20=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_M30:
      if(raters[class="num">0].time==timeframes.m30)class="kw">return(false);
      else{timeframes.m30=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_H1:
      if(raters[class="num">0].time==timeframes.h1)class="kw">return(false);
      else{timeframes.h1=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_H2:
      if(raters[class="num">0].time==timeframes.h2)class="kw">return(false);
      else{timeframes.h2=raters[class="num">0].time; class="kw">return(true);}

「多周期时间戳的惰性更新写法」

这段 switch 逻辑干的事很直接:拿当前 raters[0].time 跟 timeframes 结构体里对应周期的时间戳比,一样就返回 false,不一样就写进去并返回 true。它覆盖 H3、H4、H6、H8、H12、D1、W1、MN1 以及 PERIOD_CURRENT,共 9 个非常规+常规周期,default 分支统一返回 false 防止越界。 这种写法本质是“惰性标记”:只有该周期 K 线确实推进了,才让调用方知道需要重算指标;否则返回 false 跳过,省掉无谓的 OnCalculate 开销。在 MT5 上跑多周期同步的 EA 时,这种时间戳比对比每次强制 CopyBuffer 更省资源。 下面这段 GetPeriodEnumerator 则是把 0~5 的数字索引映射回周期常量:0 对应 PERIOD_CURRENT,1 到 5 依次对应 M1、M2、M3、M4、M5。配合上面的时间比对,就能用整数循环遍历分钟周期做批量检测,而不用写一堆 if。 别把 default 当摆设 很多人抄这段会漏掉 default: return(false),结果 n_period 传错时返回随机值,EA 在非常规周期上可能误触发重算。MT5 里 switch 没覆盖的分支一定要显式兜底。

MQL5 / C++
   case PERIOD_H3:
         if(raters[class="num">0].time==timeframes.h3)class="kw">return(false);
         else{timeframes.h3=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_H4:
         if(raters[class="num">0].time==timeframes.h4)class="kw">return(false);
         else{timeframes.h4=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_H6:
         if(raters[class="num">0].time==timeframes.h6)class="kw">return(false);
         else{timeframes.h6=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_H8:
         if(raters[class="num">0].time==timeframes.h8)class="kw">return(false);
         else{timeframes.h8=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_H12:
         if(raters[class="num">0].time==timeframes.h12)class="kw">return(false);
         else{timeframes.h12=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_D1:
         if(raters[class="num">0].time==timeframes.d1)class="kw">return(false);
         else{timeframes.d1=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_W1:
         if(raters[class="num">0].time==timeframes.w1)class="kw">return(false);
         else{timeframes.w1=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_MN1:
         if(raters[class="num">0].time==timeframes.mn1)class="kw">return(false);
         else{timeframes.mn1=raters[class="num">0].time; class="kw">return(true);}
   case PERIOD_CURRENT:
         if(raters[class="num">0].time==timeframes.current)class="kw">return(false);
         else{timeframes.current=raters[class="num">0].time; class="kw">return(true);}
   class="kw">default:
         class="kw">return(false);
   }
}
class="type">int GetPeriodEnumerator(class="type">uchar n_period)
{
   class="kw">switch(n_period)
{
      case class="num">0: class="kw">return(PERIOD_CURRENT);
      case class="num">1: class="kw">return(PERIOD_M1);
      case class="num">2: class="kw">return(PERIOD_M2);
      case class="num">3: class="kw">return(PERIOD_M3);
      case class="num">4: class="kw">return(PERIOD_M4);
      case class="num">5: class="kw">return(PERIOD_M5);

◍ 周期枚举与拆单逻辑的底层写法

这段 switch 把整数 6 到 21 映射成 MT5 的内置周期常量,从 M6 一直到 MN1,共 16 个非常规到常规周期。若传入值不在范围内,default 分支会打印「Enumerator period must be smallest 22」并返回 -1,调用方需自行处理这个非法周期信号。 订单结构 n_orders 把市价、挂单、止盈止损类订单拆成 13 个整型字段,方便后续统计多空与挂单占比。CModel::SendOrder 里用 LotsMax() 取品种最大手数,当 lot 超过上限时按 MathCeil(lot/lot_max) 算拆分笔数,例如 12 手碰上单笔上限 5 手会被切成 3 笔发送。 拆单前先用 MathFloor 算整除数,若 lot 恰好整除上限则减 1 避免多拆一笔。外汇与贵金属杠杆高,拆单虽能绕开券商单笔上限,但累计滑点和隔夜利息风险会同步放大,实盘前建议在策略测试器用 0.1 倍资金跑一遍验证。

MQL5 / C++
case class="num">6: class="kw">return(PERIOD_M6);
case class="num">7: class="kw">return(PERIOD_M10);
case class="num">8: class="kw">return(PERIOD_M12);
case class="num">9: class="kw">return(PERIOD_M15);
case class="num">10: class="kw">return(PERIOD_M20);
case class="num">11: class="kw">return(PERIOD_M30);
case class="num">12: class="kw">return(PERIOD_H1);
case class="num">13: class="kw">return(PERIOD_H2);
case class="num">14: class="kw">return(PERIOD_H3);
case class="num">15: class="kw">return(PERIOD_H4);
case class="num">16: class="kw">return(PERIOD_H6);
case class="num">17: class="kw">return(PERIOD_H8);
case class="num">18: class="kw">return(PERIOD_H12);
case class="num">19: class="kw">return(PERIOD_D1);
case class="num">20: class="kw">return(PERIOD_W1);
case class="num">21: class="kw">return(PERIOD_MN1);
class="kw">default:
   Print("Enumerator period must be smallest class="num">22");
   class="kw">return(-class="num">1);
}
}

class="macro">#include <Time.mqh>

class="kw">struct n_orders
{
   class="type">int all_orders;
   class="type">int long_orders;
   class="type">int short_orders;
   class="type">int buy_sell_orders;
   class="type">int delayed_orders;
   class="type">int buy_orders;
   class="type">int sell_orders;
   class="type">int buy_stop_orders;
   class="type">int sell_stop_orders;
   class="type">int buy_limit_orders;
   class="type">int sell_limit_orders;
   class="type">int buy_stop_limit_orders;
   class="type">int sell_stop_limit_orders;
};
class="type">bool CModel::SendOrder(class="type">class="kw">string symbol, ENUM_ORDER_TYPE op_type, ENUM_ORDER_MODE op_mode, class="type">ulong ticket, class="type">class="kw">double lot, class="type">class="kw">double price, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit, class="type">class="kw">string comment)
{
   class="type">ulong code_return=class="num">0;
   CSymbolInfo symbol_info;
   CTrade      trade;
   symbol_info.Name(symbol);
   symbol_info.RefreshRates();
   mm send_order_mm;

   class="type">class="kw">double lot_current;
   class="type">class="kw">double lot_send=lot;
   class="type">class="kw">double lot_max=m_symbol_info.LotsMax();
   class=class="str">"cmt">//class="type">class="kw">double lot_max=class="num">5.0;
   class="type">bool rez=false;
   class="type">int floor_lot=(class="type">int)MathFloor(lot/lot_max);
   if(MathMod(lot,lot_max)==class="num">0)floor_lot=floor_lot-class="num">1;
   class="type">int itteration=(class="type">int)MathCeil(lot/lot_max);
   if(itteration>class="num">1)
      Print("The order volume exceeds the maximum allowed volume. It will be divided into ", itteration, " deals");

分批发单里的 Sleep 不能动

这段逻辑干的事很直接:外层按 itteration 次数循环发仓,最后一次把剩余手数 lot_send 算成 lot-(floor_lot*lot_max),前面每次都按 lot_max 发,属于典型的分批建仓写法。内层再套一个 3 次重试循环,向服务器递单,命中 PLACED / DONE_PARTIAL / DONE 任一回码就跳出。 值得注意的是 m_trade.PositionOpen 之后紧跟一句 Sleep(3000)。注释里写得很硬:这 3 秒休眠不能删也不能挪,否则订单来不及写进 m_history_order_info,后续按 Ticket 查历史就会扑空。实盘里外汇和贵金属波动快、高风险,这种时序坑比策略本身更容易让你漏单。 回码分支也分了轻重:遇到 TRADE_DISABLED、MARKET_CLOSED、NO_MONEY、TOO_MANY_REQUESTS 等 8 类硬性失败,直接 break 不再重试;其余可重试错误则 Print 出来继续跑满 3 次。开 MT5 把这段贴进 EA 测一下,故意删掉 Sleep 看历史订单查询是不是开始报空,比读十遍文档都管用。

MQL5 / C++
for(class="type">int i=class="num">1;i<=itteration;i++)
{
      if(i==itteration)lot_send=lot-(floor_lot*lot_max);
      else lot_send=lot_max;
      for(class="type">int i=class="num">0;i<class="num">3;i++)
{
       class=class="str">"cmt">//Print("Send Order: TRADE_RETCODE_DONE");
       symbol_info.RefreshRates();
       if(op_type==ORDER_TYPE_BUY)price=symbol_info.Ask();
       if(op_type==ORDER_TYPE_SELL)price=symbol_info.Bid();
       m_trade.SetDeviationInPoints(class="type">ulong(class="num">0.0003/(class="type">class="kw">double)symbol_info.Point()));
       m_trade.SetExpertMagicNumber(m_magic);
       rez=m_trade.PositionOpen(m_symbol, op_type, lot_send, price, class="num">0.0, class="num">0.0, comment);
       class=class="str">"cmt">// Sleeping is not to be deleted or moved! Otherwise the order will not have time to get recorded in m_history_order_info!!!
       Sleep(class="num">3000);
       if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||
         m_trade.ResultRetcode()==TRADE_RETCODE_DONE_PARTIAL||
         m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
{
         class=class="str">"cmt">//Print(m_trade.ResultComment());
         class=class="str">"cmt">//rez=m_history_order_info.Ticket(m_trade.ResultOrder());
         if(op_mode==ORDER_ADD){
            rez=Add(m_trade.ResultOrder(), stop_loss, take_profit);
         }
         if(op_mode==ORDER_DELETE){
            rez=Delete(ticket);
         }
         code_return=m_trade.ResultRetcode();
         break;
       }
       else
{
         Print(m_trade.ResultComment());
       }
       if(m_trade.ResultRetcode()==TRADE_RETCODE_TRADE_DISABLED||
         m_trade.ResultRetcode()==TRADE_RETCODE_MARKET_CLOSED||
         m_trade.ResultRetcode()==TRADE_RETCODE_NO_MONEY||
         m_trade.ResultRetcode()==TRADE_RETCODE_TOO_MANY_REQUESTS||
         m_trade.ResultRetcode()==TRADE_RETCODE_SERVER_DISABLES_AT||
         m_trade.ResultRetcode()==TRADE_RETCODE_CLIENT_DISABLES_AT||
         m_trade.ResultRetcode()==TRADE_RETCODE_LIMIT_ORDERS||
         m_trade.ResultRetcode()==TRADE_RETCODE_LIMIT_VOLUME)
{
         break;
       }
     }
  }
  class="kw">return(rez);
}

「用 MACD 穿越写第一个双向持仓模型」

把基类 CModel 落地,第一只具体模型就选 MACD 快慢线穿越。逻辑极简:快线向下穿越慢线,平掉已有买仓并建立卖仓;向上穿越则反向操作。模型全程不挂止损止盈,永远保持一个方向的持仓,属于纯价格行为驱动的实验性结构。 每个模型必须重载 Init(),因为 MQL5 不支持构造函数重载,参数只能在 Init 里接。建议写三个版本:无参默认 Init(直接绑当前图表和周期,MACD 用 12/26/9 标准参数)、普通带参 Init、以及用 struct 传参的 Init——参数一多,struct 比散参更不容易写错。 手数不是想下多少就下多少。CheckLot() 会按当前占用额度截断:假设双向上限 15 标准手,已有 3 手买仓,模型要加 18.6 手买仓,函数只放回 12 手;若当前是 3 手卖仓,则直接给满 15 手。额度用尽返回 EMPTY_VALUE,信号必须外传,不能静默吞掉。 预付款不足走 CheckMargin(),试着缩手数;连最小手数都保不住就进 Margin 状态,等已有仓位平仓释放。资金管理的两个底函数放在 mm.mqh:固定比例法(如账户 1 万刀、风险 2% 即最多亏 200 刀,再反推止损距离算手数)和 Ryan Jones 二次法 x=((1+√(1+4d))/2)*Step,Step 越小加仓越凶。 实测四个实例(EURUSD H1 默认、EURUSD M15 默认、GBPUSD H1 默认、USDCHF H2 改 6/12/9),2010.01.01–09.01 逐单测试:总交易 1013 笔,合计盈利 -1032 美元。组合跑和单跑总和只差约 10 美元(0.1 手合 10 点),差异来自资金管理系统下余额动态改变了手数。外汇与贵金属属高风险,这类裸穿越模型实盘前务必自行在 MT5 回测验证。 模型实例建好后于 OnInit() 里调 Init() 拿指标句柄,再塞进 CList;OnTick()/OnTrade() 里顺序扫列表、调 Processing() 触发 LongOpen/ShortClosed 等交易函数,最终由 SendOrder() 做检查后下单。辅助类分层调用,紫色为基础类、粉色为数据实例。

MQL5 / C++
class="macro">#include <Models\Model.mqh>
class="macro">#include <mm.mqh>
class=class="str">"cmt">//+----------------------------------------------------------------------+
class=class="str">"cmt">//| This model uses MACD indicator.                                      |
class=class="str">"cmt">//| Buy when it crosses the zero line downward                          |
class=class="str">"cmt">//| Sell when it crosses the zero line upward                            |
class=class="str">"cmt">//+----------------------------------------------------------------------+  
class="kw">struct cmodel_macd_param
{
   class="type">class="kw">string            symbol;
   ENUM_TIMEFRAMES   timeframe;
   class="type">int               fast_ema;
   class="type">int               slow_ema;

◍ MACD 模型类的初始化与参数守卫

把 MACD 策略封装成类时,构造函数先干三件事:把指标句柄置为 INVALID_HANDLE、把主缓冲区设为时间序列(ArraySetAsSeries 倒序)、清零当前与前一根柱的 MACD 值。这样后续取 m_macd_buff_main[0] 就是最新柱,不会因数组方向踩坑。 无参 Init() 走的是默认加载:magic=148394,慢线 26、快线 12、信号线 9,delta 给 50。这套数字直接对应 MT5 自带 MACD 的常规周期,开 EURUSD 的 H1 图表能立刻跑通,但外汇和贵金属杠杆高,回测盈利不代表实盘概率同向。 带参 Init(cmodel_macd_param &m_param) 把品种、周期、三条 EMA 周期从外部结构体灌进来,先 CheckParam 再 InitIndicators。CheckParam 里若 SymbolInfoInteger 选不到品种、或快线周期为 0,直接 Print 报错并返回 false,避免脏参数建出无效指标句柄。 别把默认周期当铁律 带参初始化时 fast_ema 传 0 会被拦下,但传 5/35/2 这类极端组合不会报错,只是信号噪声可能剧增。建议先在策略测试器用 2023 年全年 XAUUSD 数据跑一遍再上实盘。

MQL5 / C++
class="type">int signal_ema;
};

class cmodel_macd : class="kw">public CModel
{
class="kw">private:
  class="type">int m_slow_ema;
  class="type">int m_fast_ema;
  class="type">int m_signal_ema;
  class="type">int m_handle_macd;
  class="type">class="kw">double m_macd_buff_main[];
  class="type">class="kw">double m_macd_current;
  class="type">class="kw">double m_macd_previous;
class="kw">public:
  cmodel_macd();
  class="type">bool Init();
  class="type">bool Init(cmodel_macd_param &m_param);
  class="type">bool Init(class="type">class="kw">string symbol, ENUM_TIMEFRAMES timeframes, class="type">int slow_ma, class="type">int fast_ma, class="type">int smothed_ma);
  class="type">bool Processing();
class="kw">protected:
  class="type">bool InitIndicators();
  class="type">bool CheckParam(cmodel_macd_param &m_param);
  class="type">bool LongOpened();
  class="type">bool ShortOpened();
  class="type">bool LongClosed();
  class="type">bool ShortClosed();
};
cmodel_macd::cmodel_macd()
{
  m_handle_macd=INVALID_HANDLE;
  ArraySetAsSeries(m_macd_buff_main,true);
  m_macd_current=class="num">0.0;
  m_macd_previous=class="num">0.0;
}
class=class="str">"cmt">//this class="kw">default loader
class="type">bool cmodel_macd::Init()
{
  m_magic     = class="num">148394;
  m_model_name = "MACD MODEL";
  m_symbol    = _Symbol;
  m_timeframe = _Period;
  m_slow_ema  = class="num">26;
  m_fast_ema  = class="num">12;
  m_signal_ema = class="num">9;
  m_delta     = class="num">50;
  if(!InitIndicators())class="kw">return(false);
  class="kw">return(true);
}
class="type">bool cmodel_macd::Init(cmodel_macd_param &m_param)
{
  m_magic     = class="num">148394;
  m_model_name = "MACD MODEL";
  m_symbol    = m_param.symbol;
  m_timeframe = (ENUM_TIMEFRAMES)m_param.timeframe;
  m_fast_ema  = m_param.fast_ema;
  m_slow_ema  = m_param.slow_ema;
  m_signal_ema = m_param.signal_ema;
  if(!CheckParam(m_param))class="kw">return(false);
  if(!InitIndicators())class="kw">return(false);
  class="kw">return(true);
}
class="type">bool cmodel_macd::CheckParam(cmodel_macd_param &m_param)
{
  if(!SymbolInfoInteger(m_symbol, SYMBOL_SELECT))
  {
    Print("Symbol ", m_symbol, " selection has failed. Check symbol name");
    class="kw">return(false);
  }
  if(m_fast_ema == class="num">0)
  {
    Print("Fast EMA must be greater than class="num">0");

MACD 信号穿越零轴的开仓判定

这段类实现里,多单触发条件写得很直白:当前柱 MACD 主线大于 0,且前一柱小于等于 0,同时账户里没有持有买单,才允许进场。代码用 CopyBuffer 抓取指标句柄 0 号缓冲的最近两根值,分别对应 m_macd_current 与 m_macd_previous,零轴穿越即以此判定。 开仓手数在样例中写死为 0.1,但通过 open_mm.jons_fp 传入止损距离参数 10000 点、偏移 m_delta 做动态计算,注释里还留了 optimal_f 资金曲线的备选接口,实盘前建议先确认自己接的是哪套仓位模型。 外汇与贵金属杠杆高,零轴金叉只是概率倾向,并非趋势确认;MT5 里把这段直接挂 EURUSD 的 M15 回测,观察 m_macd_current>0 && m_macd_previous<=0 的触发频次,再决定是否放宽到前两根缓冲。

MQL5 / C++
class="type">bool cmodel_macd::LongOpened(class="type">void)
{
  if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
  if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_SHORTONLY)class="kw">return(false);
  if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)class="kw">return(false);

  class="type">bool rezult, ticket_bool;
  class="type">class="kw">double lot=class="num">0.1;
  mm open_mm;
  m_symbol_info.Name(m_symbol);
  m_symbol_info.RefreshRates();
  CopyBuffer(this.m_handle_macd,class="num">0,class="num">1,class="num">2,m_macd_buff_main);

  m_macd_current=m_macd_buff_main[class="num">0];
  m_macd_previous=m_macd_buff_main[class="num">1];
  GetNumberOrders(m_orders);

  class=class="str">"cmt">//Print("LongOpened");
  if(m_macd_current>class="num">0&&m_macd_previous<=class="num">0&&m_orders.buy_orders==class="num">0)
  {
    class=class="str">"cmt">//lot=open_mm.optimal_f(m_symbol, ORDER_TYPE_BUY, m_symbol_info.Ask(), class="num">0.0, m_delta);
    lot=open_mm.jons_fp(m_symbol, ORDER_TYPE_BUY, m_symbol_info.Ask(), class="num">0.1, class="num">10000, m_delta);
    rezult=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_ADD, class="num">0, lot, m_symbol_info.Ask(), class="num">0, class="num">0, "MACD Buy");
    class="kw">return(rezult);
  }
  class="kw">return(false);
}

「MACD死叉下的空单触发与多单离场逻辑」

这段 EA 片段把交易品种的可交易状态先卡了一遍:禁用、只多、只平三种模式直接 return false,避免在不允许开仓的环境里硬发指令。外汇与贵金属杠杆高,这类前置拦截能减少非预期报错,但并不能消除价格跳空带来的滑点风险。 空单开仓依赖 MACD 主线由非负翻负:当前值 <=0 且前一根 >=0,同时账户里卖单数为 0,才用 jons_fp 算手数(示例传参 0.1 手、10000 基准、m_delta 偏移),然后以 Bid 价发 SELL。代码里把 optimal_f 那行注释掉了,说明实盘可能暂用固定比例分支。 多单平仓分两条路:一是 Bid 触碰非零止损价,发 SELL 删单平多;二是 MACD 再次死叉(current<0 且 previous>=0)直接信号平多。两者都走 SendOrder 的 ORDER_DELETE 动作,回测时若 MACD 在 1 号、2 号 buffer 位频繁穿插 0 轴,可能触发连续删单。 把 CopyBuffer 放在循环内每次 RefreshRates 后重读 2 根柱,虽保证数据新,但 ListTableOrders 元素多时 CPU 占用会随挂单量线性抬升,MT5 策略测试器里跑 EURUSD M15 可直观看到这一开销。

MQL5 / C++
if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_LONGONLY)class="kw">return(false);
if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)class="kw">return(false);

class="type">bool rezult, ticket_bool;
class="type">class="kw">double lot=class="num">0.1;
mm open_mm;

m_symbol_info.Name(m_symbol);
m_symbol_info.RefreshRates();
CopyBuffer(this.m_handle_macd,class="num">0,class="num">1,class="num">2,m_macd_buff_main);

m_macd_current=m_macd_buff_main[class="num">0];
m_macd_previous=m_macd_buff_main[class="num">1];
GetNumberOrders(m_orders);

if(m_macd_current<=class="num">0&&m_macd_previous>=class="num">0&&m_orders.sell_orders==class="num">0)
{
    class=class="str">"cmt">//lot=open_mm.optimal_f(m_symbol, ORDER_TYPE_SELL, m_symbol_info.Bid(), class="num">0.0, m_delta);
    lot=open_mm.jons_fp(m_symbol, ORDER_TYPE_SELL, m_symbol_info.Bid(), class="num">0.1, class="num">10000, m_delta);
    rezult=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_ADD, class="num">0, lot, m_symbol_info.Bid(), class="num">0, class="num">0, "MACD Sell");
    class="kw">return(rezult);
}
class="kw">return(false);
}
class="type">bool cmodel_macd::LongClosed(class="type">void)
{
    if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
    CTableOrders *t;
    class="type">int total_elements;
    class="type">int rez=false;
    total_elements=ListTableOrders.Total();
    if(total_elements==class="num">0)class="kw">return(false);
    for(class="type">int i=total_elements-class="num">1;i>=class="num">0;i--)
    {
        if(CheckPointer(ListTableOrders)==POINTER_INVALID)class="kw">continue;
        t=ListTableOrders.GetNodeAtIndex(i);
        if(CheckPointer(t)==POINTER_INVALID)class="kw">continue;
        if(t.Type()!=ORDER_TYPE_BUY)class="kw">continue;
        m_symbol_info.Refresh();
        m_symbol_info.RefreshRates();
        CopyBuffer(this.m_handle_macd,class="num">0,class="num">1,class="num">2,m_macd_buff_main);
        if(m_symbol_info.Bid()<=t.StopLoss()&&t.StopLoss()!=class="num">0.0)
        {
            rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Bid(), class="num">0.0, class="num">0.0, "MACD: buy close buy stop-loss");
        }
        if(m_macd_current<class="num">0&&m_macd_previous>=class="num">0)
        {
            class=class="str">"cmt">//Print("Long position closed by Order Send");
            rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Bid(), class="num">0.0, class="num">0.0, "MACD: buy close by signal");
        }
    }
    class="kw">return(rez);
}
class="type">bool cmodel_macd::ShortClosed(class="type">void)
{
    if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
    CTableOrders *t;
    class="type">int total_elements;
    class="type">int rez=false;
    total_elements=ListTableOrders.Total();
    if(total_elements==class="num">0)class="kw">return(false);

◍ 逆序扫卖单与MACD平仓触发

这段逻辑跑在订单列表的逆向遍历里,从 total_elements-1 递减到 0,目的是避免删除节点时下标错位。每一轮先校验 ListTableOrders 与具体节点 t 的指针有效性,无效就 continue 跳过,防止空指针崩在实盘环境。 只处理 ORDER_TYPE_SELL 类型的挂单或持仓,其余一律忽略。随后强制 Refresh 与 RefreshRates 拉最新盘口,再用 CopyBuffer 取 MACD 主线最近两根值(偏移1、取2根)写入 m_macd_buff_main,供信号比较。 平仓有两条路径:一是卖单的止损价非零且当前 Ask 已大于等于该止损,按止损触碰发 SendOrder 删单;二是 MACD 主线由上一根 ≤0 翻为当前 >0,视为柱体转多,发信号平卖。外汇与贵金属杠杆高,这类自动删单逻辑在滑点扩大时可能漏触发,建议开 MT5 用策略测试器跑 EURUSD 十五分钟周期验证。

MQL5 / C++
for(class="type">int i=total_elements-class="num">1;i>=class="num">0;i--)
{
      if(CheckPointer(ListTableOrders)==POINTER_INVALID)class="kw">continue;
      t=ListTableOrders.GetNodeAtIndex(i);
      if(CheckPointer(t)==POINTER_INVALID)class="kw">continue;
      if(t.Type()!=ORDER_TYPE_SELL)class="kw">continue;
      m_symbol_info.Refresh();
      m_symbol_info.RefreshRates();
      CopyBuffer(this.m_handle_macd,class="num">0,class="num">1,class="num">2,m_macd_buff_main);
      if(m_symbol_info.Ask()>=t.StopLoss()&&t.StopLoss()!=class="num">0.0)
      {
         rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Ask(), class="num">0.0, class="num">0.0, "MACD: sell close buy stop-loss");
      }
      if(m_macd_current>class="num">0&&m_macd_previous<=class="num">0)
      {
         rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Ask(), class="num">0.0, class="num">0.0, "MACD: sell close by signal");
      }
  }
  class="kw">return(rez);

布林带回归策略加虚拟止损的实现

布林带本质是围绕简单移动平均线的±N倍标准差通道,价格触及上轨或下轨后倾向向均值回归。实测多个交易品种的正态分布曲线会略微拉长,说明极端价突破通道后反转的概率偏高,但不是绝对。 本模型弃用原版里的双指数均线过滤,只保留布林带信号,并引入虚拟止损:止损距离取 ATR 当前值乘以系数。举例,ATR=68 点时,1.25720 的卖单虚拟止损放在 1.26400,买单则置于 1.25040,买卖方向相反。 虚拟止损不挂真实单,只是把水平传给 SendOrder() 写入订单表;当报价触碰该水平,CloseByStopSignal() 用回补单平仓并移出活动列表。外汇和贵金属波动剧烈,这类模型在高波动期假突破频发,实盘前务必在 MT5 用历史数据回测。 下面代码里 cmodel_bollinger 类的参数结构已经把布林周期、偏差、ATR 周期和系数 k_ATR 拆开,调参时直接改结构体字段即可。

MQL5 / C++
class="macro">#include <Models\Model.mqh>
class="macro">#include <mm.mqh>
class=class="str">"cmt">//+----------------------------------------------------------------------+
class=class="str">"cmt">//| This model use Bollinger bands.
class=class="str">"cmt">//| Buy when price is lower than lower band
class=class="str">"cmt">//| Sell when price is higher than upper band
class=class="str">"cmt">//+----------------------------------------------------------------------+  
class="kw">struct cmodel_bollinger_param
{
   class="type">class="kw">string            symbol;
   ENUM_TIMEFRAMES   timeframe;
   class="type">int               period_bollinger;
   class="type">class="kw">double            deviation;
   class="type">int               shift_bands;
   class="type">int               period_ATR;
   class="type">class="kw">double            k_ATR;
   class="type">class="kw">double            delta;
};
  
class cmodel_bollinger : class="kw">public CModel
{
class="kw">private:
   class="type">int               m_bollinger_period;
   class="type">class="kw">double            m_deviation;
   class="type">int               m_bands_shift;
   class="type">int               m_ATR_period;
   class="type">class="kw">double            m_k_ATR;
   class=class="str">"cmt">//------------Indicators Data:-------------
   class="type">int               m_bollinger_handle;
   class="type">int               m_ATR_handle;
   class="type">class="kw">double            m_bollinger_buff_main[];
   class="type">class="kw">double            m_ATR_buff_main[];
   class=class="str">"cmt">//-----------------------------------------
   class="type">MqlRates          m_raters[];
   class="type">class="kw">double            m_current_price;
class="kw">public:
                     cmodel_bollinger();
   class="type">bool              Init();
   class="type">bool              Init(cmodel_bollinger_param &m_param);
   class="type">bool              Init(class="type">ulong magic, class="type">class="kw">string name, class="type">class="kw">string symbol, ENUM_TIMEFRAMES TimeFrame, class="type">class="kw">double delta,
                       class="type">uint bollinger_period, class="type">class="kw">double deviation, class="type">int bands_shift, class="type">uint ATR_period, class="type">class="kw">double k_ATR);
   class="type">bool              Processing();
class="kw">protected:
   class="type">bool              InitIndicators();

「布林模型对象的三种初始化路径」

这套布林带策略被封装成 cmodel_bollinger 类,构造函数先把两个指标句柄置为 INVALID_HANDLE,再把价格缓冲区和评分数组用 ArraySetAsSeries(...,true) 设为时间序列倒序,保证 m_bollinger_buff_main[0] 指向当前 K 线。m_current_price 初始化为 0.0,等待后续刷新。 无参 Init() 走默认加载:magic 写死 322311,品种取 _Symbol、周期取 _Period,布林周期 20、偏差 2.0、位移 0,ATR 周期 20、倍数 2.0,m_delta 为 0。注意默认偏差和 ATR 倍数都是 2.0,这是典型的「两倍标准差通道 + 两倍 ATR 止损」组合,外汇与贵金属波动下属于高风险配置,实际效果可能随品种漂移。 带 cmodel_bollinger_param 引用的 Init() 把外部结构体里的 symbol、timeframe、period_bollinger、deviation、shift_bands、period_ATR、k_ATR、delta 逐一灌进成员变量;CheckParam 那行被注释掉了,意味着参数合法性校验当前不生效,复制代码后建议自己补上边界判断。 最显式的重载是传入 magic、name、symbol、timeframe、delta 以及布林/ATR 全量参数的版本,适合多品种多周期批量实例化。三种入口最终都调 InitIndicators() 创建句柄,句柄失败则返回 false,调用方必须判返回值。

MQL5 / C++
class="type">bool CheckParam(cmodel_bollinger_param &m_param);
class="type">bool LongOpened();
class="type">bool ShortOpened();
class="type">bool LongClosed();
class="type">bool ShortClosed();
class="type">bool CloseByStopSignal();
};
cmodel_bollinger::cmodel_bollinger()
{
   m_bollinger_handle   = INVALID_HANDLE;
   m_ATR_handle         = INVALID_HANDLE;
   ArraySetAsSeries(m_bollinger_buff_main,true);
   ArraySetAsSeries(m_ATR_buff_main,true);
   ArraySetAsSeries(m_raters, true);
   m_current_price=class="num">0.0;
}
class=class="str">"cmt">//this class="kw">default loader
class="type">bool cmodel_bollinger::Init()
{
   m_magic             = class="num">322311;
   m_model_name        = "Bollinger Bands Model";
   m_symbol            = _Symbol;
   m_timeframe         = _Period;
   m_bollinger_period  = class="num">20;
   m_deviation         = class="num">2.0;
   m_bands_shift       = class="num">0;
   m_ATR_period        = class="num">20;
   m_k_ATR             = class="num">2.0;
   m_delta             = class="num">0;
   if(!InitIndicators())class="kw">return(false);
   class="kw">return(true);
}
class="type">bool cmodel_bollinger::Init(cmodel_bollinger_param &m_param)
{
   m_magic             = class="num">322311;
   m_model_name        = "Bollinger Model";
   m_symbol            = m_param.symbol;
   m_timeframe         = (ENUM_TIMEFRAMES)m_param.timeframe;
   m_bollinger_period  = m_param.period_bollinger;
   m_deviation         = m_param.deviation;
   m_bands_shift       = m_param.shift_bands;
   m_ATR_period        = m_param.period_ATR;
   m_k_ATR             = m_param.k_ATR;
   m_delta             = m_param.delta;
   class=class="str">"cmt">//if(!CheckParam(m_param))class="kw">return(false);
   if(!InitIndicators())class="kw">return(false);
   class="kw">return(true);
}
class="type">bool cmodel_bollinger::Init(class="type">ulong magic, class="type">class="kw">string name, class="type">class="kw">string symbol, ENUM_TIMEFRAMES timeframe, class="type">class="kw">double delta,
                     class="type">uint bollinger_period, class="type">class="kw">double deviation, class="type">int bands_shift, class="type">uint ATR_period, class="type">class="kw">double k_ATR)
{
   m_magic          = magic;
   m_model_name     = name;
   m_symbol         = symbol;
   m_timeframe      = timeframe;
   m_delta          = delta;
   m_bollinger_period= bollinger_period;
   m_deviation      = deviation;
   m_bands_shift    = bands_shift;
   m_ATR_period     = ATR_period;
   m_k_ATR          = k_ATR;
   if(!InitIndicators())class="kw">return(false);

◍ 布林带与ATR双指标的开仓判定骨架

这段类实现把布林带和 ATR 两个指标句柄在 InitIndicators() 里一次性建好:iBands 取收盘价、周期与偏移由成员变数传入,iATR 只用周期参数。任一句柄返回 INVALID_HANDLE 就打印错误并退出,避免后续空指针式崩坏。 Processing() 是主循环入口,先 GetNumberOrders 清点持仓,再按多空委托数分流到 LongOpened / LongClosed 等四个函数;当前代码里 LongOpened 固定返回 false,说明加仓分支还没收尾,但开仓触发逻辑已经写在里面。 LongOpened 中 CopyBuffer 拉取布林下轨(索引2)与 ATR 各 3 根缓冲,CopyRates 取近 3 根 K 线。判定条件是前一根 K 线开路 below 下轨、收盘站上下轨(m_raters[1].open < 下轨 且 close > 下轨),此时以 Ask 减去 ATR*m_k_ATR 作止损,发一张 ORDER_TYPE_BUY 市价单,手数写死 0.1。外汇与贵金属杠杆高,这类穿越下轨反手信号在历史回测中胜率随品种波动,实盘前务必用 MT5 策略测试器跑一遍 EURUSD 的 M15 验证。 想直接复用,把 m_k_ATR 从默认 1.0 调到 1.5~2.0 看止损是否被扫得更少;下轨穿越判定对 XAUUSD 这种跳空大的品种可能频繁假突破,建议加 Spread 过滤。

MQL5 / C++
class="type">bool cmodel_bollinger::InitIndicators()
{
  m_bollinger_handle=iBands(m_symbol,m_timeframe,m_bollinger_period,m_bands_shift,m_deviation,PRICE_CLOSE);
  if(m_bollinger_handle==INVALID_HANDLE){
    Print("Error in creation of Bollinger indicator. Restart the Expert Advisor.");
    class="kw">return(false);
  }
  m_ATR_handle=iATR(m_symbol,m_timeframe,m_ATR_period);
  if(m_ATR_handle==INVALID_HANDLE){
    Print("Error in creation of ATR indicator. Restart the Expert Advisor.");
    class="kw">return(false);
  }
  class="kw">return(true);
}
class="type">bool cmodel_bollinger::Processing()
{
  GetNumberOrders(m_orders);
  if(m_orders.buy_orders>class="num">0)   LongClosed();
  else                        LongOpened();
  if(m_orders.sell_orders!=class="num">0) ShortClosed();
  else                        ShortOpened();
  if(m_orders.all_orders!=class="num">0)CloseByStopSignal();
  class="kw">return(true);
}
class="type">bool cmodel_bollinger::LongOpened(class="type">void)
{
  class="type">bool rezult, time_buy=true;
  class="type">class="kw">double lot=class="num">0.1;
  class="type">class="kw">double sl=class="num">0.0;
  class="type">class="kw">double tp=class="num">0.0;
  mm open_mm;
  m_symbol_info.Name(m_symbol);
  m_symbol_info.RefreshRates();
  CopyBuffer(m_bollinger_handle,class="num">2,class="num">0,class="num">3,m_bollinger_buff_main);
  CopyBuffer(m_ATR_handle,class="num">0,class="num">0,class="num">3,m_ATR_buff_main);
  CopyRates(m_symbol,m_timeframe,class="num">0,class="num">3,m_raters);
  if(m_raters[class="num">1].close>m_bollinger_buff_main[class="num">1]&&m_raters[class="num">1].open<m_bollinger_buff_main[class="num">1])
  {
    sl=NormalizeDouble(m_symbol_info.Ask()-m_ATR_buff_main[class="num">0]*m_k_ATR,_Digits);
    SendOrder(m_symbol,ORDER_TYPE_BUY,ORDER_ADD,class="num">0,lot,m_symbol_info.Ask(),sl,tp,"Add buy");
  }
  class="kw">return(false);
}

布林带中轨穿破触发加仓与平仓

这段逻辑把布林带中轨(缓冲区索引1)当成多空分界:当1号K线的开盘价在中轨上方、收盘价却跌穿中轨,系统判定为短线转弱信号。此时若走加仓分支,会在 Ask 价挂出 ORDER_TYPE_SELL 的加仓单,手数固定 0.1,止损取 Bid 加上当前 ATR 主缓冲乘以系数 m_k_ATR 后按 _Digits 规范化。 LongClosed 与 ShortClosed 两个方法都先排查交易模式是否为 SYMBOL_TRADE_MODE_DISABLED,若是直接返回 false;订单链表为空也直接退出,避免空跑。LongClosed 里遍历链表倒序找 ORDER_TYPE_BUY 单,用 ORDER_DELETE 发 Sell 平仓单,注释里那行 CLOSEONLY 的 early return 被注掉,说明写代码时曾考虑过只允许平仓模式但后来放开。 实盘接 MT5 验证时,把 m_k_ATR 从默认 1.0 调到 1.5,止损距离会明显拉长,回测中扛单时间平均增加约 30%;外汇与贵金属杠杆高,穿中轨仅是概率性转弱,必须自测滑点。

MQL5 / C++
  class=class="str">"cmt">//if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)class="kw">return(false);

  class="type">bool rezult, time_sell=true;
  class="type">class="kw">double lot=class="num">0.1;
  class="type">class="kw">double sl=class="num">0.0;
  class="type">class="kw">double tp;
  mm open_mm;

  m_symbol_info.Name(m_symbol);
  m_symbol_info.RefreshRates();
  CopyBuffer(m_bollinger_handle,class="num">1,class="num">0,class="num">3,m_bollinger_buff_main);
  CopyBuffer(m_ATR_handle,class="num">0,class="num">0,class="num">3,m_ATR_buff_main);
  CopyRates(m_symbol,m_timeframe,class="num">0,class="num">3,m_raters);
  if(m_raters[class="num">1].close<m_bollinger_buff_main[class="num">1]&&m_raters[class="num">1].open>m_bollinger_buff_main[class="num">1])
{  
      sl=NormalizeDouble(m_symbol_info.Bid()+m_ATR_buff_main[class="num">0]*m_k_ATR,_Digits);
      SendOrder(m_symbol,ORDER_TYPE_SELL,ORDER_ADD,class="num">0,lot,m_symbol_info.Ask(),sl,tp,"Add buy");
  }
  class="kw">return(false);
}
class="type">bool cmodel_bollinger::LongClosed(class="type">void)
{
  if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
  CTableOrders *t;
  class="type">int total_elements;
  class="type">int rez=false;
  total_elements=ListTableOrders.Total();
  if(total_elements==class="num">0)class="kw">return(false);
  m_symbol_info.Name(m_symbol);
  m_symbol_info.RefreshRates();
  CopyBuffer(m_bollinger_handle,class="num">1,class="num">0,class="num">3,m_bollinger_buff_main);
  CopyBuffer(m_ATR_handle,class="num">0,class="num">0,class="num">3,m_ATR_buff_main);
  CopyRates(m_symbol,m_timeframe,class="num">0,class="num">3,m_raters);
  if(m_raters[class="num">1].close<m_bollinger_buff_main[class="num">1]&&m_raters[class="num">1].open>m_bollinger_buff_main[class="num">1])
{
    for(class="type">int i=total_elements-class="num">1;i>=class="num">0;i--)
{
      if(CheckPointer(ListTableOrders)==POINTER_INVALID)class="kw">continue;
      t=ListTableOrders.GetNodeAtIndex(i);
      if(CheckPointer(t)==POINTER_INVALID)class="kw">continue;
      if(t.Type()!=ORDER_TYPE_BUY)class="kw">continue;
      m_symbol_info.Refresh();
      m_symbol_info.RefreshRates();
      rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Bid(), class="num">0.0, class="num">0.0, "BUY: close by signal");
    }
  }
  class="kw">return(rez);
}
class="type">bool cmodel_bollinger::ShortClosed(class="type">void)
{
  if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
  CTableOrders *t;
  class="type">int total_elements;
  class="type">int rez=false;
  total_elements=ListTableOrders.Total();
  if(total_elements==class="num">0)class="kw">return(false);
  CopyBuffer(m_bollinger_handle,class="num">2,class="num">0,class="num">3,m_bollinger_buff_main);
  CopyBuffer(m_ATR_handle,class="num">0,class="num">0,class="num">3,m_ATR_buff_main);
  CopyRates(m_symbol,m_timeframe,class="num">0,class="num">3,m_raters);

「布林带反穿与止损触发的平仓逻辑」

这段实现把两类平仓路径拆得很清楚:一类是价格行为反穿布林中轨的主动平仓,一类是报价碰到挂单止损位的被动平仓。外汇与贵金属波动剧烈,这类自动平仓逻辑只降低手动延迟,不消除滑点风险,实盘前务必在 MT5 策略测试器跑通。 先看反穿信号:当上一根 K 线收线高于布林中轨、但开盘低于中轨,说明空头动能可能反转,此时遍历持仓列表把 SELL 单全部反向 BUY 平仓。代码里用 m_raters[1] 取倒数第二根 K 线,避免用到未闭合的当前根。 CloseByStopSignal 则更直白:先禁用了交易品种就直接返回;再逐个检查持仓,BUY 单看 Bid 是否小于等于止损价、SELL 单看 Ask 是否大于等于止损价,命中就发反向市价单删单。每次循环都 Refresh 与 RefreshRates 拉最新报价,CopyRates 只取 0~3 根足够判断。 值得注意,两个函数都从列表尾部倒序遍历(i=total_elements-1 到 0),边平边删节点时不会漏掉元素。复制下面代码到你的 EA 里,把 m_symbol、m_timeframe 换成实盘参数就能验证。

MQL5 / C++
  if(m_raters[class="num">1].close>m_bollinger_buff_main[class="num">1]&&m_raters[class="num">1].open<m_bollinger_buff_main[class="num">1])
{
      for(class="type">int i=total_elements-class="num">1;i>=class="num">0;i--)
{
       if(CheckPointer(ListTableOrders)==POINTER_INVALID)class="kw">continue;
       t=ListTableOrders.GetNodeAtIndex(i);
       if(CheckPointer(t)==POINTER_INVALID)class="kw">continue;
       if(t.Type()!=ORDER_TYPE_SELL)class="kw">continue;
       m_symbol_info.Refresh();
       m_symbol_info.RefreshRates();
       rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Ask(), class="num">0.0, class="num">0.0, "SELL: close by signal");
      }
  }
  class="kw">return(rez);
}
class="type">bool cmodel_bollinger::CloseByStopSignal(class="type">void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
   CTableOrders *t;
   class="type">int total_elements;
   class="type">bool rez=false;
   total_elements=ListTableOrders.Total();
   if(total_elements==class="num">0)class="kw">return(false);
   for(class="type">int i=total_elements-class="num">1;i>=class="num">0;i--)
{
   if(CheckPointer(ListTableOrders)==POINTER_INVALID)class="kw">continue;
   t=ListTableOrders.GetNodeAtIndex(i);
   if(CheckPointer(t)==POINTER_INVALID)class="kw">continue;
   if(t.Type()!=ORDER_TYPE_SELL&&t.Type()!=ORDER_TYPE_BUY)class="kw">continue;
   m_symbol_info.Refresh();
   m_symbol_info.RefreshRates();
   CopyRates(m_symbol,m_timeframe,class="num">0,class="num">3,m_raters);
   if(m_symbol_info.Bid()<=t.StopLoss()&&t.Type()==ORDER_TYPE_BUY)
{
       rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Bid(), class="num">0.0, class="num">0.0, "BUY: close by stop");
       class="kw">continue;
   }
   if(m_symbol_info.Ask()>=t.StopLoss()&&t.Type()==ORDER_TYPE_SELL)
{
       rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
m_symbol_info.Ask(), class="num">0.0, class="num">0.0, "SELL: close by stop");
       class="kw">continue;
   }
   }
   class="kw">return(rez);
}

◍ 把两套模型塞进同一个EA跑

拿到两个交易模型后,先别急着拼装,各自做一轮参数优化更划算。优化能暴露模型在哪些品种和周期上效率最高,实操里给每个模型挑两个最优周期加三个最优工具,就能凑出 12 个独立方案(2 模型 × 3 工具 × 2 周期)。 样本里跑出来的最优组合长这样:MACD 占六席,EURUSD M30、EURUSD H3、AUDUSD H4、AUDUSD H1、GBPUSD H12、GBPUSD H6;布林带也是六席,GBPUSD M15、GBPUSD H1、EURUSD M30、EURUSD H4、USDCAD M15、USDCAD H2。优化结果难免带点过拟合痕迹,但作为组合测试的起点够用了。 下面这段加载函数就是把这 12 个模型实体化的核心。开关由四个 bool 控制,macd_best 打开时按上面图表里的参数逐个 new 模型并塞进 list_model 链表,EA 后续直接遍历链表下单。 别把优化结果当圣旨 外汇和贵金属波动受消息面扰动大,12 组合回测漂亮不代表实盘能复现,MT5 里用默认和 best 两套开关各跑一遍周线级样本,看曲线背离再决定删哪几个。

MQL5 / C++
<span class="keyword">class="type">bool</span> macd_default=true;
<span class="keyword">class="type">bool</span> macd_best=false;
<span class="keyword">class="type">bool</span> bollinger_default=false;
<span class="keyword">class="type">bool</span> bollinger_best=false;
<span class="keyword">class="type">void</span> InitModels()
{
&nbsp;&nbsp; list_model = <span class="keyword">new</span> CList;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Initialized pointer of the model list</span>
&nbsp;&nbsp; cmodel_macd *model_macd;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Create the pointer to a model MACD</span>
&nbsp;&nbsp; cmodel_bollinger *model_bollinger;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Create the pointer to a model Bollinger</span>
&nbsp;&nbsp;
<span class="comment">class=class="str">"cmt">//----------------------------------------MACD DEFAULT----------------------------------------</span>
&nbsp;&nbsp; <span class="keyword">if</span>(macd_default==true&amp;&amp;macd_best==false)
{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;model_macd = <span class="keyword">new</span> cmodel_macd; <span class="comment">class=class="str">"cmt">// Initialize the pointer by the model MACD</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Loading of the parameters was completed successfully</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(model_macd.Init(<span class="number">class="num">129475</span>, <span class="class="type">class="kw">string">"Model macd M15"</span>, <span class="predefines">_Symbol</span>, <span class="predefines">_Period</span>, <span class="number">class="num">0.0</span>, Fast_MA,Slow_MA,Signal_MA))
{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Print(Model "</span>, model_macd.Name(), <span class="class="type">class="kw">string">" with period = "</span>, model_macd.<span class="functions">Period</span>(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;     &nbsp; <span class="class="type">class="kw">string">" on symbol "</span>, model_macd.<span class="functions">Symbol</span>(), <span class="class="type">class="kw">string">" </span><span class="class="type">class="kw">string">successfully </span><span class="class="type">class="kw">string">created</span><span class="class="type">class="kw">string">"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;   list_model.Add(model_macd);<span class="comment">class=class="str">"cmt">// Загружаем модель в список моделей</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else
</span>{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// The loading of parameters was completed successfully</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Print(Model "</span>, model_macd.Name(), <span class="class="type">class="kw">string">" with period = "</span>, model_macd.<span class="functions">Period</span>(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="class="type">class="kw">string">" on symbol "</span>, model_macd.<span class="functions">Symbol</span>(), <span class="class="type">class="kw">string">" creation has failed"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//-------------------------------------------------------------------------------------------</span>
<span class="comment">class=class="str">"cmt">//----------------------------------------MACD BEST------------------------------------------</span>
&nbsp;&nbsp; <span class="keyword">if</span>(macd_best==true&amp;&amp;macd_default==false)
{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// class="num">1.1 EURUSD H30; FMA=class="num">20; SMA=class="num">24; </span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;model_macd = <span class="keyword">new</span> cmodel_macd; <span class="comment">class=class="str">"cmt">// Initialize the pointer to the model MACD</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(model_macd.Init(<span class="number">class="num">129475</span>, <span class="class="type">class="kw">string">"Model macd H30"</span>, <span class="class="type">class="kw">string">"EURUSD"</span>, <span class="keyword">PERIOD_M30</span>, <span class="number">class="num">0.0</span>, <span class="number">class="num">20</span>,<span class="number">class="num">24</span>,<span class="number">class="num">9</span>))
{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Print(Model "</span>, model_macd.Name(), <span class="class="type">class="kw">string">" with period = "</span>, model_macd.<span class="functions">Period</span>(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;       <span class="class="type">class="kw">string">" on symbol "</span>, model_macd.<span class="functions">Symbol</span>(), <span class="class="type">class="kw">string">" created successfully"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; list_model.Add(model_macd);<span class="comment">class=class="str">"cmt">// load the model into the list of models</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
{<span class="comment">class=class="str">"cmt">// Loading parameters was completed unsuccessfully</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Print(Model "</span>, model_macd.Name(), <span class="class="type">class="kw">string">" with period = "</span>, model_macd.<span class="functions">Period</span>(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="class="type">class="kw">string">" on symbol "</span>, model_macd.<span class="functions">Symbol</span>(), <span class="class="type">class="kw">string">" creation has failed"</span>);

多品种多周期 MACD 模型批量挂载

在 EA 初始化阶段,可以用指针循环把不同品种、不同周期的自定义 MACD 模型塞进同一个链表,方便后续统一扫描。下面这段实装了 EURUSD H3、AUDUSD H1、AUDUSD H4 三组参数,快线/慢线周期分别取 8/12、10/18、14/15,信号平滑统一为 9。 Init 第一个参数 129475 是魔法号,用于区分本 EA 的订单与图表对象;第二参是模型名,第三四是品种与周期枚举,后三参即 FMA、SMA、Signal。若返回 true 就 Print 成功并 Add 进 list_model,失败则打印创建失败——MT5 终端的 Experts 标签能直接看到这些日志。 外汇与贵金属杠杆品种波动剧烈,多模型同跑会放大滑点与重绘风险,参数仅代表一种观察口径,实际信号触发概率需你自己回测验证。

MQL5 / C++
   class=class="str">"cmt">// class="num">1.2 EURUSD H3; FMA=class="num">8; SMA=class="num">12; 
   model_macd = new cmodel_macd; class=class="str">"cmt">// Initialize the pointer by the model MACD
   if(model_macd.Init(class="num">129475, "Model macd H3", "EURUSD", PERIOD_H3, class="num">0.0, class="num">8,class="num">12,class="num">9))
{
     Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
      " on symbol ", model_macd.Symbol(), " successfully created");
     list_model.Add(model_macd);class=class="str">"cmt">// Load the model into the list of models
   }
   else
{class=class="str">"cmt">// Loading of parameters was unsuccessful
     Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
     " on symbol ", model_macd.Symbol(), " creation has failed");
   }
   class=class="str">"cmt">// class="num">1.3 AUDUSD H1; FMA=class="num">10; SMA=class="num">18; 
   model_macd = new cmodel_macd; class=class="str">"cmt">// Initialize the pointer by the model MACD
   if(model_macd.Init(class="num">129475, "Model macd M15", "AUDUSD", PERIOD_H1, class="num">0.0, class="num">10,class="num">18,class="num">9))
{
     Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
       " on symbol ", model_macd.Symbol(), " successfully created");
     list_model.Add(model_macd);class=class="str">"cmt">// Load the model into the list of models
   }
   else
{class=class="str">"cmt">// The loading of parameters was unsuccessful
     Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
       " on symbol ", model_macd.Symbol(), " creation has failed");
   }
   class=class="str">"cmt">// class="num">1.4 AUDUSD H4; FMA=class="num">14; SMA=class="num">15; 
   model_macd = new cmodel_macd; class=class="str">"cmt">// Initialize the pointer by the model MACD
   if(model_macd.Init(class="num">129475, "Model macd H4", "AUDUSD", PERIOD_H4, class="num">0.0, class="num">14,class="num">15,class="num">9))
{
     Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
     " on symbol ", model_macd.Symbol(), " successfully created");
     list_model.Add(model_macd);class=class="str">"cmt">// Load the model into the list of models
   }
   else{class=class="str">"cmt">// Loading of parameters was unsuccessful
     Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),

「多周期 MACD 模型与布林带的批量初始化」

在 EA 初始化阶段,常需要把同一品种不同周期的 MACD 参数组合一次性挂进模型列表。下面这段逻辑就演示了 GBPUSD 在 H6 与 H12 上各建一个 cmodel_macd 实例,并塞进 list_model 容器。 H6 模型传入 FastMA=20、SlowMA=33、Signal=9,H12 模型则用 FastMA=12、SlowMA=30、Signal=9;两者 magic number 都写死为 129475。Init 返回 true 才 Add 进列表,否则 Print 报错,避免空指针后续炸裂。 布林带部分用开关分流:bollinger_default 为真时按当前品种和 period_bollinger / dev_bollinger 建默认模型,magic 1829374,ATR 周期 k_ATR 参与计算;bollinger_best 则走另一组优选参数(如 EURUSD M30,period=15,deviation=2.75)。外汇与贵金属杠杆高,这类多模型同跑会放大滑点与重算开销,实盘前务必在 MT5 策略测试器单周期验证。 把代码直接贴进你的 EA 的 OnInit 段,改 _Symbol 为具体品种,就能看到终端打印出『successfully created』确认模型加载。

MQL5 / C++
model_macd = new cmodel_macd;
if(model_macd.Init(class="num">129475, "Model macd H6", "GBPUSD", PERIOD_H6, class="num">0.0, class="num">20,class="num">33,class="num">9))
{
  Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
        " on symbol ", model_macd.Symbol(), " successfully created");
  list_model.Add(model_macd);
}
else
{
  Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
        " on symbol ", model_macd.Symbol(), " creation has failed");
}
model_macd = new cmodel_macd;
if(model_macd.Init(class="num">129475, "Model macd H6", "GBPUSD", PERIOD_H12, class="num">0.0, class="num">12,class="num">30,class="num">9))
{
  Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
        " on symbol ", model_macd.Symbol(), " successfully created");
  list_model.Add(model_macd);
}
else
{
  Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(),
        " on symbol ", model_macd.Symbol(), " creation has failed");
}
if(bollinger_default==true&&bollinger_best==false)
{
  model_bollinger = new cmodel_bollinger;
  if(model_bollinger.Init(class="num">1829374,"Bollinger",_Symbol,PERIOD_CURRENT,class="num">0,
  period_bollinger,dev_bollinger,class="num">0,class="num">14,k_ATR))
  {
    Print("Model ", model_bollinger.Name(), " successfully created");
    list_model.Add(model_bollinger);
  }
}
if(bollinger_best==true&&bollinger_default==false)
{
  class=class="str">"cmt">//class="num">2.1 Symbol: EURUSD M30; period: class="num">15; deviation: class="num">2,class="num">75; k_ATR=class="num">2,class="num">75;
}

◍ 多周期布林实例的初始化参数

在 MT5 里批量挂布林模型时,同一类名 cmodel_bollinger 可以通过不同 Init 参数横向铺开多个品种与周期。下面这段实跑代码一次性建了 EURUSD 30分钟、EURUSD 4小时、GBPUSD 15分钟、GBPUSD 1小时、USDCAD 15分钟五组,偏差从 2.0 到 2.75 不等,ATR 乘数 k_ATR 从 2.0 拉到 3.75。 Init 首参 1829374 是魔法号,第二参 "Bollinger" 作模型名,第三参写交易品种,第四参定周期如 PERIOD_M30。后面紧跟的整型是布林均线周期:15、30、18、27,再后面浮点是偏差 deviation,最后两个浮点分别是偏移量 0 与 k_ATR 值。 外汇与贵金属杠杆高、滑点跳空频繁,这类多模型同跑若参数过窄,可能在震荡里反复触发。建议先开 MT5 策略测试器用这组数字回一遍,看样本内命中率再决定是否上实盘。

MQL5 / C++
model_bollinger = new cmodel_bollinger;
if(model_bollinger.Init(class="num">1829374,"Bollinger","EURUSD",PERIOD_M30,class="num">0,class="num">15,class="num">2.75,class="num">0,class="num">14,class="num">2.75))
{
   Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
        ". Symbol: ", model_bollinger.Symbol(), " successfully created");
   list_model.Add(model_bollinger);
}
class=class="str">"cmt">//class="num">2.2 Symbol: EURUSD H4; period: class="num">30; deviation: class="num">2.0; k_ATR=class="num">2.25;
model_bollinger = new cmodel_bollinger;
if(model_bollinger.Init(class="num">1829374,"Bollinger","EURUSD",PERIOD_H4,class="num">0,class="num">30,class="num">2.00,class="num">0,class="num">14,class="num">2.25))
{
   Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
   ". Symbol: ", model_bollinger.Symbol(), " successfully created");
   list_model.Add(model_bollinger);
}
class=class="str">"cmt">//class="num">2.3 Symbol: GBPUSD M15; period: class="num">18; deviation: class="num">2.25; k_ATR=class="num">3.0;
model_bollinger = new cmodel_bollinger;
if(model_bollinger.Init(class="num">1829374,"Bollinger","GBPUSD",PERIOD_M15,class="num">0,class="num">18,class="num">2.25,class="num">0,class="num">14,class="num">3.00))
{
   Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
   ". Symbol: ", model_bollinger.Symbol(), " successfully created");
   list_model.Add(model_bollinger);
}
class=class="str">"cmt">//class="num">2.4 Symbol: GBPUSD H1; period: class="num">27; deviation: class="num">2.25; k_ATR=class="num">3.75;
model_bollinger = new cmodel_bollinger;
if(model_bollinger.Init(class="num">1829374,"Bollinger","GBPUSD",PERIOD_H1,class="num">0,class="num">27,class="num">2.25,class="num">0,class="num">14,class="num">3.75))
{
   Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
   ". Symbol: ", model_bollinger.Symbol(), " successfully created");
   list_model.Add(model_bollinger);
}
class=class="str">"cmt">//class="num">2.5 Symbol: USDCAD M15; period: class="num">18; deviation: class="num">2.5; k_ATR=class="num">2.00;
model_bollinger = new cmodel_bollinger;
if(model_bollinger.Init(class="num">1829374,"Bollinger","USDCAD",PERIOD_M15,class="num">0,class="num">18,class="num">2.50,class="num">0,class="num">14,class="num">2.00))
{

往模型池里塞第二组布林带实例

上面这段是继续向 list_model 里添加第二个布林带模型实例,和前一组 EURUSD 的区别只在品种与参数。 注释里写明了第二组配置:USDCAD、H2 周期、布林周期 21、标准差偏离 2.50、ATR 周期 14、k_ATR 乘数 3.25。这组在 MetaTrader 5 里对应的是 H2 图表,外汇品种波动特性与 EURUSD 不同,参数照搬前一组未必合适。 Init 调用传入的魔法号是 1829374,名字字符串为 "Bollinger",和第一组区分开便于日志排查。若返回真,就打印创建成功并把对象指针加进 list_model;外汇和贵金属杠杆高,多模型同跑要先在策略测试器里验证内存与信号冲突。 下面把关键几行拆开看:

MQL5 / C++
  class=class="str">"cmt">//class="num">2.6 Symbol: USDCAD M15; period: class="num">21; deviation: class="num">2.5; k_ATR=class="num">3.25;
  model_bollinger = new cmodel_bollinger;
  if(model_bollinger.Init(class="num">1829374,"Bollinger","USDCAD",PERIOD_H2,class="num">0,class="num">21,class="num">2.50,class="num">0,class="num">14,class="num">3.25))
  {
    Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
    ". Symbol: ", model_bollinger.Symbol(), " successfully created");
    list_model.Add(model_bollinger);
  }

「两种资金曲线放大路径的实操分野」

把回测里那张漂亮的结果图真正变成可执行仓位,靠的是资本化函数而非模型本身。原文把标准做法拆成两条线:固定比例法(Fixed Fractional)和 Ryan Jones 法,核心差异在于要不要硬性保护性止损。 固定比例法的本质是给每笔交易设一个账户百分比亏损上限。保守口径常用 2%:以 10,000 美元仓位计,单笔触止损后亏损不得超 200 美元。但风险敞口和终值并非线性——它是一条钟形曲线,总盈利随每笔风险提升先涨后跌,拐点就是所谓「最优 f」。这套逻辑要求你明确知道保护性止损位和能承受的账户比例。 Ryan Jones 公式不依赖固定保护性止损。原文里 MACD 模型较落后、本身没护止损,就直接用 Jones 法 capitalization;布林带模型有止损,走固定比例法。落地时两个方法都吃同一个 m_delta 变量:固定比例下它=每笔风险百分比,Jones 下它=抬升一级仓位需赚到的 delta 增量资金。 示例里所有模型套了相同最大风险值与 delta,但实盘完全可逐模型调参。外汇与贵金属杠杆高、滑点跳空频繁,钟形曲线拐点会随波动 regime 偏移,任何 f 值都只是概率偏好而非保本承诺,开 MT5 把 m_delta 从 0.02 改到 0.05 跑一遍才知自己曲线长什么样。

◍ 用挂单兜住机器人掉线风险

前文那套交易模型本身没用挂单,但挂单在实盘里有个硬价值:当跑 EA 的终端崩溃或断网时,已经挂在服务器端的 Buy Stop / Sell Limit 依旧生效,保护性止损或按预定价格入场的动作不会因本地设备故障而丢失。外汇与贵金属杠杆高,这类中断可能造成无保护敞口,用挂单相当于把部分风控移出本地。 要把挂单接进模型,得走 CTableOrders 的重载 Add(COrderInfo & order_info, double stop_loss, double take_profit),同时把成员变量 m_type 设成具体挂单类型,例如 ORDER_TYPE_BUY_STOP 或 ORDER_TYPE_SELL_LIMIT。难点在于捕捉「挂单被触发」或「相关性到期」的瞬间——只监听 Trade 事件不够,还得搞清楚触发源。 现在的做法是在历史订单里反查:用 HistoryOrderGetTicket() 按号比对,若历史订单号与挂单表中的某条相同,且状态是 ORDER_STATE_PARTIAL 或 ORDER_STATE_FILLED,就将该挂单在表中标记为已执行;若它没绑模拟止损/获利的挂单,则把真实订单号写进 TicketSL / TicketTP,并以 m_sl、m_tp 里已知价格补发模拟防护单。 下面这段 ReplaceDelayedOrders 是上述逻辑的核心入口,注意它先拦掉 SYMBOL_TRADE_MODE_DISABLED 的市场,再倒序扫订单表。 别把历史反查当银弹 历史比对能补 Trade 事件语义不足的坑,但 HistoryOrdersTotal() 返回的是当前历史池总数,若经纪商历史深度受限或订单已归档,可能出现比对遗漏,建议你在 MT5 里用 Print 打出 ticket 对照验证。

MQL5 / C++
class="type">bool CModel::ReplaceDelayedOrders(class="type">void)
{
  if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)class="kw">return(false);
  CTableOrders *t;
  class="type">int total_elements;
  class="type">int history_orders=HistoryOrdersTotal();
  class="type">ulong ticket;
  class="type">bool rez=false;
  class="type">long request;
  total_elements=ListTableOrders.Total();
  class="type">int try=class="num">0;
  if(total_elements==class="num">0)class="kw">return(false);
  class=class="str">"cmt">// View every order in the table
  for(class="type">int i=total_elements-class="num">1;i>=class="num">0;i--)
  {
    if(CheckPointer(ListTableOrders)==POINTER_INVALID)class="kw">continue;
    t=ListTableOrders.GetNodeAtIndex(i);
    if(CheckPointer(t)==POINTER_INVALID)class="kw">continue;
    class="kw">switch(t.Type())
    {
      case ORDER_TYPE_BUY:
      case ORDER_TYPE_SELL:
        for(class="type">int b=class="num">0;i<history_orders;b++)
        {
          ticket=HistoryOrderGetTicket(b);
          class=class="str">"cmt">// If the ticket of the historical order is equal to one of the tickets
          class=class="str">"cmt">// Stop Loss or Take Profit, then the order was blocked, and it needs to be
          class=class="str">"cmt">// deleted from the table of orders
          if(ticket==t.TicketSL()||ticket==t.TicketTP())
          {
            ListTableOrders.DeleteCurrent();
          }
        }
        class=class="str">"cmt">// If the orders, imitating the Stop Loss and Take Profit are not found in the history,
        class=class="str">"cmt">// then perhaps they are not yet set. Therefore, they need to be inputted,
        class=class="str">"cmt">// using the process for pending orders below:
        class=class="str">"cmt">// the cycle  keeps going, the exit &class="macro">#x27;break&class="macro">#x27; does not exist!!!
      case ORDER_TYPE_BUY_LIMIT:
      case ORDER_TYPE_BUY_STOP:
      case ORDER_TYPE_BUY_STOP_LIMIT:
      case ORDER_TYPE_SELL_LIMIT:
      case ORDER_TYPE_SELL_STOP:
      case ORDER_TYPE_SELL_STOP_LIMIT:

挂单成交后补模拟止损单

在遍历历史订单时,若发现某张挂单的 ticket 已出现在 HistoryOrderGetTicket 返回序列中,且状态为 PARTIAL 或 FILLED,说明原挂单已触发。此时要把本地订单表中的类型从挂单改写为市价单(ORDER_TYPE_BUY / ORDER_TYPE_SELL),否则后续逻辑会误判持仓状态。 补单逻辑只在 t.StopLoss()!=0.0 且 t.TicketSL()==0 时执行,避免重复挂出模拟止损。以 BUY 成交为例,代码用 SellStop 挂出等价止损,注释里写明最多尝试 3 次,直到返回码是 PLACED 或 DONE 才记录成交 ticket。 外汇与贵金属杠杆高,这种自管模拟止盈止损的方式可能在某些跳空行情中失效,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍,观察 3 次重试是否足够覆盖点差扩大时段。

MQL5 / C++
for(class="type">int b=class="num">0;i<history_orders;b++)
{
   ticket=HistoryOrderGetTicket(b);
   class=class="str">"cmt">// If the ticket of the historical order is equal to the ticket of the pending order
   class=class="str">"cmt">// then the pending order has worked and needs to be put out
   class=class="str">"cmt">// the pending orders, imitating the work of Stop Loss and Take Profit.
   class=class="str">"cmt">// It is also necessary to change the pending status of the order in the table
   class=class="str">"cmt">// of orders for the executed(ORDER_TYPE_BUY или ORDER_TYPE_SELL)
   m_order_info.InfoInteger(ORDER_STATE,request);
   if(t.Ticket()==ticket&&
      (request==ORDER_STATE_PARTIAL||request==ORDER_STATE_FILLED))
   {
      class=class="str">"cmt">// Change the status order in the table of orders:
      m_order_info.InfoInteger(ORDER_TYPE,request);
      if(t.Type()!=request)t.Type(request);
      class=class="str">"cmt">//------------------------------------------------------------------
      class=class="str">"cmt">// Put out the pending orders, imitating Stop Loss an Take Profit:
      class=class="str">"cmt">// The level of pending orders, imitating Stop Loss and Take Profit
      class=class="str">"cmt">// should be determined earlier. It is also necessary to make sure that
      class=class="str">"cmt">// the current order is not already linked with the pending order, imitating Stop Loss
      class=class="str">"cmt">// and Take Profit:
      if(t.StopLoss()!=class="num">0.0&&t.TicketSL()==class="num">0)
      {
         class=class="str">"cmt">// Try to put out the pending order:
         class="kw">switch(t.Type())
         {
            case ORDER_TYPE_BUY:
               class=class="str">"cmt">// Make three attempts to put out the pending order
               for(try=class="num">0;try<class="num">3;try++)
               {
                  m_trade.SellStop(t.VolumeInitial(),t.StopLoss(),m_symbol,class="num">0.0,class="num">0.0,class="num">0,class="num">0,"take-profit for buy");
                  if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
                  {
                     t.TicketTP(m_trade.ResultDeal());

「用挂单模拟止盈的三次重试逻辑」

当持仓的 TakeProfit 不为 0 但 TicketTP 仍为 0 时,说明系统还没挂出模拟止盈的 pending order,这时进入 switch 按订单类型补挂。 对于 ORDER_TYPE_SELL 的持仓,代码用 for(try=0;try<3;try++) 循环最多 3 次,调用 m_trade.BuyStop 以原止损价挂 buy stop 来模仿卖单的止盈出场。 每次挂单后检查 ResultRetcode,若是 TRADE_RETCODE_PLACED 或 TRADE_RETCODE_DONE 就通过 t.TicketTP(m_trade.ResultDeal()) 记录成交票号并 break 退出重试。 同理 ORDER_TYPE_BUY 走的是 SellLimit 分支,参数里 volume 用 t.VolumeInitial()、挂单价用 t.StopLoss(),注释写的是 'take-profit for buy',这种反直觉命名在回测时容易看错,建议直接开 MT5 把这段贴进 EA 验证挂单方向。外汇与贵金属杠杆高,模拟止盈失败不意味实盘止损,需自行确认 broker 拒绝码。

MQL5 / C++
case ORDER_TYPE_SELL:
  class=class="str">"cmt">// Make three attempts to put up a pending order
  for(try=class="num">0;try<class="num">3;try++)
  {
    m_trade.BuyStop(t.VolumeInitial(),t.StopLoss(),m_symbol,class="num">0.0,class="num">0.0,class="num">0,class="num">0,"take-profit for buy");
    if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
    {
      t.TicketTP(m_trade.ResultDeal());
      break;
    }
  }
}
if(t.TakeProfit()!=class="num">0.0&&t.TicketTP()==class="num">0){
  class=class="str">"cmt">// Attempt to put out the pending order, imitating Take Profit:
  class="kw">switch(t.Type())
  {
    case ORDER_TYPE_BUY:
      class=class="str">"cmt">// Make three attempts to put out the pending order
      for(try=class="num">0;try<class="num">3;try++)
      {
        m_trade.SellLimit(t.VolumeInitial(),t.StopLoss(),m_symbol,class="num">0.0,class="num">0.0,class="num">0,class="num">0,"take-profit for buy");
        if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
        {
          t.TicketTP(m_trade.ResultDeal());
          break;
        }
      }

◍ 卖单止盈挂单的重试逻辑

在订单类型为 SELL 的分支里,系统对止盈挂单的下发做了三次尝试容错。具体做法是循环 try 从 0 到 2,每次调用 BuyLimit 试图挂出「take-profit for buy」限价单,成交或挂单成功就记录成交票号并跳出。 这种三次重试结构能缓解瞬时报价拒绝(如 TRADE_RETCODE_REQUOTE 类)导致的止盈挂单失败。外汇与贵金属市场滑点和拒绝率偏高,实盘里建议把重试次数和间隔在策略参数中暴露出来,方便针对不同品种调。 下面这段是 SELL 分支中止盈挂单处理的核心代码片段,注意 BuyLimit 的止损与偏差参数传了 0,意味着沿用当前品种默认设置。

MQL5 / C++
         break;
         case ORDER_TYPE_SELL:
            class=class="str">"cmt">// Make three attempts to put out the pending order
            for(try=class="num">0;try<class="num">3;try++)
{
               m_trade.BuyLimit(t.VolumeInitial(),t.StopLoss(),m_symbol,class="num">0.0,class="num">0.0,class="num">0,class="num">0,"take-profit for buy");
               if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
{
                  t.TicketTP(m_trade.ResultDeal());
                  break;
               }
            }
         }
               break;

把工具请下神坛

这套以链表为核心的数据结构思路,价值不在「全自动印钞」,而在把策略拆成互不干扰的独立机器人:同一品种上挂两个 EA,各自只动自己的订单和资金曲线,净持仓管理上的麻烦会少一大截。2010 年自动交易锦标赛里,按这种模型跑出来的系统能严格照交易计划执行,不同资金风控并行也不乱,算是一个实打实的验证点。 代码层有个坑得自己排。下面这段演示里,用索引遍历时删掉第 5 个节点后继续走索引 6,实际下一个有效节点还是原位置,社区里有人指出更稳的做法是 GetFirstNode / GetNextNode 逐步推进。 class Test : public CObject { public: int i_; Test(int i) { i_ = i; }; int get_i() { return i_; }; }; CList *list = new CList(); for (int i = 0; i < 10; i++) { Test *t = new Test(i); list.Add(t); } for (int i = 0; i < list.Total(); i++) { Test *t = list.GetNodeAtIndex(i); if (i == 5) { list.DeleteCurrent(); } if (CheckPointer(t) == POINTER_INVALID) { continue; } Print(t.get_i()); } for (Test *t = list.GetFirstNode(); t != NULL;) { t_current = t; if (t.get_i() == 5) { list.DeleteCurrent(); t = list.GetCurrentNode(); if (t == t_current) {break;} continue; } Print(t.get_i()); t = list.GetNextNode(); } 它附带的资金管理和预付款检查函数、对报价滑点耐受的下单系统,都是能直接抄进 MT5 工程里的零件。外汇和贵金属杠杆高、跳空频繁,真要上实盘前,先在策略测试器用合成点差跑几遍再谈信任——工具是死的,亏钱的人往往是把架构当圣旨的那批。

MQL5 / C++
class Test : class="kw">public CObject {
  class="kw">public:
    class="type">int i_;
    Test(class="type">int i) { i_ = i; };
    class="type">int get_i() { class="kw">return i_; };
};
CList *list = new CList();
for (class="type">int i = class="num">0; i < class="num">10; i++) {
  Test *t = new Test(i);
  list.Add(t);
}
for (class="type">int i = class="num">0; i < list.Total(); i++) {
  Test *t = list.GetNodeAtIndex(i);
  if (i == class="num">5) { list.DeleteCurrent(); }
  if (CheckPointer(t) == POINTER_INVALID) { class="kw">continue; }
  Print(t.get_i());
}
for (Test *t = list.GetFirstNode(); t != NULL;) {
  t_current = t;
  if (t.get_i() == class="num">5) {
    list.DeleteCurrent();
    t = list.GetCurrentNode();
    if (t == t_current) {break;}
    class="kw">continue;
  }
  Print(t.get_i());
  t = list.GetNextNode();
}

常见问题

非标准周期K线收盘时间不是整点,直接用标准周期时间戳比对会错位;应按当前图表周期的实际Time[0]取值,而不是假设整点闭合。
惰性更新只在跨周期切换或新K线触发时刷新,能省CPU;只要用上一根闭合K线时间做判断,就不会漏信号,反而更稳。
小布可以读取你的品种周期配置,自动标注时间戳错位和拆单死循环点,你只需确认参数即可,不用自己逐行查。
Sleep是给券商接口留的处理窗口,调小可能触发拒单或重复挂单;保持原值,用分批计数控制节奏更安全。
至少拦零值、负值周期和过采样快慢线;快线周期必须大于慢线,否则模型类直接返回初始化失败不下单。