MetaTrader 5 中的订单、持仓和成交·综合运用
📊

MetaTrader 5 中的订单、持仓和成交·综合运用

(3/3)· 多数EA在回测与实盘间数据错位,根源是没搞清订单、持仓和成交的缓存边界

偏理论 第 3/3 篇
把活动订单和历史订单混为一谈,是MQL5新手写交易函数时最常见的坑。一旦在Trade选项卡里去找已执行订单,逻辑就会在断线重连后悄悄崩掉。理清三类对象的存储位置,比多写十个指标都有用。

「从成交历史里捞出自己的订单」

在 MT5 的 EA 或脚本里遍历历史订单时,最怕把别的程序挂的单向标志(magic number)混进来。上面这段用 HistoryOrderGetInteger 按 ticket 取 ORDER_TIME_DONE、ORDER_MAGIC、ORDER_POSITION_ID 三个字段,再用 if(order_magic==my_magic) 把自家订单筛出来单独处理。 PrintFormat 那行把订单号、magic、成交时间和关联持仓 ID 打到日志,方便你回看某笔单子到底什么时候平、挂在哪个持仓下。若 HistoryOrderGetInteger 返回失败,else 分支会报出索引和 GetLastError 代码,通常错误 4301 代表历史里没这条 ticket。 实盘或复盘前,先把 my_magic 设成你 EA 专用的非零整数,否则默认 0 会和手动单撞车。外汇与贵金属杠杆高,历史校验只用于排查逻辑,不代表任何收益倾向。

MQL5 / C++
  class=class="str">"cmt">//--- 执行时间
  class="type">class="kw">datetime time_done=HistoryOrderGetInteger(order_ticket,ORDER_TIME_DONE);
  class="type">long order_magic  =HistoryOrderGetInteger(order_ticket,ORDER_MAGIC);
  class="type">long pos_ID       =HistoryOrderGetInteger(order_ticket,ORDER_POSITION_ID);
  if(order_magic==my_magic)
    {
    class=class="str">"cmt">//  处理设为 ORDER_MAGIC 的订单
    }
  PrintFormat("订单 #%d: ORDER_MAGIC=#%d, time_done %s, ORDER_POSITION_ID=%d",
               order_ticket,order_magic,TimeToString(time_done),pos_ID);
  }
  else                class=class="str">"cmt">// 从历史中获取订单没有成功
  {
  PrintFormat("我们无法根据索引 %d 选择订单. 错误 %d",
               i,GetLastError());
  }
  }
}

◍ 从缓存里捞历史成交的正确姿势

MT5 里读历史成交和读历史订单走的是同一套缓存机制。HistoryDealsTotal() 返回的数量,完全取决于你之前用 HistorySelect(start, end) 或 HistorySelectByPosition() 往内存里塞了多少条记录。只想要某一笔,直接 HistoryDealSelect(ticket) 单点载入即可,不必拖整库。 实战里更常见的是循环扫描:先用 HistorySelect 限定时间窗把记录载进缓存,再拿 HistoryDealsTotal 取总数,随后用 HistoryDealGetTicket 按索引拿单证号,最后用 HistoryDealGetDouble / GetInteger / GetString 把字段抠出来分析。重点提醒:别把全量历史无脑载进来——外汇和贵金属交易历史动辄几万条,缓存爆满会直接拖垮 EA 性能,属于典型的高风险低效写法。 下面这段演示了限定 24 小时窗口取最后一笔成交,并打印订单号与 magic: datetime end=TimeCurrent(); // 当前服务器时间 datetime start=end-PeriodSeconds(PERIOD_D1); // 起始时间设为24小时之前 HistorySelect(start,end); int deals=HistoryDealsTotal(); ulong deal_ticket=HistoryDealGetTicket(deals-1); if(deal_ticket>0) { ulong order =HistoryDealGetInteger(deal_ticket,DEAL_ORDER); long order_magic=HistoryDealGetInteger(deal_ticket,DEAL_MAGIC); long pos_ID =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID); PrintFormat("交易 #%d 订单 #%d ORDER_MAGIC=%d 仓位编号",deals-1,order,order_magic,pos_ID); } 把 start 改成 end-PeriodSeconds(PERIOD_W1) 就能切到近一周,跑一遍看 Print 日志,比读文档直观。

MQL5 / C++
class=class="str">"cmt">// --- 决定请求交易历史的时间间隔
  class="type">class="kw">datetime end=TimeCurrent();                    class=class="str">"cmt">// 当前服务器时间
  class="type">class="kw">datetime start=end-PeriodSeconds(PERIOD_D1);  class=class="str">"cmt">// 把起始时间设为24小时之前
