MetaTrader 5 中的订单、持仓和成交·进阶篇
📊

MetaTrader 5 中的订单、持仓和成交·进阶篇

(2/3)· 搞不清活动订单和历史成交的读取边界,MQL5 交易代码就会在实盘里静默出错

偏理论进阶 第 2/3 篇
很多脚本在回测跑得好好的,一上真实账户就漏单或重复处理,根子常在对订单和成交的缓存分层理解错了。把活动订单当成历史订单去循环,或反过来从历史里抓未平持仓,都会让逻辑 silently 崩掉。MT5 的 Trade 与 History 标签背后是两套不同生命周期的数据,混用必出事。

◍ 缓存不刷新,属性函数读到的就是旧账

在 MT5 里取持仓、订单、成交的属性前,必须先刷新对应缓存。MQL5 程序本地存的是上一次拉取的副本,服务端状态早变了,直接调属性函数只会拿到过时数据。 活动订单先用 OrderGetTicket() 或 OrderSelect() 把订单塞进缓存,之后 OrderGetDouble / OrderGetInteger / OrderGetString 才从缓存吐属性。持仓同理,PositionGetSymbol 或 PositionSelect 先就位,再调 PositionGet* 系列。这两类函数自身不联网,全靠缓存喂数据。 历史侧稍重:HistorySelect(start, end)、HistorySelectByPosition() 或 HistoryOrderSelect(ticket) 任一执行成功,缓存里订单数等于 HistoryOrdersTotal() 返回值。随后用 HistoryOrderGetDouble / Integer / String(ticket, prop) 按单证取字段;HistoryOrderGetTicket(index) 则按索引反查单证号,再喂给 HistoryOrderSelect 会清空缓存只留这一单。成交缓存机制一致,HistoryDealGetTicket(index) 配 HistoryDealSelect(ticket),HistoryDealsTotal() 也随缓存内容浮动。 一个易踩的坑:调 HistoryOrderGetTicket / HistoryDealGetTicket 前,若没用上述四个函数之一把历史区间铺满,索引对应的单证可能根本不在缓存里,返回空号。外汇与贵金属行情跳动快,缓存陈旧会让 EA 误判仓位,实盘前务必在策略测试器里单步验证刷新顺序。

遍历挂单缓存的正确姿势

在 MT5 里查活动订单,最稳的办法不是赌你手里有 ticket,而是先问总数再按索引扫。OrdersTotal() 返回当前挂单数量,配合 for 循环从 0 到总数-1,用 OrderGetTicket(i) 把第 i 个订单塞进缓存,这一步失败就直接读 GetLastError() 找原因。 缓存一旦有数据,OrderGetDouble / OrderGetInteger / OrderGetString 三个函数分别掏价格、时间整数和字符串字段。比如 ORDER_PRICE_OPEN 拿开仓价,ORDER_TIME_SETUP 拿挂单设置时间,ORDER_SYMBOL 拿品种名,ORDER_MAGIC 拿魔术码——靠 magic 过滤自己 EA 下的单最实用。 下面这段脚本把全部挂单打印出来,并单独判断 magic==555 的订单。外汇和贵金属挂单受点差与流动性影响,扫出来的价格只是当时缓存值,实盘成交概率随行情跳变,别把扫描结果当成交依据。 别把 ticket 当永远有效 OrderGetTicket(i) 里的 i 是列表序号,不是订单号;重启终端或订单状态变化后,同一个 i 对应的 ticket 可能变。写循环逻辑时永远用函数返回值判断,而不是缓存上一次运行的 ticket。

MQL5 / C++
class="type">bool selected=OrderSelect(ticket);
if(selected)
  {
   class="type">class="kw">double price_open=OrderGetDouble(ORDER_PRICE_OPEN);
   class="type">class="kw">datetime time_setup=OrderGetInteger(ORDER_TIME_SETUP);
   class="type">class="kw">string symbol=OrderGetString(ORDER_SYMBOL);
   PrintFormat("订单 #%d 交易品种 %s 设置时间 %s",ticket,symbol,TimeToString(time_setup));
  }
else
  {
   PrintFormat("根据单号 %d 选择订单出错. 错误 %d",ticket, GetLastError());
  }
class="kw">input class="type">long my_magic=class="num">555;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- 获取订单总数
   class="type">int orders=OrdersTotal();
