在 MetaTrader 5 中交易的可视评估和调整·综合运用
🎞️

在 MetaTrader 5 中交易的可视评估和调整·综合运用

(3/3)·多 EA 混跑账户跑了一阵后,怎样一次性回看所有品种开平仓并试改止损策略

含代码示例实战向 第 3/3 篇
很多人账户里 EA 和手动单混着跑,想复盘只靠 Alt+E 报告,根本看不出止损价位合不合理。真要试“如果把止损挪到保本会怎样”,手动重算不现实。把历史成交导进测试器重放,才是看清全貌的办法。

「实盘加载时顺手把回测参数也备好」

这段初始化逻辑只在非回测环境下跑:用 MQLInfoInteger(MQL_TESTER) 判断当前是不是策略测试器,若不是才去准备历史成交文件并提示回测起点。

MQL5 / C++
if(!MQLInfoInteger(MQL_TESTER))
  {
  if(!PreparesDealsHistoryFile(ExtArrayDeals))
     class="kw">return(INIT_FAILED);
  if(InpShowDataInLog)
     DealsArrayPrint(ExtArrayDeals);
  SDeal   deal=ExtArrayDeals[class="num">0];
  class="type">long    leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
  class="type">class="kw">double  start_money=deal.profit;
  class="type">class="kw">datetime first_time=deal.time;
  class="type">class="kw">string  start_time=TimeToString(deal.time, TIME_DATE);
  class="type">class="kw">string  message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage class="num">1:%I64u", start_time, start_money, leverage);
  Alert(message);
  }
class="kw">return(INIT_SUCCEEDED);
逐行看:第1行排除回测态;第4行把历史成交写进 ExtArrayDeals,失败直接 INIT_FAILED;第7行按开关打印数组;第10到16行取首笔成交的时间、账户杠杆、首笔利润,拼出“现在可以跑回测”的 Alert。 实测弹窗会类似:Interval: 2024.09.13 - current date / Initial deposit: 3000.00, leverage 1:500。外汇和贵金属杠杆高,这类自动提示只是辅助,实盘验证前请在 MT5 策略测试器里用对应品种和日期复跑。 后面定义的 CDeal 类继承 CObject,把每笔成交的 ticket、order、time、type、entry、magic、pos_id 及 volume 等存成成员变量,方便在 EA 初始化阶段就把历史成交结构化成对象数组。

MQL5 / C++
if(!MQLInfoInteger(MQL_TESTER))
  {
  class=class="str">"cmt">//--- prepare a file with all historical deals
  if(!PreparesDealsHistoryFile(ExtArrayDeals))
     class="kw">return(INIT_FAILED);
  
  class=class="str">"cmt">//--- print all deals in the journal after loading them from the file 
  if(InpShowDataInLog)
     DealsArrayPrint(ExtArrayDeals);
  
  class=class="str">"cmt">//--- get the first balance deal, create the message text and display it class="kw">using Alert
  SDeal   deal=ExtArrayDeals[class="num">0];
  class="type">long    leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
  class="type">class="kw">double  start_money=deal.profit;
  class="type">class="kw">datetime first_time=deal.time;
  class="type">class="kw">string  start_time=TimeToString(deal.time, TIME_DATE);
  class="type">class="kw">string  message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage class="num">1:%I64u", start_time, start_money, leverage);
  
  class=class="str">"cmt">//--- notify via alert of the recommended parameters of the strategy tester for starting the test
  Alert(message);
  }

class=class="str">"cmt">//--- All is successful
  class="kw">return(INIT_SUCCEEDED);
}

◍ 成交对象里的字段与赋值入口

在 MT5 自建回测或实盘统计模块时,先把每笔 deal 的关键字段摊开看,比直接调 HistoryDealGetDouble 更不容易漏算。下面这段结构体成员定义了价格、手续费、库存费、盈亏以及 SL/TP 等数值属性,外加品种名、备注、外部系统 ID 等字符串属性,还有小数位、点值、测试器内 ticket 与 position id 等辅助项。 数值类里 m_swap 是平仓时累计的库存费,m_fee 是成交后立即扣除的费用,两者在跨天持仓的贵金属策略里会明显吃掉利润,外汇品种点差之外的这部分成本也得单独拉出来看。字符串与辅助属性中的 m_digits 和 m_point 决定了后续止损止盈换算的精度,测试器字段则只在 Strategy Tester 环境有值。 公开区的 Set 方法只做一件事:把外部取到的值写进对应成员。比如 SetTicket 收 ulong 类型的成交单号,SetTimeMsc 收毫秒级时间,SetType 收 ENUM_DEAL_TYPE 枚举。你可以直接把这些片段塞进自己的 CDeal 类,开 MT5 用脚本打印十笔历史成交验证字段对齐情况。

MQL5 / C++
  class="type">class="kw">double                m_price;                class=class="str">"cmt">// Deal price
  class="type">class="kw">double                m_commission;            class=class="str">"cmt">// Deal commission
  class="type">class="kw">double                m_swap;                 class=class="str">"cmt">// Accumulated swap when closing
  class="type">class="kw">double                m_profit;               class=class="str">"cmt">// Deal financial result
  class="type">class="kw">double                m_fee;                  class=class="str">"cmt">// Fee for making a deal charged immediately after performing a deal
  class="type">class="kw">double                m_sl;                   class=class="str">"cmt">// Stop Loss level
  class="type">class="kw">double                m_tp;                   class=class="str">"cmt">// Take Profit level
class=class="str">"cmt">//--- String properties
  class="type">class="kw">string                m_symbol;               class=class="str">"cmt">// Name of the symbol for which the deal is executed
  class="type">class="kw">string                m_comment;              class=class="str">"cmt">// Deal comment
  class="type">class="kw">string                m_external_id;          class=class="str">"cmt">// Deal ID in an external trading system(on the exchange)

class=class="str">"cmt">//--- Additional properties
  class="type">int                   m_digits;               class=class="str">"cmt">// Symbol digits
  class="type">class="kw">double                m_point;                class=class="str">"cmt">// Symbol point
  class="type">ulong                 m_ticket_tester;        class=class="str">"cmt">// Position ticket in the tester
  class="type">long                  m_pos_id_tester;        class=class="str">"cmt">// Position ID in the tester

class="kw">public:
class=class="str">"cmt">//--- Set deal propertie
  class="type">void                  SetTicket(class="kw">const class="type">ulong ticket)                  { this.m_ticket=ticket;                 }
  class="type">void                  SetOrder(class="kw">const class="type">long order)                     { this.m_order=order;                   }
  class="type">void                  SetTime(class="kw">const class="type">class="kw">datetime time)                   { this.m_time=time;                     }
  class="type">void                  SetTimeMsc(class="kw">const class="type">long value)                   { this.m_time_msc=value;                }
  class="type">void                  SetType(class="kw">const ENUM_DEAL_TYPE type)             { this.m_type=type;                     }

成交对象的一键字段赋值

在 MT5 的 EA 架构里,把一笔交易抽象成对象后,最频繁的操作就是往里塞字段。下面这组 setter 覆盖了入场方向、魔术码、成交原因、关联持仓 ID、手数、价格、佣金、库存费、浮动盈亏、手续费、止损、止盈、品种与备注,共 14 个属性。 void SetEntry(const ENUM_DEAL_ENTRY entry) { this.m_entry=entry; } // 设定成交入场类型(IN、OUT、INOUT 等),写入 m_entry void SetMagic(const long magic) { this.m_magic=magic; } // 绑定 EA 的魔术码,用于区分不同策略订单,写入 m_magic void SetReason(const ENUM_DEAL_REASON reason) { this.m_reason=reason; } // 记录成交触发原因(如专家、手动、止损),写入 m_reason void SetPositionID(const long id) { this.m_pos_id=id; } // 关联持仓唯一 ID,便于回查持仓,写入 m_pos_id void SetVolume(const double volume) { this.m_volume=volume; } // 设定交易手数,写入 m_volume void SetPrice(const double price) { this.m_price=price; } // 设定成交价格,写入 m_price void SetCommission(const double commission) { this.m_commission=commission; } // 设定佣金数值,写入 m_commission void SetSwap(const double swap) { this.m_swap=swap; } // 设定库存费,写入 m_swap void SetProfit(const double profit) { this.m_profit=profit; } // 设定当前盈亏,写入 m_profit void SetFee(const double fee) { this.m_fee=fee; } // 设定额外手续费,写入 m_fee void SetSL(const double sl) { this.m_sl=sl; } // 设定止损价位,写入 m_sl void SetTP(const double tp) { this.m_tp=tp; } // 设定止盈价位,写入 m_tp void SetSymbol(const string symbol) { this.m_symbol=symbol; } // 绑定交易品种名称,写入 m_symbol void SetComment(const string comment) { this.m_comment=comment; } // 设定订单备注文本,写入 m_comment 开盘前把 SetMagic 和 SetSymbol 先配好,能避免多品种 EA 在回测里串品种。外汇与贵金属杠杆高,字段写错可能导致实盘下错单,建议直接把这段代码贴进 MT5 编译跑一遍构造测试。

MQL5 / C++
class="type">void SetEntry(class="kw">const ENUM_DEAL_ENTRY entry) { this.m_entry=entry; }
class="type">void SetMagic(class="kw">const class="type">long magic) { this.m_magic=magic; }
class="type">void SetReason(class="kw">const ENUM_DEAL_REASON reason) { this.m_reason=reason; }
class="type">void SetPositionID(class="kw">const class="type">long id) { this.m_pos_id=id; }
class="type">void SetVolume(class="kw">const class="type">class="kw">double volume) { this.m_volume=volume; }
class="type">void SetPrice(class="kw">const class="type">class="kw">double price) { this.m_price=price; }
class="type">void SetCommission(class="kw">const class="type">class="kw">double commission) { this.m_commission=commission; }
class="type">void SetSwap(class="kw">const class="type">class="kw">double swap) { this.m_swap=swap; }
class="type">void SetProfit(class="kw">const class="type">class="kw">double profit) { this.m_profit=profit; }
class="type">void SetFee(class="kw">const class="type">class="kw">double fee) { this.m_fee=fee; }
class="type">void SetSL(class="kw">const class="type">class="kw">double sl) { this.m_sl=sl; }
class="type">void SetTP(class="kw">const class="type">class="kw">double tp) { this.m_tp=tp; }
class="type">void SetSymbol(class="kw">const class="type">class="kw">string symbol) { this.m_symbol=symbol; }
class="type">void SetComment(class="kw">const class="type">class="kw">string comment) { this.m_comment=comment; }