class=class="str">"cmt">//--- 在程序缓存中根据所需时间间隔请求交易历史
  HistorySelect(start,end);
class=class="str">"cmt">//--- 获得历史中的交易总数
  class="type">int deals=HistoryDealsTotal();
class=class="str">"cmt">//--- 获取列表中最后一个索引的交易单号
  class="type">class="kw">ulong deal_ticket=HistoryDealGetTicket(deals-class="num">1);
  if(deal_ticket>class="num">0) class=class="str">"cmt">// 我们在缓存中得到交易了,处理它
   {
   class=class="str">"cmt">//--- 所达成交易的订单编号
     class="type">class="kw">ulong order      =HistoryDealGetInteger(deal_ticket,DEAL_ORDER);
     class="type">long order_magic=HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
     class="type">long pos_ID      =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
     PrintFormat("交易 #%d 订单 #%d ORDER_MAGIC=%d  仓位编号",
                 deals-class="num">1,order,order_magic,pos_ID);
   }
  else              class=class="str">"cmt">//尝试获取交易没有成功
   {
     PrintFormat("总之,%d 个交易历史中, 我们无法根据"+
                 "索引 %d 选择交易. 错误 %d",deals,deals-class="num">1,GetLastError());
   }
class="kw">input class="type">long my_magic=class="num">111;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">// --- 决定请求交易历史的时间间隔
   class="type">class="kw">datetime end=TimeCurrent();                    class=class="str">"cmt">// 当前服务器时间
   class="type">class="kw">datetime start=end-PeriodSeconds(PERIOD_D1);  class=class="str">"cmt">// 起始时间设为24小时之前
class=class="str">"cmt">//--- 在程序缓存中请求交易历史所需的时间间隔
   HistorySelect(start,end);
class=class="str">"cmt">//--- 获得历史中的交易数量
   class="type">int deals=HistoryDealsTotal();
   class="type">int returns=class="num">0;
   class="type">class="kw">double profit=class="num">0;
   class="type">class="kw">double loss=class="num">0;
class=class="str">"cmt">//--- 扫描历史中的所有交易
   for(class="type">int i=class="num">0;i<deals;i++)
    {
    class=class="str">"cmt">//--- 根据其在列表中的索引获取交易单号
      class="type">class="kw">ulong deal_ticket=HistoryDealGetTicket(i);
      if(deal_ticket>class="num">0) class=class="str">"cmt">// 在缓存中获取交易,处理它
        {

从成交历史里扒出属于自己的盈亏账

在 MT5 的回测或实盘后处理里,想单独统计某个 EA 的战绩,核心是先用 HistoryDealGetTicket 按索引拿到成交票号,再用 HistoryDealGetString / HistoryDealGetInteger / HistoryDealGetDouble 把字段抠出来。下面这段逻辑只认 order_magic 等于自定义 my_magic 的成交,避免把手动单或其它 EA 的单子混进来。 DEAL_ENTRY 字段决定这笔是开仓还是平仓:只有当 entry_type == DEAL_ENTRY_OUT 时才算一次完整平仓闭环,此时 returns 自增、并用 DEAL_PROFIT 的正负分别累进 profit 和 loss。这样跑完循环,PrintFormat 会直接吐出类似「财务结果中共有 37 个交易. 利润=1240.50 , 亏损= -832.20」的汇总,数字随账户历史变动。 别把正态当圣经:profit 和 loss 是带符号累加,loss 本身是负数,做收益率分母时要取绝对值,否则回撤算出来会虚低。外汇与贵金属杠杆高,这套统计只还原已平仓事实,不代表未来盈亏倾向。

MQL5 / C++
class="type">class="kw">string symbol = HistoryDealGetString(deal_ticket,DEAL_SYMBOL);
class="type">class="kw">datetime time = HistoryDealGetInteger(deal_ticket,DEAL_TIME);
class="type">class="kw">ulong order = HistoryDealGetInteger(deal_ticket,DEAL_ORDER);
class="type">long order_magic = HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
class="type">long pos_ID = HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
ENUM_DEAL_ENTRY entry_type=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket,DEAL_ENTRY);
class=class="str">"cmt">//--- 根据指定的 DEAL_MAGIC 处理交易
if(order_magic==my_magic)
  {
   class=class="str">"cmt">//... 所需行为
  }
class=class="str">"cmt">//--- 根据交易的结果计算利润和亏损
if(entry_type==DEAL_ENTRY_OUT)
  {
   class=class="str">"cmt">//--- 增加交易数 
   returns++;
   class=class="str">"cmt">//--- 交易结果
   class="type">class="kw">double result=HistoryDealGetDouble(deal_ticket,DEAL_PROFIT);
   class=class="str">"cmt">//--- 把正的结果输入到累计利润中
   if(result>class="num">0) profit+=result;
   class=class="str">"cmt">//--- 把负的结果输入到累计亏损中
   if(result<class="num">0) loss+=result;
  }
}
else class=class="str">"cmt">// 未能成功尝试获取交易
  {
   PrintFormat("我们无法根据索引 %d 选择交易. 错误 %d",
                i,GetLastError());
  }
  }