class=class="str">"cmt">//--- 扫描订单列表
   for(class="type">int i=class="num">0;i<orders;i++)
    {
     ResetLastError();
     class=class="str">"cmt">//--- 把订单根据其在列表中的编号复制到缓存中
     class="type">class="kw">ulong ticket=OrderGetTicket(i);
     if(ticket!=class="num">0)class=class="str">"cmt">// 如果订单被成功复制到缓冲区,处理它
      {
       class="type">class="kw">double price_open   =OrderGetDouble(ORDER_PRICE_OPEN);
       class="type">class="kw">datetime time_setup=OrderGetInteger(ORDER_TIME_SETUP);
       class="type">class="kw">string symbol       =OrderGetString(ORDER_SYMBOL);
       class="type">long magic_number   =OrderGetInteger(ORDER_MAGIC);
       if(magic_number==my_magic)
        {
         class=class="str">"cmt">//  使用指定的 ORDER_MAGIC 处理订单
         }
       PrintFormat("订单 #%d 交易品种 %s 设置时间 %s, ORDER_MAGIC=%d",ticket,symbol,TimeToString(time_setup),magic_number);
      }
     else        class=class="str">"cmt">// 调用 OrderGetTicket() 没有成功完成
      {
       PrintFormat("从列表复制订单到缓存时出错. 错误代码: %d",GetLastError());
      }
    }
  }

「遍历未平持仓拿到具体字段」

盯盘 EA 不能只认当前图表品种,账户里跨品种持仓随时可能影响风控。单品种场景直接用 Symbol() 或 _Symbol 拿到品种名,再调 PositionSelect() 把该品种持仓塞进缓存即可;多品种就得靠索引扫全列表。 通用做法是先 PositionsTotal() 取持仓数,再用 for 循环配 PositionGetSymbol(i) 逐个索引调出品种并写入缓存,随后用 PositionGetInteger / Double / String 三类函数抓标识符、开仓价、类型、魔法码和注释等字段。 下面这段脚本演示了两种路径:前半段按图表品种精确选仓并打印,后半段用 my_magic=555 做输入,扫全列表只处理匹配魔法的仓。外汇与贵金属杠杆高,持仓字段读错可能触发误平,跑之前先在策略测试器用演示账户验证。

MQL5 / C++
class=class="str">"cmt">//--- 我们将根据EA工作的图表的交易品种来查找仓位
  class="type">class="kw">string symbol=Symbol();
class=class="str">"cmt">//--- 尝试取得仓位
  class="type">bool selected=PositionSelect(symbol);
  if(selected) class=class="str">"cmt">// 如果仓位被选择
    {
      class="type">long pos_id                =PositionGetInteger(POSITION_IDENTIFIER);
      class="type">class="kw">double price               =PositionGetDouble(POSITION_PRICE_OPEN);
      class="type">ENUM_POSITION_TYPE type=(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      class="type">long pos_magic             =PositionGetInteger(POSITION_MAGIC);
      class="type">class="kw">string comment             =PositionGetString(POSITION_COMMENT);
      PrintFormat("仓位 #%d 交易品种 %s: POSITION_MAGIC=%d, 价位=%G, 类型=%s, 注释=%s",
                  pos_id, symbol, pos_magic, price,EnumToString(type), comment);
    }
  else                        class=class="str">"cmt">// 如果选择仓位没有成功
    {
      PrintFormat("根据交易品种 %s 选择仓位没有成功. 错误",symbol,GetLastError());
    }
  }
class="macro">#class="kw">property script_show_inputs
class="kw">input class="type">long my_magic=class="num">555;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- 获取仓位总数
  class="type">int positions=PositionsTotal();
class=class="str">"cmt">//--- 扫描仓位列表
  for(class="type">int i=class="num">0;i<positions;i++)
    {
      ResetLastError();
      class=class="str">"cmt">//--- 根据仓位在列表中的编号把它复制到缓存中
      class="type">class="kw">string symbol=PositionGetSymbol(i); class=class="str">"cmt">//  获取所开启仓位对应的交易品种
      if(symbol!="") class=class="str">"cmt">// 如果把仓位复制到缓存了,处理它
        {
          class="type">long pos_id                =PositionGetInteger(POSITION_IDENTIFIER);
          class="type">class="kw">double price               =PositionGetDouble(POSITION_PRICE_OPEN);
          class="type">ENUM_POSITION_TYPE type=(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
          class="type">long pos_magic             =PositionGetInteger(POSITION_MAGIC);
          class="type">class="kw">string comment             =PositionGetString(POSITION_COMMENT);
          if(pos_magic==my_magic)
            {

◍ 用 POSITION_MAGIC 精准圈定持仓

遍历持仓时如果只用 PositionGetSymbol(i) 拿到品种名,却不去读 POSITION_MAGIC,很容易把 EA 自己和其他手动单、其他策略的单子混在一起处理。上面这段逻辑在循环里先按索引取持仓,成功后才进一步打印 magic、价格、类型与注释,失败则直接报 GetLastError() 的错误码。 实战里建议在 for 循环开头加一句 if(PositionGetInteger(POSITION_MAGIC)!=your_magic) continue;,这样只处理目标 magic 的仓位。外汇与贵金属杠杆高,误平无关持仓可能瞬间放大回撤,务必在 MT5 策略测试器里用多 EA 同账户环境验证一次。 出错分支也别忽略:PositionGetSymbol 返回 false 时,PrintFormat 打出索引和错误代码能省掉大把排查时间。把这段直接贴进 OnTick 里的持仓扫描段,跑一周模拟单看日志是否只出现你设定的 magic。

MQL5 / C++
   class=class="str">"cmt">//  使用指定的 POSITION_MAGIC 处理仓位
   }
   PrintFormat("仓位 #%d 交易品种 %s: POSITION_MAGIC=%d, 价格=%G, 类型=%s, 注释=%s",
                 pos_id,symbol,pos_magic,price,EnumToString(type),comment);
   }
   else       class=class="str">"cmt">// 调用 PositionGetSymbol() 没有成功
   {
    PrintFormat("根据索引 %d 复制仓位到缓存出错."+
                 " 错误代码: %d", i, GetLastError());
   }
  }
}