「成交对象的属性读写接口」

在自建的成交(deal)封装类里,用一组 setter 把外部标识、测试环境票据号、测试仓位 ID 写进私有成员,便于回测与实盘区分来源。 void SetExternalID(const string ext_id) { this.m_external_id=ext_id; } void SetTicketTester(const ulong ticket) { this.m_ticket_tester=ticket; } void SetPosIDTester(const long pos_id) { this.m_pos_id_tester=pos_id; } 上面三行分别写入:外部系统ID、策略测试器里的成交票据、测试器里的持仓ID,实盘运行时这些字段通常留空。 随后是一组 const 取值器,直接返回对应私有字段,调用方无法修改:Ticket() 回 ulong 票据号,Order() 回挂单号 long,Time() 回 datetime、TimeMsc() 回毫秒级时间 long。 TypeDeal() 回 ENUM_DEAL_TYPE 区分买/卖成交,Entry() 回 ENUM_DEAL_ENTRY 标明是开仓还是平仓,Magic() 回策略魔法码 long,Reason() 回 ENUM_DEAL_REASON 说明成交触发源(如 EA 或手动)。 PositionID() 回关联持仓 ID long,Volume() 回成交手数 double——在 MT5 里用 HistoryDealGet 取到原始结构后灌进这个类,就能用统一接口跑统计。外汇与贵金属杠杆高,回测成交不代表实盘可复现,验证时建议先开 MT5 策略测试器跑一笔最小手数。

MQL5 / C++
class="type">void SetExternalID(class="kw">const class="type">class="kw">string ext_id) { this.m_external_id=ext_id; }
class="type">void SetTicketTester(class="kw">const class="type">ulong ticket) { this.m_ticket_tester=ticket; }
class="type">void SetPosIDTester(class="kw">const class="type">long pos_id) { this.m_pos_id_tester=pos_id; }

class=class="str">"cmt">//--- Return deal properties
class="type">ulong Ticket(class="type">void) class="kw">const { class="kw">return this.m_ticket; }
class="type">long Order(class="type">void) class="kw">const { class="kw">return this.m_order; }
class="type">class="kw">datetime Time(class="type">void) class="kw">const { class="kw">return this.m_time; }
class="type">long TimeMsc(class="type">void) class="kw">const { class="kw">return this.m_time_msc; }
ENUM_DEAL_TYPE TypeDeal(class="type">void) class="kw">const { class="kw">return this.m_type; }
ENUM_DEAL_ENTRY Entry(class="type">void) class="kw">const { class="kw">return this.m_entry; }
class="type">long Magic(class="type">void) class="kw">const { class="kw">return this.m_magic; }
ENUM_DEAL_REASON Reason(class="type">void) class="kw">const { class="kw">return this.m_reason; }
class="type">long PositionID(class="type">void) class="kw">const { class="kw">return this.m_pos_id; }
class="type">class="kw">double Volume(class="type">void) class="kw">const { class="kw">return this.m_volume; }

◍ 持仓对象的只读访问器该怎么写

在封装交易持仓的结构体或类时,把内部成员变量用 const 成员函数暴露出来,是避免外部误改底仓数据的常用做法。下面这组函数全部带 const 限定,调用方只能取不能改,MT5 编译期就会拦住写操作。 Price() 返回 m_price,即该持仓的成交价;Commission() 与 Swap() 分别回传 m_commission、m_swap,也就是单笔佣金与隔夜息费。Profit() 给的是浮动盈亏 m_profit,Fee() 对应 m_fee 总手续费。 SL()、TP() 拿的是止损止盈价 m_sl、m_tp;Symbol() 回品种名 m_symbol,Comment() 回注释 m_comment,ExternalID() 回外部标识 m_external_id。最后 Digits() 返回价格精度位数 m_digits,Point() 返回点值 m_point。 开 MT5 新建一个 include 头文件,把这组访问器原样贴进你的持仓类,编译后写个 Print(Deal.Price()) 就能在日志里看到具体数值,验证只读约束是否生效。外汇与贵金属杠杆高,回测里的佣金 swap 数字和实盘可能有偏差,需以券商结算为准。

MQL5 / C++
class="type">class="kw">double Price(class="type">void) class="kw">const { class="kw">return this.m_price; }
class="type">class="kw">double Commission(class="type">void) class="kw">const { class="kw">return this.m_commission; }
class="type">class="kw">double Swap(class="type">void) class="kw">const { class="kw">return this.m_swap; }
class="type">class="kw">double Profit(class="type">void) class="kw">const { class="kw">return this.m_profit; }
class="type">class="kw">double Fee(class="type">void) class="kw">const { class="kw">return this.m_fee; }
class="type">class="kw">double SL(class="type">void) class="kw">const { class="kw">return this.m_sl; }
class="type">class="kw">double TP(class="type">void) class="kw">const { class="kw">return this.m_tp; }
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; }
class="type">class="kw">string Comment(class="type">void) class="kw">const { class="kw">return this.m_comment; }
class="type">class="kw">string ExternalID(class="type">void) class="kw">const { class="kw">return this.m_external_id; }
class="type">int Digits(class="type">void) class="kw">const { class="kw">return this.m_digits; }
class="type">class="kw">double Point(class="type">void) class="kw">const { class="kw">return this.m_point; }

成交对象的排序键与回测标识

在 MT5 的成交(deal)封装类里,除了常规的业务字段,还藏了两个专供策略测试器使用的只读标识:TicketTester 返回回测环境下系统分配的模拟成交号,PosIDTester 返回对应的模拟持仓 ID。实盘里这两者和真实成交票号无关,但在用 MT5 策略测试器做历史回放时,它们是定位某笔成交属于哪个测试批次的关键。 Compare 方法决定了成交对象在集合里如何排序。它接收一个 mode 参数,按不同枚举切换比较字段:票号、订单号、成交时间(秒级与毫秒级)、成交类型都可以作为排序依据。返回值是典型的三态:-1 表示当前对象小于入参,1 表示大于,0 表示相等。 下面这段是核心实现的原始写法,注意 switch 里每个 case 都只是用三元运算符做标量比较,没有调用虚函数开销。外汇与贵金属回测本身带高杠杆与滑点偏差风险,排序逻辑写错可能让统计样本错位,验证前先在脚本里打印前 10 条成交的 TimeMsc 确认升序。

MQL5 / C++
class="type">ulong TicketTester(class="type">void) class="kw">const { class="kw">return this.m_ticket_tester; }
class="type">long PosIDTester(class="type">void) class="kw">const { class="kw">return this.m_pos_id_tester; }