class=class="str">"cmt">//--- 输出计算的结果
PrintFormat("财务结果中共有 %d 个交易. 利润=%.2f , 亏损= %.2f",
             returns,profit,loss);
}

「用持仓标识符反查整段交易历史」

HistorySelectByPosition(position_ID) 和 HistorySelect(start, end) 都会把成交与订单填进历史缓存,但前者只认一个条件:记录必须带指定的 POSITION_IDENTIFIER。这个标识符是系统给每个新建仓位的唯一数字,仓位活着的时候恒定不变,即便持仓方向从 POSITION_TYPE_BUY 翻成 POSITION_TYPE_SELL,它也不会变。 未平持仓本质是该品种上一笔或多笔成交叠出来的。所以每笔成交、以及成交背后的订单,都会被打上所属持仓的标识符。只要拿到当前持仓的 ID,就能把改动过它的订单和成交全挖出来,重拼出这条仓位的完整生命周期。 这个函数省掉了自己写循环扫全量历史的麻烦。典型三步:先取对当前持仓的标识符;调 HistorySelectByPosition() 把同 ID 的订单和成交灌进缓存;再按你的逻辑处理这批记录。外汇和贵金属杠杆高,用这套重构历史做复盘时,要留意点差和库存费对成交序列的干扰。

◍ 别急着下结论

MT5 这套交易子系统平台把接口封装得很干净,标准库里的交易类能让你不碰底层细节就写出水平不低的 EA;但真要吃透缓存和历史处理的边界,还是得回到基础函数自己跑一遍。 上面那段代码在实盘终端里跑过:加载 134502 笔历史成交、218740 张历史订单后,终端内存占用从 426 MB 跳到 789 MB,而 MQL 脚本自身只占 1 MB——历史数据才是吃内存的大头。 fxsaber 在 M1 图表、5000 根 K 线、单品种下测过,10 个同步发单 EA 能吃掉 4GB,这种负载下更该考虑 OrderSendAsync 合并或砍掉旧历史重做算法。外汇和贵金属杠杆高、滑点跳空频繁,照搬例子前先在策略测试器里压一遍再上真金。 例子文件里的 wrongworkwithhistory 和 rightworkwithhistory 两个 mq5 直接对比了错误与正确写法,下载下来 diff 一下,比看文档来得快。

MQL5 / C++
class="macro">#define PRINT(A) Print(class="macro">#A + " = " + (class="type">class="kw">string)(A))
class="type">void OnStart()
{
  PRINT(TerminalInfoInteger(TERMINAL_MEMORY_USED));
  
  if (HistorySelect(class="num">0, INT_MAX))
  {
    PRINT(HistoryDealsTotal());
    PRINT(HistoryOrdersTotal());
    
    PRINT(MQLInfoInteger(MQL_MEMORY_USED));
    PRINT(TerminalInfoInteger(TERMINAL_MEMORY_USED));
  }
}
TerminalInfoInteger(TERMINAL_MEMORY_USED) = class="num">426
HistoryDealsTotal() = class="num">134502
HistoryOrdersTotal() = class="num">218740
MQLInfoInteger(MQL_MEMORY_USED) = class="num">1
TerminalInfoInteger(TERMINAL_MEMORY_USED) = class="num">789
让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到订单与持仓的实时分离视图,把重复劳动交给小布,你专注决策。

常见问题

部分成交机制下,大单可能分几次撮合,每笔生成一个成交记录,但都挂在同一个订单ID下,遍历时需按订单号聚合。
平仓后活动持仓消失,但历史成交里仍带该标识符,可从历史缓存反查某次建仓对应的全部流水。
为省流量,本地已存成交和完结订单不重传,仅拉取断连期间新数据,换账户才整库写盘。
小布盯盘在品种页直接标出活动订单、未平持仓及历史成交分区,避免手动翻Trade和History选项卡出错。
不能,MT5每个品种任一时刻仅允许一个净持仓,同向加仓、反向则减仓或平仓。