历史缓存别一把梭进内存

多数 EA 编写者习惯把 5~10 笔成交和订单量级的历史当默认场景,于是直接在缓存里全量载入交易历史再循环扫描。一旦账户跑了几个月、成交订单累积到成千上万,这种写法会让程序响应明显变慢,回测时尤其容易误以为是终端抽风。 真正稳妥的做法是先判断有没有必要碰 HistorySelect()。若没必要,绝不去拉全量缓存;即便要拉,也别从 1970 年拉到现在,EA 通常只要一天或一周的数据就够用。从结束时间往前推一个起始日期,再用 HistoryOrderGetTicket / HistoryDealGetTicket 按索引取单号,拿单号去调 HistoryOrderGetInteger 这类接口取属性,避免无差别大循环。 别把 HistorySelect 当免租内存 每次无脑请求全量历史并在循环里反复扫,CPU 和内存都会被空转吃掉。在 OnTrade() 里置一个 TradeHistoryChanged 标志,只有交易历史真变了才重载增量部分,每个成交订单只处理一次,计算机资源效率会高出一个量级。外汇与贵金属杠杆高、滑点跳空频繁,历史堆积速度可能远超预期,未经优化的历史处理代码会让实盘 EA 在高波动时段直接卡死。 下面这段演示了两种极端:前半段从 1970 拉到当前全量扫,后半段只在 TradeHistoryChanged 为真时拉最近 3 天。开 MT5 建个脚本把起止改成自己账户真实区间,能直观看到耗时差。

MQL5 / C++
class=class="str">"cmt">//---
  class="type">class="kw">datetime start=class="num">0;          class=class="str">"cmt">// 起始时间设为 class="num">1970 年
  class="type">class="kw">datetime end=TimeCurrent(); class=class="str">"cmt">// 结束时间设为当前服务器时间
class=class="str">"cmt">//--- 在程序的缓存中请求交易历史的所有信息
  HistorySelect(start,end);
class=class="str">"cmt">//--- 获取交易历史中的订单总数
  class="type">int history_orders=HistoryOrdersTotal();
class=class="str">"cmt">//--- 现在扫描全部订单
  for(class="type">int i=class="num">0;i<history_orders;i++)
    {
    class=class="str">"cmt">//  处理历史中的每一个订单
    }  

    ...
     
class=class="str">"cmt">//--- 获取历史中的交易总数
  class="type">int deals=HistoryDealsTotal();
class=class="str">"cmt">//--- 现在扫描全部交易
  for(class="type">int i=class="num">0;i<deals;i++)
    {
    class=class="str">"cmt">//  处理历史中的每一个交易
    }
class=class="str">"cmt">//--- 只有在交易历史改变过程中才被设为true的变量
  class="type">bool TradeHistoryChanged=class="kw">false;
class=class="str">"cmt">//--- 在此我们检查历史中的改变,如有必要把 TradeHistoryChanged 设为 true
class=class="str">"cmt">//... 所需代码
class=class="str">"cmt">//--- 检查交易历史是否有改变
  if(!TradeHistoryChanged) class="kw">return;
class=class="str">"cmt">//--- 如果历史改变了,那么顺理成章把它载入缓存
class=class="str">"cmt">//--- 结束时间设为当前服务器时间
  class="type">class="kw">datetime end=TimeCurrent();
class=class="str">"cmt">//--- 起始时间设为3天前
  class="type">class="kw">datetime start=end-class="num">3*PeriodSeconds(PERIOD_D1);
class=class="str">"cmt">//--- 在程序缓存中请求近3天的交易历史
  HistorySelect(start,end);