class=class="str">"cmt">//--- Compare two objects by the class="kw">property specified in &class="macro">#x27;mode&class="macro">#x27;
class="kw">virtual class="type">int Compare(class="kw">const CObject *node, class="kw">const class="type">int mode=class="num">0) class="kw">const
  {
   class="kw">const CDeal *obj=node;
   class="kw">switch(mode)
     {
      case SORT_MODE_DEAL_TICKET      :  class="kw">return(this.Ticket() > obj.Ticket()      ?  class="num">1  :  this.Ticket() < obj.Ticket()      ? -class="num">1  :  class="num">0);
      case SORT_MODE_DEAL_ORDER       :  class="kw">return(this.Order() > obj.Order()        ?  class="num">1  :  this.Order() < obj.Order()        ? -class="num">1  :  class="num">0);
      case SORT_MODE_DEAL_TIME        :  class="kw">return(this.Time() > obj.Time()          ?  class="num">1  :  this.Time() < obj.Time()          ? -class="num">1  :  class="num">0);
      case SORT_MODE_DEAL_TIME_MSC    :  class="kw">return(this.TimeMsc() > obj.TimeMsc()    ?  class="num">1  :  this.TimeMsc() < obj.TimeMsc()    ? -class="num">1  :  class="num">0);
      case SORT_MODE_DEAL_TYPE        :  class="kw">return(this.TypeDeal() > obj.TypeDeal()  ?  class="num">1  :  this.TypeDeal() < obj.TypeDeal()  ? -class="num">1  :  class="num">0);

「成交记录按字段排队的比较分支」

在成交容器里做排序,最实在的办法是给每个可排字段写一条 case 分支,用三目运算直接比大小返回 1 / -1 / 0。下面这段覆盖了入场方向、魔术码、触发原因、关联持仓 ID、成交量、成交价、佣金和库存费共 8 个维度,MT5 里跑一遍就能看出某笔 deal 在序列里该往前还是往后。 比较逻辑高度同质:this 成员大于 obj 返回 1,小于返回 -1,相等返回 0。如果你只想按成交价或成交量筛,把其他 case 注释掉能少编译几百字节;外汇与贵金属交易本身高风险,这类排序只帮你整理历史成交,不预示下一根 K 线方向。 把代码贴进 EA 的 Compare 方法,改 SORT_MODE 宏就能切换排序键,回测时观察日志里 deal 顺序是否符合预期。

MQL5 / C++
case SORT_MODE_DEAL_ENTRY         : class="kw">return(this.Entry() > obj.Entry()         ?  class="num">1 : this.Entry() < obj.Entry()          ? -class="num">1 :  class="num">0);
case SORT_MODE_DEAL_MAGIC         : class="kw">return(this.Magic() > obj.Magic()         ?  class="num">1 : this.Magic() < obj.Magic()          ? -class="num">1 :  class="num">0);
case SORT_MODE_DEAL_REASON        : class="kw">return(this.Reason() > obj.Reason()        ?  class="num">1 : this.Reason() < obj.Reason()         ? -class="num">1 :  class="num">0);
case SORT_MODE_DEAL_POSITION_ID   : class="kw">return(this.PositionID() > obj.PositionID() ?  class="num">1 : this.PositionID() < obj.PositionID() ? -class="num">1 :  class="num">0);
case SORT_MODE_DEAL_VOLUME        : class="kw">return(this.Volume() > obj.Volume()        ?  class="num">1 : this.Volume() < obj.Volume()         ? -class="num">1 :  class="num">0);
case SORT_MODE_DEAL_PRICE         : class="kw">return(this.Price() > obj.Price()          ?  class="num">1 : this.Price() < obj.Price()           ? -class="num">1 :  class="num">0);
case SORT_MODE_DEAL_COMMISSION    : class="kw">return(this.Commission() > obj.Commission() ?  class="num">1 : this.Commission() < obj.Commission() ? -class="num">1 :  class="num">0);
case SORT_MODE_DEAL_SWAP          : class="kw">return(this.Swap() > obj.Swap()            ?  class="num">1 : this.Swap() < obj.Swap()             ? -class="num">1 :  class="num">0);

◍ 成交记录的多字段排序分支

在 MT5 的成交容器类里,排序逻辑通常落在 Compare 方法的 switch 分支中。下面这段把八种排序模式逐一映射成两两比较的返回值:盈利、手续费、止损、止盈、品种、注释、外部 ID、测试器订单号,各自独立成 case。 每个分支都返回 1 / -1 / 0 三态:当前对象字段大于对比对象返回 1,小于返回 -1,相等返回 0。这种写法让标准库的 ArraySort 能直接对 CDeal 数组做升序排列,无需外部比较器。 注意 SORT_MODE_DEAL_SYMBOL 与 SORT_MODE_DEAL_COMMENT 走的是字符串比较(> 运算符在 MQL5 中对 string 按字典序生效),而 SORT_MODE_DEAL_TICKET_TESTER 仅回测环境有意义。实盘调用前应在终端里开 EURUSD 的成交样本,打印排序后首末条的 Profit() 差值验证分支是否生效。外汇与贵金属杠杆交易风险较高,排序结果只用于复盘统计,不构成方向判断。

MQL5 / C++
case SORT_MODE_DEAL_PROFIT     : class="kw">return(this.Profit() > obj.Profit()     ?  class="num">1  :  this.Profit() < obj.Profit()      ? -class="num">1  :  class="num">0);
case SORT_MODE_DEAL_FEE         : class="kw">return(this.Fee() > obj.Fee()         ?  class="num">1  :  this.Fee() < obj.Fee()          ? -class="num">1  :  class="num">0);
case SORT_MODE_DEAL_SL          : class="kw">return(this.SL() > obj.SL()           ?  class="num">1  :  this.SL() < obj.SL()            ? -class="num">1  :  class="num">0);
case SORT_MODE_DEAL_TP          : class="kw">return(this.TP() > obj.TP()           ?  class="num">1  :  this.TP() < obj.TP()            ? -class="num">1  :  class="num">0);
case SORT_MODE_DEAL_SYMBOL      : class="kw">return(this.Symbol() > obj.Symbol()   ?  class="num">1  :  this.Symbol() < obj.Symbol()   ? -class="num">1  :  class="num">0);
case SORT_MODE_DEAL_COMMENT     : class="kw">return(this.Comment() > obj.Comment() ?  class="num">1  :  this.Comment() < obj.Comment() ? -class="num">1  :  class="num">0);
case SORT_MODE_DEAL_EXTERNAL_ID : class="kw">return(this.ExternalID()  >obj.ExternalID()  ?  class="num">1  :  this.ExternalID()  <obj.ExternalID()  ? -class="num">1  :  class="num">0);
case SORT_MODE_DEAL_TICKET_TESTER : class="kw">return(this.TicketTester()>obj.TicketTester()?  class="num">1  :  this.TicketTester()<obj.TicketTester() ? -class="num">1  :  class="num">0);

成交对象排序与逐品种交易类骨架

在自建成交管理结构里,比较逻辑依赖排序模式分支。当模式为 SORT_MODE_DEAL_POS_ID_TESTER 时,用测试环境下的持仓 ID 做大小判定,返回 1、-1 或 0;落到 default 则交还 WRONG_VALUE,避免未知排序键污染容器。 CDeal 的带参构造直接吃 ticket 与 symbol,并把 m_ticket_tester、m_pos_id_tester 置 0。Digits 与 Point 通过 SymbolInfoInteger / SymbolInfoDouble 现取,意味着对象绑定品种后报价精度就固定了,跨品种复用前得重新构造。 CSymbolTrade 用 m_list_deals 装该符号下的全部成交,配 m_index_next_deal、m_deals_processed 两个游标跟踪未处理与已处理条数。外部先声明 DealTmp 作属性检索临时体,GetListDeals() 直接回传列表指针,调用方可以零拷贝遍历。开 MT5 把这段塞进 EA 头文件,编译后 watch m_list_deals.Total() 就能看到某品种历史成交是否被完整捕获。

MQL5 / C++
case SORT_MODE_DEAL_POS_ID_TESTER : class="kw">return(this.PosIDTester() >obj.PosIDTester() ? class="num">1 : this.PosIDTester() <obj.PosIDTester() ? -class="num">1 : class="num">0);
 class="kw">default : class="kw">return(WRONG_VALUE);
 }
 }
class=class="str">"cmt">//--- Constructors/destructor
 CDeal(class="kw">const class="type">ulong ticket, class="kw">const class="type">class="kw">string symbol) : m_ticket(ticket), m_symbol(symbol), m_ticket_tester(class="num">0), m_pos_id_tester(class="num">0)
 { this.m_digits=(class="type">int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS); this.m_point=::SymbolInfoDouble(symbol, SYMBOL_POINT); }
 CDeal(class="type">void) {}
 ~CDeal(class="type">void) {}
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for trading by symbol |
class=class="str">"cmt">//+------------------------------------------------------------------+
CDeal DealTmp; class=class="str">"cmt">// Temporary deal object for searching by properties
class CSymbolTrade : class="kw">public CObject
 {
class="kw">private:
 class="type">int m_index_next_deal; class=class="str">"cmt">// Index of the next deal that has not yet been handled
 class="type">int m_deals_processed; class=class="str">"cmt">// Number of handled deals
class="kw">protected:
 class="type">MqlTick m_tick; class=class="str">"cmt">// Tick structure
 CArrayObj m_list_deals; class=class="str">"cmt">// List of deals carried out by symbol
 CTrade m_trade; class=class="str">"cmt">// Trading class
 class="type">class="kw">string m_symbol; class=class="str">"cmt">// Symbol name
class="kw">public:
class=class="str">"cmt">//--- Return the list of deals
 CArrayObj *GetListDeals(class="type">void) { class="kw">return(&this.m_list_deals); }

class=class="str">"cmt">//--- Set a symbol

「成交容器类的接口骨架」

在 MT5 的 EA 架构里,把逐笔成交抽象成一个 CDeal 容器类,能让你在回测或实盘里随时按索引、时间或持仓 ID 翻出某笔单子。下面这段头文件声明给出了该类对外暴露的核心方法,可直接粘进你的 .mqh 里做基类。 设置品种用 SetSymbol(const string symbol),内部只把传入值赋给 m_symbol 成员;已处理成交数由 SetNumProcessedDeals 写入、NumProcessedDeals 读出,这两个方法配合回测循环计数很有用。 AddDeal(CDeal *deal) 负责把一笔成交塞进 m_list_deals 链表;取数则有四个出口:GetDealByTime 按秒级时间、GetDealByIndex 按下标、GetDealInByPosID 按持仓 ID、GetDealCurrent 取当前指针。DealsTotal 返回链表 Total(),也就是当前累计成交笔数——这是验证你的成交统计是否漏单的第一个观察点。 Description 方法用 StringFormat 拼出"%s trade object. Total deals: %d",把品种名和总单数回显出来,方便在日志里一眼看清对象状态。Bid、Ask、Time、TimeMsc 四个未实现声明,分别预留了买价、卖价、秒级与毫秒级时间接口,实盘高频逻辑里建议优先用 TimeMsc 降低滑点误判。 开 MT5 新建一个空白 EA,把下方声明原样贴入并补全 Bid/Ask 的行情获取,编译后打印 DealsTotal 就能确认容器是否已接上你的成交流。外汇与贵金属杠杆高,任何成交统计误差都可能放大仓位风险,参数上线前务必在策略测试器跑满 3 个月 Tick 数据。

MQL5 / C++
class="type">void SetSymbol(class="kw">const class="type">class="kw">string symbol) { this.m_symbol=symbol; }

class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) returns the number of handled deals
class="type">void SetNumProcessedDeals(class="kw">const class="type">int num) { this.m_deals_processed=num; }
class="type">int NumProcessedDeals(class="type">void) class="kw">const { class="kw">return this.m_deals_processed; }

class=class="str">"cmt">//--- Add a deal to the deal array
class="type">bool AddDeal(CDeal *deal);

class=class="str">"cmt">//--- Return the deal(class="num">1) by time in seconds, (class="num">2) by index in the list,
class=class="str">"cmt">//--- (class="num">3) opening deal by position ID, (class="num">4) current deal in the list
CDeal *GetDealByTime(class="kw">const class="type">class="kw">datetime time);
CDeal *GetDealByIndex(class="kw">const class="type">int index);
CDeal *GetDealInByPosID(class="kw">const class="type">long pos_id);
CDeal *GetDealCurrent(class="type">void);

class=class="str">"cmt">//--- Return(class="num">1) the number of deals in the list, (class="num">2) the index of the current deal in the list
class="type">int DealsTotal(class="type">void) class="kw">const { class="kw">return this.m_list_deals.Total(); }
class="type">int DealCurrentIndex(class="type">void) class="kw">const { class="kw">return this.m_index_next_deal; }

class=class="str">"cmt">//--- Return(class="num">1) symbol and(class="num">2) object description
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; }
class="type">class="kw">string Description(class="type">void) class="kw">const
 {
 class="kw">return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() );
 }
class=class="str">"cmt">//--- Return the current(class="num">1) Bid and(class="num">2) Ask price, time in(class="num">3) seconds, (class="num">4) milliseconds
class="type">class="kw">double Bid(class="type">void);
class="type">class="kw">double Ask(class="type">void);
class="type">class="kw">datetime Time(class="type">void);
class="type">long TimeMsc(class="type">void);

class=class="str">"cmt">//--- Open(class="num">1) class="type">long, (class="num">2) class="type">class="kw">short position, (class="num">3) close a position by ticket

◍ 交易对象的方法与回测钩子

在封装多品种交易逻辑时,CSymbolTrade 类把下单、平仓和时间判断都收进了成员函数。Buy 和 Sell 返回 ulong 类型的订单 ticket,入参里 volume、magic、sl、tp、comment 全部用 const 限定,避免函数内部误改调用方传进来的值。 ClosePos 只吃一个 ticket 就能平掉对应持仓,比直接调 CTrade 更收敛。CheckTime 是个一行内联:把当前 Time() 和传入的 datetime 比大小,大于等于就返回 true,用来卡策略的触发时刻很顺手。 回测阶段 OnTester 会打印类似「Symbol EURUSD: Total deals: 142, number of processed deals: 138」的日志,并返回 m_deals_processed 这个 double。你在 MT5 策略测试器里跑完,看一眼处理成交数和总成交数的差,就能判断有没有单子漏在队列里没轮到。 Compare 只按 Symbol 名做字典序比较,返回 1 / -1 / 0,所以把这个类塞进 CArrayObj 这类容器时,排序依据仅仅是品种名,不关手数盈亏的事。构造函数把 m_index_next_deal 和 m_deals_processed 都初始化成 0,带 symbol 参数的重载还会顺手调 m_trade.SetMarginMode() 把保证金模式设好。

MQL5 / C++
class="type">ulong Buy(class="kw">const class="type">class="kw">double volume, class="kw">const class="type">ulong magic, class="kw">const class="type">class="kw">double sl, class="kw">const class="type">class="kw">double tp, class="kw">const class="type">class="kw">string comment);
class="type">ulong Sell(class="kw">const class="type">class="kw">double volume, class="kw">const class="type">ulong magic, class="kw">const class="type">class="kw">double sl, class="kw">const class="type">class="kw">double tp, class="kw">const class="type">class="kw">string comment);
class="type">bool ClosePos(class="kw">const class="type">ulong ticket);
class=class="str">"cmt">//--- Return the result of comparing the current time with the specified one
class="type">bool CheckTime(class="kw">const class="type">class="kw">datetime time) { class="kw">return(this.Time()>=time); }
class=class="str">"cmt">//--- Sets the index of the next deal
class="type">void SetNextDealIndex(class="type">void) { this.m_index_next_deal++; }

class=class="str">"cmt">//--- OnTester handler. Returns the number of deals processed by the tester.
class="type">class="kw">double OnTester(class="type">void)
  {
  ::PrintFormat("Symbol %s: Total deals: %d, number of processed deals: %d", this.Symbol(), this.DealsTotal(), this.NumProcessedDeals());
  class="kw">return this.m_deals_processed;
  }
class=class="str">"cmt">//--- Compares two objects to each other(comparison by symbol only)
class="kw">virtual class="type">int Compare(class="kw">const CObject *node, class="kw">const class="type">int mode=class="num">0) class="kw">const
  {
  class="kw">const CSymbolTrade *obj=node;
  class="kw">return(this.Symbol()>obj.Symbol() ? class="num">1 : this.Symbol()<obj.Symbol() ? -class="num">1 : class="num">0);
  }
class=class="str">"cmt">//--- Constructors/destructor
CSymbolTrade(class="type">void) : m_index_next_deal(class="num">0), m_deals_processed(class="num">0) {}
CSymbolTrade(class="kw">const class="type">class="kw">string symbol) : m_symbol(symbol), m_index_next_deal(class="num">0), m_deals_processed(class="num">0)
  {
  this.m_trade.SetMarginMode();

按品种封装成交记录与查询

把单个交易品种的成交管理收进 CSymbolTrade 类,能避免 EA 在多品种跑的时候把不同标的的 deal 搅成一锅粥。构造函数里除了记 symbol,还顺手调了 SetMarginMode 和 SetTypeFillingBySymbol,保证后续下单的保证金计算与成交模式跟品种属性对齐。 AddDeal 方法先按 ticket 排序查重,已存在就直接返回 true,不重复插;再切到毫秒时间排序做 InsertSort 插入。插入失败会打印 Failed to add deal 并返回 false,正常则回 true。 查询侧给了两个常用入口:GetDealByTime 拿 datetime 秒级时间去链表里二分定位,返回对应 CDeal 指针;GetDealInByPosID 则遍历全表,按 position id 匹配且过滤 DEAL_ENTRY_IN,专门抓开仓那笔成交。下面这段是原文核心实现,逐行拆完就能直接抄进自己的类里用。 别在查询前忘了先 Sort m_list_deals 的 Search 依赖当前排序键,GetDealByTime 里若漏了 Sort(SORT_MODE_DEAL_TIME_MSC),用时间查会命中错误索引甚至越界,这个坑在回测切换品种时最容易爆。

MQL5 / C++
class="type">bool CSymbolTrade::AddDeal(CDeal *deal)
  {
  this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET);
  if(this.m_list_deals.Search(deal)>WRONG_VALUE)
    class="kw">return true;
  this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
  if(!this.m_list_deals.InsertSort(deal))
   {
     ::PrintFormat("%s: Failed to add deal", __FUNCTION__);
     class="kw">return false;
   }
  class="kw">return true;
  }
CDeal* CSymbolTrade::GetDealByTime(class="kw">const class="type">class="kw">datetime time)
  {
  DealTmp.SetTime(time);
  this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
  class="type">int index=this.m_list_deals.Search(&DealTmp);
  class="kw">return this.m_list_deals.At(index);
  }
CDeal *CSymbolTrade::GetDealInByPosID(class="kw">const class="type">long pos_id)
  {
  class="type">int total=this.m_list_deals.Total();
  for(class="type">int i=class="num">0; i<total; i++)
   {
     CDeal *deal=this.m_list_deals.At(i);
     if(deal==NULL || deal.PositionID()!=pos_id)
       class="kw">continue;
     if(deal.Entry()==DEAL_ENTRY_IN)
       class="kw">return deal;
   }

「从成交列表到实时报价的方法封装」

CSymbolTrade 类把成交记录和行情抓取都收敛到几个短方法里。GetDealByIndex 直接按索引从链表取 CDeal 指针,GetDealCurrent 则先以 SORT_MODE_DEAL_TIME_MSC 对成交按毫秒时间排序,再返回 m_index_next_deal 指向的那一笔,逻辑上保证了拿到的永远是时间轴上有序的当前成交。 Bid、Ask、Time、TimeMsc 四个方法都走同一套防御式写法:先 ResetLastError 清错误码,再调 SymbolInfoTick 刷新 m_tick,失败就打印函数名加错误号并返回 0,成功才吐出对应字段。这种写法在 MT5 实盘里很实用——若返回 0,基本可判定是品种不可交易或报价中断,而不是价格真为 0。 外汇与贵金属报价受流动性影响,SymbolInfoTick 偶发失败概率不低,封装里把错误暴露出来比静默返回更利于排查。开 MT5 把这段塞进自己的 EA 框架,跑一晚就能看到失败日志集中在哪些时段。

MQL5 / C++
CDeal *CSymbolTrade::GetDealByIndex(class="kw">const class="type">int index)
  {
   class="kw">return this.m_list_deals.At(index);
  }
CDeal *CSymbolTrade::GetDealCurrent(class="type">void)
  {
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   class="kw">return this.GetDealByIndex(this.m_index_next_deal);
  }
class="type">class="kw">double CSymbolTrade::Bid(class="type">void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      class="kw">return class="num">0;
     }
   class="kw">return this.m_tick.bid;
  }
class="type">class="kw">double CSymbolTrade::Ask(class="type">void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      class="kw">return class="num">0;
     }
   class="kw">return this.m_tick.ask;
  }
class="type">class="kw">datetime CSymbolTrade::Time(class="type">void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      class="kw">return class="num">0;
     }
   class="kw">return this.m_tick.time;
  }
class="type">long CSymbolTrade::TimeMsc(class="type">void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {

◍ 封装买卖与平仓的轻量接口

把交易指令收口到 CSymbolTrade 类里,能少写很多重复代码。Buy 和 Sell 都先绑定 magic 号,再调底层 m_trade 的下单方法,成交失败直接返回 0,成功则返回订单号。 平仓更简单,ClosePos 只接一个 ticket,内部转调 PositionClose。实盘里外汇和贵金属波动快,这类封装让你在 EA 里切换品种时只改 m_symbol,不用重写整套下单逻辑。 下面对这段实现做逐行拆解: ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); —— 打印函数名与错误码,定位 tick 获取失败。 return 0; —— 取不到 tick 时间就退出,返回 0 表示无效。 return this.m_tick.time_msc; —— 正常则返回最新 tick 的毫秒时间。 ulong CSymbolTrade::Buy(...) —— 定义开多函数,参数含 volume、magic、sl、tp、comment。 this.m_trade.SetExpertMagicNumber(magic); —— 给交易对象设魔术码,便于多策略共存。 if(!this.m_trade.Buy(...)) return 0; —— 市价开多失败返回 0。 return this.m_trade.ResultOrder(); —— 成功回传订单 ticket。 Sell 逻辑对称,仅改成交方向为卖。 bool CSymbolTrade::ClosePos(const ulong ticket) —— 平仓接口,return this.m_trade.PositionClose(ticket); 一行搞定。

MQL5 / C++
::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
   class="kw">return class="num">0;
   }
   class="kw">return this.m_tick.time_msc;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSymbolTrade::Open a class="type">long position                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CSymbolTrade::Buy(class="kw">const class="type">class="kw">double volume, class="kw">const class="type">ulong magic, class="kw">const class="type">class="kw">double sl, class="kw">const class="type">class="kw">double tp, class="kw">const class="type">class="kw">string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Buy(volume, this.m_symbol, class="num">0, sl, tp, comment))
     {
      class="kw">return class="num">0;
     }
   class="kw">return this.m_trade.ResultOrder();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSymbolTrade::Open a class="type">class="kw">short position                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CSymbolTrade::Sell(class="kw">const class="type">class="kw">double volume, class="kw">const class="type">ulong magic, class="kw">const class="type">class="kw">double sl, class="kw">const class="type">class="kw">double tp, class="kw">const class="type">class="kw">string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Sell(volume, this.m_symbol, class="num">0, sl, tp, comment))
     {
      class="kw">return class="num">0;
     }
   class="kw">return this.m_trade.ResultOrder();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSymbolTrade::Close position by ticket                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSymbolTrade::ClosePos(class="kw">const class="type">ulong ticket)
  {
   class="kw">return this.m_trade.PositionClose(ticket);
  }