class=class="str">"cmt">//--- 获取历史缓存中的订单总数
  class="type">int history_orders=HistoryOrdersTotal();
class=class="str">"cmt">//--- 扫描订单
  for(class="type">int i=class="num">0;i<history_orders;i++)
    {
      class=class="str">"cmt">//--- 获得历史订单编号
      class="type">class="kw">ulong ticket=HistoryOrderGetTicket(i);
      class=class="str">"cmt">//--- 处理订单 - 获取其幻数
      class="type">long order_magic=HistoryOrderGetInteger(ticket,ORDER_MAGIC);
      class=class="str">"cmt">// 根据单号获取其余的订单属性
      class=class="str">"cmt">// ...
    }

「从缓存里翻历史订单的正确姿势」

和活动订单不同,历史订单的数量完全取决于你用 HistorySelect()、HistorySelectByPosition() 或 HistoryOrderSelect() 往 MQL5 程序缓存里塞了多少记录。没先加载历史,后续任何读取都会落空——这是新手脚本最常踩的坑。 别图省事把全部成交历史一次性灌进缓存。实测中只取必要时间窗(比如最近 24 小时)能明显减轻终端负担;盲目全量加载会让 EA 或脚本响应变慢,尤其在跑了几年的老账户上。 标准流程就五步:用 HistorySelect(start,end) 限定范围加载;HistoryOrdersTotal() 拿总数;循环里用 HistoryOrderGetTicket(i) 按索引取单号;再用 HistoryOrderGetInteger / Double / String 把字段抠出来;最后按业务判断。下面这段脚本就是抓昨天最后一个订单并打印其 magic、状态与关联持仓 ID。 [CODE] 标记内的例子先用 TimeCurrent()-PeriodSeconds(PERIOD_D1) 框出一天,再取 history_orders-1 的票据。若返回 0 会走 else 分支报 GetLastError(),这种防御写法在真实账户排查时很实用。

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">//--- 请求程序缓存中1天的交易历史
  HistorySelect(start,end);
class=class="str">"cmt">//--- 接收历史中的订单总数
  class="type">int history_orders=HistoryOrdersTotal();
class=class="str">"cmt">//--- 根据历史中列表最后的索引获取订单编号
  class="type">class="kw">ulong order_ticket=HistoryOrderGetTicket(history_orders-class="num">1);
  if(order_ticket>class="num">0) class=class="str">"cmt">// 在历史订单缓存中得到了,处理它
    {
      class=class="str">"cmt">//--- 订单状态
      ENUM_ORDER_STATE state=(ENUM_ORDER_STATE)HistoryOrderGetInteger(order_ticket,ORDER_STATE);
      class="type">long order_magic        =HistoryOrderGetInteger(order_ticket,ORDER_MAGIC);
      class="type">long pos_ID             =HistoryOrderGetInteger(order_ticket,ORDER_POSITION_ID);
      PrintFormat("订单 #%d: ORDER_MAGIC=#%d, ORDER_STATE=%d, ORDER_POSITION_ID=%d",
                  order_ticket,order_magic,EnumToString(state),pos_ID);
    }
  else                class=class="str">"cmt">// 未能成功获得订单
    {
      PrintFormat("总之,在 %d 个订单历史中, 我们未能"+
                  "根据索引%d选择订单.错误%d",history_orders,history_orders-class="num">1,GetLastError());
    }
class="macro">#class="kw">property script_show_inputs
class="kw">input class="type">long my_magic=class="num">999;
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 history_orders=HistoryOrdersTotal();
class=class="str">"cmt">//--- 现在扫描所有订单
  for(class="type">int i=class="num">0;i<history_orders;i++)
    {
      class=class="str">"cmt">//--- 根据订单在列表中的编号获取单号
      class="type">class="kw">ulong order_ticket=HistoryOrderGetTicket(i);
      if(order_ticket>class="num">0) class=class="str">"cmt">//  在缓存中得到历史订单,并处理它
        {
把订单状态巡检交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到活动订单与持仓的实时边界,你只需在 MQL5 里写好缓存读取分支。

常见问题

订单未成交前只存在于 Trade 标签的活动订单缓存,执行或撤销后才移入 History;用历史函数查不到是正常分层行为。
不可行。每个交易品种在任一时刻只能有唯一未平持仓,新同向成交加仓、反向成交减仓直至平仓。
小布盯盘的品种页直接展示活动订单与未平持仓的分离视图,可对照你 MQL5 代码的缓存读取结果排查偏差。
为省流量,MT5 仅在连接时补全缺失的历史成交与已完成订单,本地硬盘已存部分不重复下载。
可能。一个订单可生成一组成交,部分成交连续发生,每笔成交都绑定原订单且不可修改。