用历史成交在测试器里复刻真实交易

把历史成交文件读进 EA 后,核心动作是构造一个按品种归类的交易对象列表。每笔成交先查所属品种是否已有对象,没有就新建并挂进列表,有则直接取指针,再把该笔成交塞进对象的成交子列表。这样循环跑完,就得到「品种→成交组」的树形结构,方便后续在测试器逐笔重放。 重放逻辑最坑的一点是:测试器的即时报价时间(tick time)和真实成交时间经常对不上,哪怕差几毫秒也会让「先取报价再找成交」的写法直接失效。实测中,基于同服务器真实 tick 回测时,这种写法会导致交易亏损。正确思路是反过来的——以成交时间为锚,等到 tick 时间 ≥ 成交时间才处理这笔成交,其余时间只等待。 成交在文件里已按毫秒时间排好序。算法很简单:取第一笔成交时间,等测试器 tick 到达该时刻,处理它并标记已处理,再把索引移到下一笔,循环到列表末尾。成交是否被测试器真正执行,看其「测试器内部仓位票根」属性是否为非零,为零就是还没跑。 原始实现里所有仓位都不带止损止盈,文件里的平仓成交由测试器自行处理,不关心真实是止损还是止盈触发的。编译后 EA 会在共享文件夹生成 HistoryDealsData.bin(路径类似 C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files\TradingByHistoryDeals),并弹窗提示测试器设置。 按指定日期、存款和杠杆跑全品种全魔幻数测试,整个历史交易重放下来账户亏损 550 美元。外汇与贵金属属高风险品种,回测亏损不预示实盘走向,但足以提示止损参数可能需要重调。 下面这段是 EA 全局声明与成交对象创建函数的骨架,注意 CSymbolTrade 临时对象和 ExtListSymbols 列表的配合:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- input parameters
input    class="type">class="kw">string  InpTestedSymbol  = "";      class=class="str">"cmt">/* The symbol being tested in the tester        */
input    class="type">long    InpTestedMagic   = -class="num">1;      class=class="str">"cmt">/* The magic number being tested in the tester  */
sinput   class="type">bool    InpShowDataInLog = false;   class=class="str">"cmt">/* Show collected data in the log               */
class=class="str">"cmt">//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a deal object from the structure                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
CDeal *CreateDeal(SDeal &deal_str)
  {
class=class="str">"cmt">//--- If failed to create an object, inform of the error in the journal and class="kw">return NULL
   CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol());
   if(deal==NULL)
     {
      PrintFormat("%s: Error. Failed to create deal object");
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- fill in the deal properties from the structure fields
   deal.SetOrder(deal_str.order);                 class=class="str">"cmt">// Order the deal was based on
   deal.SetPositionID(deal_str.pos_id);           class=class="str">"cmt">// Position ID
   deal.SetTimeMsc(deal_str.time_msc);            class=class="str">"cmt">// Time in milliseconds
   deal.SetTime(deal_str.time);                   class=class="str">"cmt">// Time

「逐笔成交对象的字段填充与品种归集」

在 MQL5 里把一笔成交记录写进自定义结构,得把交易所关心的字段逐个 Set 进去。下面这段把 volume、price、profit、commission、swap、fee、sl、tp、type、entry、reason、magic、comment、external_id 全部映射到 CSymbolTrade 类实例,最后返回对象指针,少一个字段后续统计持仓成本就会偏。 deal.SetVolume(deal_str.volume); // 成交量 deal.SetPrice(deal_str.price); // 成交价 deal.SetProfit(deal_str.profit); // 浮动/已实现利润 deal.SetCommission(deal_str.commission); // 手续费 deal.SetSwap(deal_str.swap); // 平仓时累计隔夜息 deal.SetFee(deal_str.fee); // 成交后立即扣除的费 deal.SetSL(deal_str.sl); // 止损位 deal.SetTP(deal_str.tp); // 止盈位 deal.SetType(deal_str.type); // 成交类型 deal.SetEntry(deal_str.entry); // 开平改仓方式 deal.SetReason(deal_str.reason); // 成交触发源 deal.SetMagic(deal_str.magic); // EA 识别码 deal.SetComment(deal_str.Comment()); // 备注 deal.SetExternalID(deal_str.ExternalID()); // 外部系统成交号 return deal; 把散落成交按品种归类的函数 CreateListSymbolTrades 先判空:array_deals.Size() 为 0 直接 PrintFormat 报错并返回 false,避免空循环污染统计。 循环里只处理 DEAL_TYPE_BUY 与 DEAL_TYPE_SELL,其余类型 continue 跳过;用 deal_str.Symbol() 取品种名,丢进已排序的 list_symbols 做 Search,命中 index 非 WRONG_VALUE 才累加,没命中就新建 CSymbolTrade 再插入。外汇与贵金属杠杆高,swap 与 commission 字段若没写准,多品种回测的净值曲线可能失真。

MQL5 / C++
  deal.SetVolume(deal_str.volume);                  class=class="str">"cmt">// Volume
  deal.SetPrice(deal_str.price);                  class=class="str">"cmt">// Price
  deal.SetProfit(deal_str.profit);                class=class="str">"cmt">// Profit
  deal.SetCommission(deal_str.commission);        class=class="str">"cmt">// Deal commission
  deal.SetSwap(deal_str.swap);                    class=class="str">"cmt">// Accumulated swap when closing
  deal.SetFee(deal_str.fee);                      class=class="str">"cmt">// Fee for making a deal charged immediately after performing a deal
  deal.SetSL(deal_str.sl);                        class=class="str">"cmt">// Stop Loss level
  deal.SetTP(deal_str.tp);                        class=class="str">"cmt">// Take Profit level
  deal.SetType(deal_str.type);                    class=class="str">"cmt">// Type
  deal.SetEntry(deal_str.entry);                  class=class="str">"cmt">// Position change method
  deal.SetReason(deal_str.reason);                class=class="str">"cmt">// Deal execution reason or source
  deal.SetMagic(deal_str.magic);                  class=class="str">"cmt">// EA ID
  deal.SetComment(deal_str.Comment());            class=class="str">"cmt">// Deal comment
  deal.SetExternalID(deal_str.ExternalID());      class=class="str">"cmt">// Deal ID in an external trading system(on the exchange)
class=class="str">"cmt">//--- Return the pointer to a created object
  class="kw">return deal;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create an array of used symbols                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
  class="type">bool res=true;                                 class=class="str">"cmt">// result
  class="type">int total=(class="type">int)array_deals.Size();             class=class="str">"cmt">// total number of deals in the array

class=class="str">"cmt">//--- if the deal array is empty, class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(total==class="num">0)
     {
       PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
       class="kw">return false;
     }

class=class="str">"cmt">//--- in a loop through the deal array
  CSymbolTrade *SymbolTrade=NULL;
  for(class="type">int i=class="num">0; i<total; i++)
     {
       class=class="str">"cmt">//--- get the next deal and, if it is neither buy nor sell, move on to the next one
       SDeal deal_str=array_deals[i];
       if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
          class="kw">continue;

       class=class="str">"cmt">//--- find a trading object in the list whose symbol is equal to the deal symbol
       class="type">class="kw">string symbol=deal_str.Symbol();
       SymbTradeTmp.SetSymbol(symbol);
       list_symbols.Sort();
       class="type">int index=list_symbols.Search(&SymbTradeTmp);

       class=class="str">"cmt">//--- if the index of the desired object in the list is -class="num">1, there is no such object in the list
       if(index==WRONG_VALUE)
         {
           class=class="str">"cmt">//--- we create a new trading symbol object and, if creation fails,

◍ 把成交记录按品种拆进交易对象

在批量构建品种交易对象时,先尝试用 new 创建 CSymbolTrade。若返回 NULL,说明对象没建起来,直接把结果位 res 置 false 并 continue 跳到下一笔,不阻塞整个循环。 如果 list_symbols.Add() 失败,说明对象进不了列表,此时必须 delete 掉刚 new 出来的 SymbolTrade 释放内存,同样 res &= false 后跳过。这一步漏写 delete 会在长周期回测里缓慢吃内存。 若对象已存在于列表中,就通过 At(index) 按索引取回指针;取回为 NULL 也直接 continue,不往下跑。接着用 GetDealByTime() 判断这笔成交是否已在对象的成交列表里,避免重复添加。 下面这段是核心拼接逻辑,逐行拆一下: //--- 新建品种交易对象,失败则结果置false并跳过 SymbolTrade=new CSymbolTrade(symbol); if(SymbolTrade==NULL) { res &=false; continue; } //--- 加入列表失败则删对象、置false、跳过 if(!list_symbols.Add(SymbolTrade)) { delete SymbolTrade; res &=false; continue; } //--- 已存在则按索引取回 else { SymbolTrade=list_symbols.At(index); if(SymbolTrade==NULL) continue; } //--- 该成交不在对象列表中才创建并添加 if(SymbolTrade.GetDealByTime(deal_str.time)==NULL) { CDeal *deal=CreateDeal(deal_str); if(deal==NULL) { res &=false; continue; } res &=SymbolTrade.AddDeal(deal); } return res; 日志打印函数 SymbolsArrayPrint() 会遍历列表输出每个品种的描述与成交数。实测一段历史成交跑下来,AUDUSD 对象装了 218 笔、EURJPY 装了 116 笔,说明按品种聚合是可行的。 开 MT5 把这段接进你的回测脚本,重点看 AUDUSD 在 200 笔以上时内存曲线是否平稳,外汇和贵金属品种波动大、成交密集,高杠杆下风险显著,聚合逻辑若漏掉 delete 可能让终端卡死。

MQL5 / C++
SymbolTrade=new CSymbolTrade(symbol);
if(SymbolTrade==NULL)
  {
   res &=false;
   class="kw">continue;
  }
if(!list_symbols.Add(SymbolTrade))
  {
   class="kw">delete SymbolTrade;
   res &=false;
   class="kw">continue;
  }
else
  {
   SymbolTrade=list_symbols.At(index);
   if(SymbolTrade==NULL)
     class="kw">continue;
  }
if(SymbolTrade.GetDealByTime(deal_str.time)==NULL)
  {
   CDeal *deal=CreateDeal(deal_str);
   if(deal==NULL)
     {
      res &=false;
      class="kw">continue;
     }
   res &=SymbolTrade.AddDeal(deal);
  }
class="kw">return res;

多品种成交分布与回放函数拆解

在一段历史成交回放样本里,EURUSD 累计成交 524 笔,GBPUSD 352 笔,NZDUSD 178 笔,USDCAD 仅 22 笔,USDCHF 250 笔,USDJPY 142 笔,XAUUSD 118 笔。外汇与贵金属均属高杠杆品种,样本量差异本身就意味着不同品种的策略容量和滑点暴露不在同一量级,USDCAD 流动性稀薄时复制参数要格外谨慎。 DealDescription 函数负责把单笔成交对象格式化成可读字符串。非平衡类成交会拼出序号、成交票号、开平方向、成交类型、持仓 ID、品种、magic 值、带小数位的价格与止损止盈;平衡类成交则只输出票号、方向、类型、盈亏金额和账户币种、时间,跳过持仓相关字段。 OnInit 里先判断是否在回测环境:若不在测试器,就调用 PreparesDealsHistoryFile 把历史成交落盘,失败直接 INIT_FAILED;随后按 InpShowDataInLog 开关把数组打印到日志,并取 ExtArrayDeals[0] 的首笔平衡成交准备弹窗。想验证的话,把这段逻辑塞进 EA 在 MT5 非回测环境跑一次,看日志里 XAUUSD 那 118 笔是否按上述格式完整展开。

MQL5 / C++
class="type">class="kw">string DealDescription(CDeal *deal, class="kw">const class="type">int index)
  {
   class="type">class="kw">string indexs=StringFormat("% 5d", index);
   if(deal.TypeDeal()!=DEAL_TYPE_BALANCE)
      class="kw">return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s(magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                           indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                           deal.PositionID(), deal.Symbol(), deal.Magic(), deal.Digits(), deal.Price(),
                           TimeToString(deal.Time(), TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.Digits(), deal.SL(), deal.Digits(), deal.TP()));
   else
      class="kw">return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                           indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                           deal.Profit(), AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.Time())));
  }

class="type">int OnInit()
  {
class=class="str">"cmt">//--- If the EA is not running in the tester
   if(!MQLInfoInteger(MQL_TESTER))
     {
      class=class="str">"cmt">//--- prepare a file with all historical deals
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         class="kw">return(INIT_FAILED);
      
      class=class="str">"cmt">//--- print all deals in the journal after loading them from the file 
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
      
      class=class="str">"cmt">//--- get the first balance deal, create the message text and display it class="kw">using Alert
      SDeal   deal=ExtArrayDeals[class="num">0];

「EA 初始化时如何回填历史成交并拉起测试环境」

这段逻辑跑在 OnInit 后半段,区分了实盘加载与策略测试器内加载两种场景。若 EA 在测试器外启动,会先用 Alert 弹出建议的测试区间与初始入金、杠杆,方便你直接照着开 MT5 回测。 测试器内启动时,程序先读历史成交文件到 ExtArrayDeals 数组,ArrayResize 清零后由 FileReadDealsToArray 填充,失败则 PrintFormat 报错并返回 INIT_FAILED。读成功后会在日志打印读取字节数与成交总笔数,例如 "%I64u bytes were read ... A total of %u deals were received",这是一个可核对的数据点。 随后 CreateListSymbolTrades 按成交标的建表,SymbolsArrayPrint 输出清单;再用 for 循环对每个标的调 CopyTime(Symbol, PERIOD_CURRENT, 0, 1, array),触发历史数据下载并打开对应图表。OnDeinit 里只做收尾:ExtListSymbols.Clear 与 ArrayFree(ExtArrayDeals)。外汇与贵金属品种波动剧烈、杠杆风险高,回测参数仅作概率参考,实盘前务必在 MT5 用极小仓位验证。

MQL5 / C++
class="type">long    leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
class="type">class="kw">double   start_money=deal.profit;
class="type">class="kw">datetime first_time=deal.time;
class="type">class="kw">string   start_time=TimeToString(deal.time, TIME_DATE);
class="type">class="kw">string   message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage class="num">1:%I64u", start_time, start_money, leverage);

class=class="str">"cmt">//--- notify via alert of the recommended parameters of the strategy tester for starting the test
Alert(message);
   }
class=class="str">"cmt">//--- The EA has been launched in the tester
   else
   {
   class=class="str">"cmt">//--- read data from the file into the array
   class="type">ulong file_size=class="num">0;
   ArrayResize(ExtArrayDeals, class="num">0);
   if(!FileReadDealsToArray(ExtArrayDeals, file_size))
     {
      PrintFormat("Failed to read file \"%s\". Error %d", FILE_NAME, GetLastError());
      class="kw">return(INIT_FAILED);
     }
     
   class=class="str">"cmt">//--- report the number of bytes read from the file and writing the deals array in the journal.
   PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, ExtArrayDeals.Size());
   }
   
class=class="str">"cmt">//--- Create a list of trading objects by symbols from the array of historical deals
   if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols))
   {
      Print("Errors found class="kw">while creating symbol list");
      class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- Print the created list of deals in the journal
   SymbolsArrayPrint(&ExtListSymbols);
   
class=class="str">"cmt">//--- Access each symbol to start downloading historical data
class=class="str">"cmt">//--- and opening charts of traded symbols in the strategy tester
class="type">class="kw">datetime array[];
class="type">int total=ExtListSymbols.Total();
for(class="type">int i=class="num">0; i<total; i++)
   {
   CSymbolTrade *obj=ExtListSymbols.At(i);
   if(obj==NULL)
      class="kw">continue;
   CopyTime(obj.Symbol(), PERIOD_CURRENT, class="num">0, class="num">1, array);
   }
   
class=class="str">"cmt">//--- All is successful
   class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- clear the created lists and arrays
   ExtListSymbols.Clear();
   ArrayFree(ExtArrayDeals);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 在回测环境里重演历史成交

想把历史成交记录搬到 MT5 策略测试器里重跑,第一步是拦住实盘环境。OnTick 里用 MQLInfoInteger(MQL_TESTER) 判断,非测试器直接 return,避免把文件里的老单子误发到真实账户——外汇和贵金属杠杆高,误触发可能瞬间放大亏损。 核心逻辑在 TradeByHistory 函数。它先取 ExtListSymbols.Total() 得到交易对象总数,再逐个遍历 CSymbolTrade 对象,用 GetDealCurrent 拿到当前待处理成交。这里有三道过滤:magic 和 symbol 不匹配的跳过;只留 DEAL_TYPE_BUY / DEAL_TYPE_SELL;TicketTester() 大于 0 说明测试器已处理过,也跳过。 时间窗由 obj.CheckTime(deal.Time()) 把控,没到价的成交不动作。碰到 DEAL_ENTRY_IN 的入场成交,代码直接按原 volume 和 magic 调 obj.Buy 或 obj.Sell,sl/tp 暂传 0,comment 原样带过。复制这段代码到 EA,把历史成交文件指给 InpTestedSymbol 和 InpTestedMagic,就能在测试器里看老策略在新行情下的概率表现。

MQL5 / C++
class="type">void OnTick()
  {
class=class="str">"cmt">//--- work only in the strategy tester
   if(!MQLInfoInteger(MQL_TESTER))
        class="kw">return;

class=class="str">"cmt">//---  Handle the list of deals from the file in the tester
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading by history                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TradeByHistory(class="kw">const class="type">class="kw">string symbol="", class="kw">const class="type">long magic=-class="num">1)
  {
   class="type">class="kw">datetime time=class="num">0;
   class="type">int total=ExtListSymbols.Total();   class=class="str">"cmt">// number of trading objects in the list

class=class="str">"cmt">//--- in a loop by all symbol trading objects
   for(class="type">int i=class="num">0; i<total; i++)
     {
     class=class="str">"cmt">//--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         class="kw">continue;

     class=class="str">"cmt">//--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         class="kw">continue;

     class=class="str">"cmt">//--- sort the deal by magic number and symbol
      if((magic>-class="num">1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         class="kw">continue;

     class=class="str">"cmt">//--- sort the deal by type(only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         class="kw">continue;

     class=class="str">"cmt">//--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>class="num">0)
         class="kw">continue;

     class=class="str">"cmt">//--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         class="kw">continue;
      class=class="str">"cmt">//--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         class=class="str">"cmt">//--- open a position by deal type
         class="type">class="kw">double sl=class="num">0;
         class="type">class="kw">double tp=class="num">0;
         class="type">ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) :
                                                                  type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : class="num">0);

         class=class="str">"cmt">//--- if a position is opened(we received its ticket)

回测里平仓单与实盘仓位的对齐逻辑

在 MT5 策略测试器中,市价平仓类成交(DEAL_ENTRY_OUT / INOUT / OUT_BY)需要反向定位到当初开仓的那一笔,才能正确关闭测试器内的虚拟仓位。代码里先用 GetDealInByPosID 按真实账户成交的 PositionID 找到对应的开仓 deal 对象,拿不到就直接 continue 跳过,避免空指针导致循环崩掉。 如果开仓 deal 的 TicketTester 已为 0,说明测试器侧该仓位此前已平,此时 PrintFormat 打出“position #%I64d is already closed”并顺推索引。否则调用 ClosePos(ticket_tester) 尝试按测试器票号平仓,成功才把 NumProcessedDeals 加 1 并回写 deal.SetTicketTester。 最后用 deal.TicketTester()>0 做闸门:只要这一轮成功写入了测试器票号,就 SetNextDealIndex 推进到下一笔。这样能保证真实历史成交与测试器持仓逐笔对齐,回测中成交计数偏差通常可控制在单笔级别。

MQL5 / C++
if(ticket>class="num">0)
  {
  class=class="str">"cmt">//--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
  obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
  deal.SetTicketTester(ticket);
  class=class="str">"cmt">//--- get the position ID in the tester and write it to the properties of the deal object
  class="type">long pos_id_tester=class="num">0;
  if(HistoryDealSelect(ticket))
    {
     pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
     deal.SetPosIDTester(pos_id_tester);
     }
   }
  }

class=class="str">"cmt">//--- in case of a market exit deal
if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
  {
  class=class="str">"cmt">//--- get a deal a newly opened position is based on
  CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
  if(deal_in==NULL)
    class="kw">continue;
  class=class="str">"cmt">//--- get the position ticket in the tester from the properties of the opening deal
  class=class="str">"cmt">//--- if the ticket is zero, then most likely the position in the tester is already closed
  class="type">ulong ticket_tester=deal_in.TicketTester();
  if(ticket_tester==class="num">0)
    {
    PrintFormat("Could not get position ticket, apparently position #%I64d(#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
    obj.SetNextDealIndex();
    class="kw">continue;
    }
  class=class="str">"cmt">//--- if the position is closed by ticket
  if(obj.ClosePos(ticket_tester))
    {
    class=class="str">"cmt">//--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
    obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
    deal.SetTicketTester(ticket_tester);
    }
  }
class=class="str">"cmt">//--- if a ticket is now set in the deal object, then the deal has been successfully handled -
class=class="str">"cmt">//--- set the deal index in the list to the next deal
if(deal.TicketTester()>class="num">0)
  {
  obj.SetNextDealIndex();
  }
 }

「给历史回放EA挂上统一止损止盈」

把 EA 放到 \MQL5\Experts\TradingByHistoryDeals\ 目录,和原回放脚本同层。先加一个测试模式枚举 ENUM_TESTING_MODE,区分“原始交易”和“指定止损止盈”两种跑法;再开一组 Stops parameters 输入,用两个全局变量把止损、止盈点数递给交易对象,输入面板按组归类,不会和策略参数搅在一起。 OnInit 里把用户输入的止损点数换算好写进变量。核心在算价函数:止损价不能贴近当前价 StopLevel 距离内,若品种 StopLevel 为 0,就按 2~3 个双精度点差兜底(函数形参默认乘数为 2,可调用时改)。函数返回的是合规的止损/止盈挂单价,不是裸点数。 TradeByHistory 里插一段分支:选了 SLTP 模式才写止损止盈;平仓模块仅在原始交易模式生效,SLTP 模式下平仓交给测试器自动执行,EA 只把成交标已处理接着跑。OnTester 返回测试器经手的成交总数,各品种对象也各自打印流水。 实跑对比:上一轮回放原始交易亏 550 美元,这次统一替所有持仓挂 100 点止损、500 点止盈,净值变 +590 点。外汇与贵金属属高风险,回测盈利不预示实盘,按品种微调止损大小图形可能更平。 别把 StopLevel 当摆设 StopLevel 为 0 不代表能贴价挂单,用 2 倍点差兜底只是最小安全距离,真上 MT5 记得用 SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL) 核对。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|&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;&nbsp;&nbsp; TradingByHistoryDeals_SLTP.mq5 |
class=class="str">"cmt">//|&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;&nbsp;&nbsp;Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include "SymbolTrade.mqh"
enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,      class=class="str">"cmt">/* Original trading                      */
   TESTING_MODE_SLTP,        class=class="str">"cmt">/* Specified StopLoss and TakeProfit values  */
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- input parameters
input    group                "Strategy parameters"
input    class="type">class="kw">string              InpTestedSymbol   =  "";           class=class="str">"cmt">/* The symbol being tested in the tester        */
input    class="type">long                InpTestedMagic     =  -class="num">1;           class=class="str">"cmt">/* The magic number being tested in the tester  */
sinput   class="type">bool                InpShowDataInLog   =  false;        class=class="str">"cmt">/* Show collected data in the log               */
input    group                "Stops parameters"

◍ 止损止盈与最小止损距离的硬约束

EA 里把止损止盈做成输入参数很常见,但经纪商的最小止损距离(StopLevel)经常卡住订单。上面这段把 InpStopLoss=300、InpTakeProfit=500 点作为默认,并在 OnInit 里把小于 1 的值强制归零,等于关闭对应止损或止盈。 CorrectStopLoss 函数才是实战关键:它先取 StopLevel(symbol_name, 2) 作为最小距离(默认乘 2 倍点差缓冲),再用 fmin / fmax 把用户设定的止损往价格有利方向推,确保不低于经纪商底线。买单用 BID 价减距离,卖单用 ASK 价加距离,最后 NormalizeDouble 到小数位。 外汇和贵金属杠杆高,StopLevel 在重大数据后会瞬间扩大,回测里 300 点止损能挂出,实盘可能直接报无效。开 MT5 把这段代码丢进 EA,改 spread_multiplier 看挂单拒绝率变化,比看任何说明书都直观。

MQL5 / C++
input    ENUM_TESTING_MODE InpTestingMode    = TESTING_MODE_ORIGIN; class=class="str">"cmt">/* Testing Mode                    */
input    class="type">int                InpStopLoss         = class="num">300;                class=class="str">"cmt">/* StopLoss in points              */
input    class="type">int                InpTakeProfit       = class="num">500;                class=class="str">"cmt">/* TakeProfit in points            */
class=class="str">"cmt">//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;
class="type">int            ExtStopLoss;
class="type">int            ExtTakeProfit;
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Adjust the stops
   ExtStopLoss   =(InpStopLoss<class="num">1  ? class="num">0 : InpStopLoss);
   ExtTakeProfit=(InpTakeProfit<class="num">1 ? class="num">0 : InpTakeProfit);

class=class="str">"cmt">//--- If the EA is not running in the tester
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return correct StopLoss relative to StopLevel                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CorrectStopLoss(class="kw">const class="type">class="kw">string symbol_name, class="kw">const ENUM_ORDER_TYPE order_type, class="kw">const class="type">int stop_loss, class="kw">const class="type">int spread_multiplier=class="num">2)
  {
   if(stop_loss==class="num">0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))
      class="kw">return class="num">0;
   class="type">int lv=StopLevel(symbol_name, spread_multiplier), dg=(class="type">int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
   class="type">class="kw">double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
   class="type">class="kw">double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
   class="kw">return
      (order_type==ORDER_TYPE_BUY ?
         NormalizeDouble(fmin(price-lv*pt, price-stop_loss*pt), dg) :
         NormalizeDouble(fmax(price+lv*pt, price+stop_loss*pt), dg)
      );
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return correct TakeProfit relative to StopLevel                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CorrectTakeProfit(class="kw">const class="type">class="kw">string symbol_name, class="kw">const ENUM_ORDER_TYPE order_type, class="kw">const class="type">int take_profit, class="kw">const class="type">int spread_multiplier=class="num">2)
  {
   if(take_profit==class="num">0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))

止盈价与历史成交的底层取数逻辑

这段函数把「允许挂单的最近价格」和「经纪商止损级距」拆开算,避免直接用 Bid/Ask 减固定点值被拒单。StopLevel 先读 SYMBOL_SPREAD 和 SYMBOL_TRADE_STOPS_LEVEL,若经纪商 stops level 返回 0,就退而用 spread 乘自定义倍数当最小距离,实盘里常见 EURUSD 的 stops level 为 0 而 spread 为 10 点的情况。 价格端用 fmax / fmin 夹住两层约束:买单调 take_profit 时取 price+lv*pt 与 price+take_profit*pt 的较大者,卖单取较小者,最后 NormalizeDouble 到品种小数位 dg。这样即便你传的 TP 小于经纪商强制级距,挂单价也不会破规。 TradeByHistory 则遍历 ExtListSymbols 里的交易对象,用 magic 与 symbol 双重过滤,只留 DEAL_TYPE_BUY / SELL 且未在 tester 处理过的成交。total 来自 ExtListSymbols.Total(),循环里任一层过滤不命中就 continue,等于把历史成交当成信号源前先做了一次干净切片。 别把 stops level 当永远非零 很多新手以为 SymbolInfoInteger(SYMBOL_TRADE_STOPS_LEVEL) 必有值,结果在 ECNC 账户上拿到 0 后算出的挂单距离直接小于 spread,报 4756 错。用 spread*倍数兜底不是凑数,是实盘必须。

MQL5 / C++
  class="type">int lv=StopLevel(symbol_name, spread_multiplier), dg=(class="type">int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
  class="type">class="kw">double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
  class="type">class="kw">double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
  class="kw">return
    (order_type==ORDER_TYPE_BUY ?
      NormalizeDouble(fmax(price+lv*pt, price+take_profit*pt), dg) :
      NormalizeDouble(fmin(price-lv*pt, price-take_profit*pt), dg)
    );
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return StopLevel in points                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int StopLevel(class="kw">const class="type">class="kw">string symbol_name, class="kw">const class="type">int spread_multiplier)
  {
   class="type">int spread=(class="type">int)SymbolInfoInteger(symbol_name, SYMBOL_SPREAD);
   class="type">int stop_level=(class="type">int)SymbolInfoInteger(symbol_name, SYMBOL_TRADE_STOPS_LEVEL);
   class="kw">return(stop_level==class="num">0 ? spread*spread_multiplier : stop_level);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading by history                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TradeByHistory(class="kw">const class="type">class="kw">string symbol="", class="kw">const class="type">long magic=-class="num">1)
  {
   class="type">class="kw">datetime time=class="num">0;
   class="type">int total=ExtListSymbols.Total();   class=class="str">"cmt">// number of trading objects in the list

class=class="str">"cmt">//--- in a loop by all symbol trading objects
   for(class="type">int i=class="num">0; i<total; i++)
     {
      class=class="str">"cmt">//--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         class="kw">continue;

      class=class="str">"cmt">//--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         class="kw">continue;

      class=class="str">"cmt">//--- sort the deal by magic number and symbol
      if((magic>-class="num">1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         class="kw">continue;

      class=class="str">"cmt">//--- sort the deal by type(only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         class="kw">continue;

      class=class="str">"cmt">//--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>class="num">0)
         class="kw">continue;
      

「回放引擎里的建仓与平仓分支」

在把历史成交记录灌进 MT5 策略测试器做回放时,代码先判断这笔成交的触发时间是否落在当前交易对象的可交易窗口内;若 CheckTime 返回 false 就直接 continue 跳到下一标的,避免在非允许时段伪造成交。 进入市场开仓分支后,程序读取 deal.Entry(),只有当其为 DEAL_ENTRY_IN 才继续。若回测模式设为 TESTING_MODE_SLTP,则依据原成交买卖方向(DEAL_TYPE_BUY 映射 ORDER_TYPE_BUY,反之为 ORDER_TYPE_SELL)调用 CorrectStopLoss / CorrectTakeProfit 算出 sl、tp,再按 type 调 obj.Buy 或 obj.Sell 下模拟单,返回 ticket。 ticket>0 代表测试器里仓位真开出来了:此时把已处理成交数 +1,写回 deal 的 tester ticket 与 position id(后者从 HistoryDealSelect + HistoryDealGetInteger(DEAL_POSITION_ID) 取)。外汇与贵金属回放属高风险验证,模拟结果仅代表历史样本下的可能表现,不等于实盘概率。 出场分支覆盖 DEAL_ENTRY_OUT / INOUT / OUT_BY 三类。先用 GetDealInByPosID 按原仓位 ID 反查对应的开仓成交对象,拿不到(NULL)就 continue;这一步是后面把平仓动作挂到正确测试器仓位上的前提,漏掉会导致平错单或平不出。

MQL5 / C++
class=class="str">"cmt">//--- if the deal time has not yet arrived, move to the next trading object of the next symbol
if(!obj.CheckTime(deal.Time()))
   class="kw">continue;
class=class="str">"cmt">//--- in case of a market entry deal
ENUM_DEAL_ENTRY entry=deal.Entry();
if(entry==DEAL_ENTRY_IN)
  {
   class=class="str">"cmt">//--- set the sizes of stop orders depending on the stop setting method
   class="type">class="kw">double sl=class="num">0;
   class="type">class="kw">double tp=class="num">0;
   if(InpTestingMode==TESTING_MODE_SLTP)
     {
      ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
      sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
      tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
     }
   class=class="str">"cmt">//--- open a position by deal type
   class="type">ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) :
                 type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : class="num">0);
   
   class=class="str">"cmt">//--- if a position is opened(we received its ticket)
   if(ticket>class="num">0)
     {
      class=class="str">"cmt">//--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
      obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
      deal.SetTicketTester(ticket);
      class=class="str">"cmt">//--- get the position ID in the tester and write it to the properties of the deal object
      class="type">long pos_id_tester=class="num">0;
      if(HistoryDealSelect(ticket))
        {
         pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
         deal.SetPosIDTester(pos_id_tester);
        }
     }
   }

class=class="str">"cmt">//--- in case of a market exit deal
if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
  {
   class=class="str">"cmt">//--- get a deal a newly opened position is based on
   CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
   if(deal_in==NULL)
      class="kw">continue;
   class=class="str">"cmt">//--- get the position ticket in the tester from the properties of the opening deal

◍ 回测里持仓票号为0时的跳过逻辑

在策略测试器中重演真实账户成交时,若从成交对象取到的测试器持仓票号 ticket_tester 为 0,通常意味着该持仓在测试环境里已经平掉。此时代码会打印原持仓 ID 与测试器持仓 ID,并直接令对象跳到下一笔成交、用 continue 退出本次循环,不再尝试平仓。 当运行模式为 TESTING_MODE_ORIGIN(原样重演)时,若票号有效就调用 obj.ClosePos(ticket_tester) 平掉测试器持仓;平仓成功后把已处理成交数加 1,并把测试器票号写回成交对象属性。其他模式下(如挂单算法模式)则跳过实际平仓动作,仅累加已处理成交数并写回票号。 循环末尾再判断 deal.TicketTester() 是否大于 0,若已写入有效票号说明本笔成交处理完毕,调用 obj.SetNextDealIndex() 推进列表指针。OnTester() 函数开头先用 double ret=0.0 与 int total=ExtListSymbols.Total() 取符号列表总数,作为后续统计测试器处理成交总量的基底。 开 MT5 把这段塞进你的成交重演 EA,重点看 ticket_tester==0 分支的日志输出频率;若真实历史里平仓频繁但测试器报大量‘already closed’,可能要排查持仓同步时序。外汇与贵金属品种回测存在滑点与时序错配高风险,结论仅作概率性参考。

MQL5 / C++
  class=class="str">"cmt">//--- if the ticket is zero, then most likely the position in the tester is already closed
  class="type">ulong ticket_tester=deal_in.TicketTester();
  if(ticket_tester==class="num">0)
    {
     PrintFormat("Could not get position ticket, apparently position #%I64d(#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
     obj.SetNextDealIndex();
     class="kw">continue;
    }
  class=class="str">"cmt">//--- if we reproduce the original trading history in the tester,
  if(InpTestingMode==TESTING_MODE_ORIGIN)
    {
     class=class="str">"cmt">//--- if the position is closed by ticket
     if(obj.ClosePos(ticket_tester))
       {
        class=class="str">"cmt">//--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
        obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
        deal.SetTicketTester(ticket_tester);
       }
    }
  class=class="str">"cmt">//--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped
  class=class="str">"cmt">//--- accordingly, simply increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
  else
    {
     obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
     deal.SetTicketTester(ticket_tester);
    }
   }
  class=class="str">"cmt">//--- if a ticket is now set in the deal object, then the deal has been successfully handled -
  class=class="str">"cmt">//--- set the deal index in the list to the next deal
  if(deal.TicketTester()>class="num">0)
    {
     obj.SetNextDealIndex();
    }
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tester function                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester(class="type">void)
  {
class=class="str">"cmt">//--- calculate and class="kw">return the total number of deals handled in the tester
   class="type">class="kw">double ret=class="num">0.0;
   class="type">int total=ExtListSymbols.Total();

多品种回测的成交统计与汇总出口

在 MT5 多品种 EA 的 OnTester 里,常用一段循环把每个品种对象的测试结果累加后返回。下面这段就是典型的遍历写法,跑完优化后能在日志里看到分品种成交明细。 for(int i=0; i<total; i++) { CSymbolTrade *obj=ExtListSymbols.At(i); if(obj!=NULL) ret+=obj.OnTester(); } return(ret); 逐行看:先以 i 从 0 到 total-1 遍历品种列表;At(i) 取出第 i 个 CSymbolTrade 指针;判空后调用该对象的 OnTester() 并把返回值叠进 ret;最后把汇总值交回测试器。 某次 2025.01.21 的回测里,EURUSD 总成交 524 笔、处理 518 笔,XAUUSD 成交 118 笔全部处理,USDCAD 与 USDJPY 也都是零遗漏。最终日志打出 final balance 3591.70 pips、OnTester result 1902。外汇与贵金属杠杆品种波动剧烈,这类回测结果仅反映历史样本,实盘可能明显偏离。 开 MT5 把这段塞进自己的多品种框架,重点核对 ExtListSymbols 的 total 是否等于实际挂载品种数,否则漏算会让 OnTester 返回值偏小。

MQL5 / C++
for(class="type">int i=class="num">0; i<total; i++)
  {
   CSymbolTrade *obj=ExtListSymbols.At(i);
   if(obj!=NULL)
     ret+=obj.OnTester();
  }
class="kw">return(ret);

「下一篇该玩尾随止损了」

这一轮我们用“如果…”的句式把真实成交记录喂回测试器,等于让历史自己重跑一遍。这种回放式实验有可能暴露出你原交易风格里被忽略的出场弱点,值得在 MT5 里亲手点开看。 文中四类文件已经齐活:SymbolTrade.mqh 是品种交易类库(53.86 KB),TradingByHistoryDeals.mq5 与带止损止盈版的 TradingByHistoryDeals_SLTP.mq5 分别是 55.19 KB、63.76 KB 的 EA,MQL5.zip 仅 22.3 KB,解压后直接丢进客户端 MQL5 目录就能用。 外汇与贵金属品种波动剧烈、杠杆风险高,实盘前务必先在策略测试器用历史数据验证逻辑。下一篇我们会把各种持仓尾随止损也接进这套回放框架,事情会更有看头。

把回放诊断交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到成交重放与止损合理性提示,你只管决定要不要改算法。

常见问题

图表上启动时它只收集账户全部成交存文件后空闲;测试器中启动则读取该文件并重现所有开平仓。
可以,EA 输入项支持选定测试品种与魔幻数字,便于隔离分析特定 EA 或标的的成交。
在测试器里给重放成交注入新止损逻辑再跑一遍,视觉上能直接对照平仓时间与权益曲线的变化。
小布盯盘对应品种页已内置成交重放与止损诊断,无需自己写 EA 导文件也能看到大致结论,外汇贵金属波动剧烈仍属高风险。
前篇铺垫了文件导出与测试器读档,本篇收尾才在重放基础上改条件,循序渐进避免一上来就改代码看不懂。