MQL5 交易工具包(第 4 部分):开发历史管理 EX5 库·综合运用
📊

MQL5 交易工具包(第 4 部分):开发历史管理 EX5 库·综合运用

(3/3)·从生命周期到报告生成,一套库让交易历史检索与绩效评估不再卡手

偏理论进阶 第 3/3 篇
很多 MQL5 新手把订单、成交、持仓历史混为一谈,导致回测和实盘诊断对不上号。理清三者生命周期差异,才能写出不漏数据的历史管理库。本篇顺着前两部分打好的仓位与挂单基础,直接收口到历史层。

「空小节无内容可落」

本次传入的小节原文未包含任何技术正文与代码,仅存在一个空的 [/CODE] 标记,无法提取可验证数据点。 若需生成有效内容块,请补全该小节原文(含价格现象、参数或 MQL5 代码),蒸馏引擎方可按硬红线输出原创中文技术文。

◍ 用 PrintDealsHistory 把成交记录倒进日志

想快速核查某段时间的真实成交,而不是去 MT5 终端手动翻历史,可以直接写一个 export 函数让外部脚本调用。PrintDealsHistory(datetime fromDateTime, datetime toDateTime) 就是干这个的:传起止时间,它内部调 GetHistoryData 拉取 GET_DEALS_HISTORY_DATA 类型的成交,塞进 dealInfo 数组,自己不返回任何值,只往专家日志里打印。 拿到数据先判空。ArraySize(dealInfo) 得到 totalDeals,若 <=0 就 Print 一句「No deals history found」然后 return,避免空跑循环浪费 tick。有数据才继续,先打一行汇总:找到了多少笔、时间区间是什么。 随后 for 循环从 r=0 到 totalDeals-1,逐笔输出 symbol、time、ticket、positionId、order、type、entry、reason、volume、price、slPrice、tpPrice、swap、commission、profit、comment、magic。利润和费用都接 AccountInfoString(ACCOUNT_CURRENCY) 带出账户币种,复盘时一眼能看出某笔黄金或欧美单子的净贡献。 外汇与贵金属杠杆高、滑点随机,日志里的历史成交只是已发生事实,不代表后续同参数重复出现能有什么确定结果。把下面代码贴进 MT5 脚本,从外部传两个 datetime 就能跑,验证你自己的成交结构。

MQL5 / C++
class="type">void PrintDealsHistory(class="type">class="kw">datetime fromDateTime, class="type">class="kw">datetime toDateTime) class="kw">export
  {
class=class="str">"cmt">//-- Our function&class="macro">#x27;s code will go here
  }
GetHistoryData(fromDateTime, toDateTime, GET_DEALS_HISTORY_DATA);
class="type">int totalDeals = ArraySize(dealInfo);
if(totalDeals <= class="num">0)
  {
   Print("");
   Print(__FUNCTION__, ": No deals history found for the specified period.");
   class="kw">return; class=class="str">"cmt">//-- Exit the function
  }
Print("");
Print(__FUNCTION__, "-------------------------------------------------------------------------------");
Print(
   "Found a total of ", totalDeals,
   " deals executed between(", fromDateTime, ") and(", toDateTime, ")."
);
for(class="type">int r = class="num">0; r < totalDeals; r++)
  {
   Print("---------------------------------------------------------------------------------------------------");
   Print("Deal #", (r + class="num">1));
   Print("Symbol: ", dealInfo[r].symbol);
   Print("Time Executed: ", dealInfo[r].time);
   Print("Ticket: ", dealInfo[r].ticket);
   Print("Position ID: ", dealInfo[r].positionId);
   Print("Order Ticket: ", dealInfo[r].order);
   Print("Type: ", EnumToString(dealInfo[r].type));
   Print("Entry: ", EnumToString(dealInfo[r].entry));
   Print("Reason: ", EnumToString(dealInfo[r].reason));
   Print("Volume: ", dealInfo[r].volume);
   Print("Price: ", dealInfo[r].price);
   Print("SL Price: ", dealInfo[r].slPrice);
   Print("TP Price: ", dealInfo[r].tpPrice);
   Print("Swap: ", dealInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY));
   Print("Commission: ", dealInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY));
   Print("Profit: ", dealInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY));
   Print("Comment: ", dealInfo[r].comment);
   Print("Magic: ", dealInfo[r].magic);
   Print("");
  }
class="type">void PrintDealsHistory(class="type">class="kw">datetime fromDateTime, class="type">class="kw">datetime toDateTime) class="kw">export
  {
class=class="str">"cmt">//- Get and save the deals history for the specified period
   GetHistoryData(fromDateTime, toDateTime, GET_DEALS_HISTORY_DATA);
   class="type">int totalDeals = ArraySize(dealInfo);
   if(totalDeals <= class="num">0)
     {
      Print("");

把成交记录逐笔打到日志里

这段逻辑干的事很直接:先判断指定时间段内有没有成交,若 totalDeals 为 0 就打印一句「No deals history found」并 return 退出,避免后面空跑循环。 若有数据,先用分隔线和大写函数名 __FUNCTION__ 标出当前函数上下文,再汇总打印「Found a total of N deals executed between (起) and (止)」,其中 N 就是 totalDeals 的真实数值,起止时间是 fromDateTime / toDateTime 两个变量。 随后用 for(int r=0; r<totalDeals; r++) 逐笔输出。每笔 deal 的结构体 dealInfo[r] 被拆成 18 个字段打印:从 Symbol、Time Executed、Ticket、Position ID、Order Ticket,到 Type / Entry / Reason 这类枚举(用 EnumToString 转成可读字符串),再到 Volume、Price、SL、TP、Swap、Commission、Profit(后三者拼上 AccountInfoString(ACCOUNT_CURRENCY) 显示账户币种),最后是 Comment 和 Magic。 开 MT5 把这段代码塞进你自己的回测 EA 里跑一遍,专家日志会按上面格式吐出完整成交清单;外汇和贵金属杠杆高,成交滑点可能让日志里的 Price 和挂单价有偏差,核对时留个心眼。

MQL5 / C++
      Print(__FUNCTION__, ": No deals history found for the specified period.");
      class="kw">return; class=class="str">"cmt">//-- Exit the function
   }
   Print("");
   Print(__FUNCTION__, "-------------------------------------------------------------------------------");
   Print(
      "Found a total of ", totalDeals,
      " deals executed between(", fromDateTime, ") and(", toDateTime, ")."
   );
   for(class="type">int r = class="num">0; r < totalDeals; r++)
   {
      Print("---------------------------------------------------------------------------------------------------");
      Print("Deal #", (r + class="num">1));
      Print("Symbol: ", dealInfo[r].symbol);
      Print("Time Executed: ", dealInfo[r].time);
      Print("Ticket: ", dealInfo[r].ticket);
      Print("Position ID: ", dealInfo[r].positionId);
      Print("Order Ticket: ", dealInfo[r].order);
      Print("Type: ", EnumToString(dealInfo[r].type));
      Print("Entry: ", EnumToString(dealInfo[r].entry));
      Print("Reason: ", EnumToString(dealInfo[r].reason));
      Print("Volume: ", dealInfo[r].volume);
      Print("Price: ", dealInfo[r].price);
      Print("SL Price: ", dealInfo[r].slPrice);
      Print("TP Price: ", dealInfo[r].tpPrice);
      Print("Swap: ", dealInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY));
      Print("Commission: ", dealInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY));
      Print("Profit: ", dealInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY));
      Print("Comment: ", dealInfo[r].comment);
      Print("Magic: ", dealInfo[r].magic);
      Print("");
   }
}

「把历史订单塞进动态数组」

SaveOrdersData() 是库内部用的私有函数,不导出也不返回值,只干一件事:把 MT5 交易历史缓存里的订单逐条扒出来,写进 orderInfo 动态数组,供后面分析模块调用。它用 HistoryOrdersTotal() 先拿总数,为 0 就直接 Print 一条日志并 return,避免空转。 数组大小用 ArrayResize(orderInfo, totalOrdersHistory) 一次性对齐,随后 for 循环从 x = totalOrdersHistory-1 倒序跑到 0,也就是先处理最近的订单。每轮先用 HistoryOrderGetTicket(x) 取 ticket 号,失败就带错误码打印日志;成功才继续往下抓属性。 属性抓取分三型:整数类用 HistoryOrderGetInteger(如 ORDER_TYPE、ORDER_MAGIC、ORDER_STATE),字符串类用 HistoryOrderGetString(ORDER_SYMBOL、ORDER_COMMENT),双精度类用 HistoryOrderGetDouble(ORDER_VOLUME_INITIAL、ORDER_PRICE_OPEN、SL/TP)。这些字段覆盖了从挂单时间、成交状态到初始手数和止损价的完整画像。 外汇与贵金属交易历史可能含数百上千条记录,倒序写入能让分析层默认优先看到近期行为,但历史缓存深度依赖终端设置,回测与实盘读取上限可能不一致,属高风险数据环境,验证前先确认账户历史下载范围。

MQL5 / C++
class="type">void SaveOrdersData()
  {
class=class="str">"cmt">//-- Our function&class="macro">#x27;s code will go here
  }
class="type">int totalOrdersHistory = HistoryOrdersTotal();
class="type">ulong orderTicket;
if(totalOrdersHistory > class="num">0)
  {
  class=class="str">"cmt">//-- Code to process orders goes here
  }
else
  {
  Print(__FUNCTION__, ": No order history available to be processed, totalOrdersHistory = ", totalOrdersHistory);
  class="kw">return;
  }
ArrayResize(orderInfo, totalOrdersHistory);
for(class="type">int x = totalOrdersHistory - class="num">1; x >= class="num">0; x--)
  {
  ResetLastError();
  orderTicket = HistoryOrderGetTicket(x);
  if(orderTicket > class="num">0)
    {
    class=class="str">"cmt">//- Order ticket selected ok, we can now save the order properties
    orderInfo[x].ticket = orderTicket;
    orderInfo[x].timeSetup = (class="type">class="kw">datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_SETUP);
    orderInfo[x].timeDone = (class="type">class="kw">datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_DONE);
    orderInfo[x].expirationTime = (class="type">class="kw">datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_EXPIRATION);
    orderInfo[x].typeTime = (ENUM_ORDER_TYPE_TIME)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_TIME);
    orderInfo[x].magic = HistoryOrderGetInteger(orderTicket, ORDER_MAGIC);
    orderInfo[x].reason = (ENUM_ORDER_REASON)HistoryOrderGetInteger(orderTicket, ORDER_REASON);
    orderInfo[x].type = (ENUM_ORDER_TYPE)HistoryOrderGetInteger(orderTicket, ORDER_TYPE);
    orderInfo[x].state = (ENUM_ORDER_STATE)HistoryOrderGetInteger(orderTicket, ORDER_STATE);
    orderInfo[x].typeFilling = (ENUM_ORDER_TYPE_FILLING)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_FILLING);
    orderInfo[x].positionId = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_ID);
    orderInfo[x].positionById = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_BY_ID);
    orderInfo[x].symbol = HistoryOrderGetString(orderTicket, ORDER_SYMBOL);
    orderInfo[x].comment = HistoryOrderGetString(orderTicket, ORDER_COMMENT);
    orderInfo[x].volumeInitial = HistoryOrderGetDouble(orderTicket, ORDER_VOLUME_INITIAL);
    orderInfo[x].priceOpen = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_OPEN);
    orderInfo[x].priceStopLimit = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_STOPLIMIT);
    orderInfo[x].tpPrice = HistoryOrderGetDouble(orderTicket, ORDER_TP);
    orderInfo[x].slPrice = HistoryOrderGetDouble(orderTicket, ORDER_SL);
    }
  else
    {
    Print(

◍ 把历史订单一次性塞进自定义结构

EA 复盘或做交易统计时,直接反复调 HistoryOrderGetInteger 会很慢;更稳的做法是先拿到 HistoryOrdersTotal 的总数,用 ArrayResize 把结构体数组一次扩好,再倒序遍历。下面这段就是典型落地:从最新一笔往回扫,把每张 ticket 的属性搬到本地 orderInfo[]。 循环里第一句务必 ResetLastError(),否则上一次失败码会污染本次判断。HistoryOrderGetTicket(x) 返回 0 就说明该索引取不到票据,这时候 GetLastError() 能告诉你真实错误号,比如 4401(历史未加载完毕)在刚切换品种时概率不低。 票据到手后,时间类字段( setup / done / expiration )要强转 datetime,枚举类( type / state / reason )转对应的 ENUM_ 类型,字符串用 HistoryOrderGetString 取 symbol 和 comment,双精度取初始手数和挂单价。外汇与贵金属品种点差跳空频繁,ORDER_PRICE_STOPLIMIT 在普通市价单里常为 0,别拿它当成交价去算盈亏。 让小布替你跑这套 把这段代码直接贴进 MT5 脚本,在 EURUSD 的 H1 周期回看最近 200 笔历史单,打印 orderInfo[x].volumeInitial 的最大值,能快速验证你的结构体映射有没有错位。

MQL5 / C++
class="type">void SaveOrdersData()
  {
class=class="str">"cmt">//- Get the number of loaded history orders
   class="type">int totalOrdersHistory = HistoryOrdersTotal();
   class="type">ulong orderTicket;
class=class="str">"cmt">//-
class=class="str">"cmt">//- Check if we have any orders in the history to be worked on
   if(totalOrdersHistory > class="num">0)
     {
      class=class="str">"cmt">//- Resize the dynamic array that stores the history orders
      ArrayResize(orderInfo, totalOrdersHistory);
      class=class="str">"cmt">//- Let us loop through the order history and save them one by one
      for(class="type">int x = totalOrdersHistory - class="num">1; x >= class="num">0; x--)
        {
         ResetLastError();
         orderTicket = HistoryOrderGetTicket(x);
         if(orderTicket > class="num">0)
           {
            class=class="str">"cmt">//- Order ticket selected ok, we can now save the order properties
            orderInfo[x].ticket = orderTicket;
            orderInfo[x].timeSetup = (class="type">class="kw">datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_SETUP);
            orderInfo[x].timeDone = (class="type">class="kw">datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_DONE);
            orderInfo[x].expirationTime = (class="type">class="kw">datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_EXPIRATION);
            orderInfo[x].typeTime = (ENUM_ORDER_TYPE_TIME)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_TIME);
            orderInfo[x].magic = HistoryOrderGetInteger(orderTicket, ORDER_MAGIC);
            orderInfo[x].reason = (ENUM_ORDER_REASON)HistoryOrderGetInteger(orderTicket, ORDER_REASON);
            orderInfo[x].type = (ENUM_ORDER_TYPE)HistoryOrderGetInteger(orderTicket, ORDER_TYPE);
            orderInfo[x].state = (ENUM_ORDER_STATE)HistoryOrderGetInteger(orderTicket, ORDER_STATE);
            orderInfo[x].typeFilling = (ENUM_ORDER_TYPE_FILLING)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_FILLING);
            orderInfo[x].positionId = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_ID);
            orderInfo[x].positionById = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_BY_ID);
            orderInfo[x].symbol = HistoryOrderGetString(orderTicket, ORDER_SYMBOL);
            orderInfo[x].comment = HistoryOrderGetString(orderTicket, ORDER_COMMENT);
            orderInfo[x].volumeInitial = HistoryOrderGetDouble(orderTicket, ORDER_VOLUME_INITIAL);
            orderInfo[x].priceOpen = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_OPEN);
            orderInfo[x].priceStopLimit = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_STOPLIMIT);

从已平单里回捞止损止盈价

这段逻辑只做一件事:遍历历史订单,把每笔单子的 TP、SL 价格写进 orderInfo 数组,方便后续做盈亏分布或价格行为统计。 遍历前先用 HistoryOrdersTotal 拿到 totalOrdersHistory。若等于 0,直接 Print 提示「没有可处理的历史订单」并退出,避免在空循环里浪费 tick。 对每个序号 x 调用 HistoryOrderGetTicket(x) 取订单 ticket。失败就打印函数名、序号、ticket 和 GetLastError 错误码——实盘里常见于历史缓存未同步,重连后可能恢复。 成功则分别用 HistoryOrderGetDouble(orderTicket, ORDER_TP) 和 ORDER_SL 取止损止盈价。注意:若下单时未设 TP/SL,返回值为 0,后续判断要排除,否则会和「价格真的在 0 点」混淆。

MQL5 / C++
orderInfo[x].tpPrice = HistoryOrderGetDouble(orderTicket, ORDER_TP);
   orderInfo[x].slPrice = HistoryOrderGetDouble(orderTicket, ORDER_SL);
   }
   else
   {
   Print(
    __FUNCTION__, " HistoryOrderGetTicket(", x, ") failed. (orderTicket = ", orderTicket,
    ") *** Error Code: ", GetLastError()
   );
   }
  }
 }
 else
 {
  Print(__FUNCTION__, ": No order history available to be processed, totalOrdersHistory = ", totalOrdersHistory);
 }

「把历史订单倒进终端日志」

做历史回看时,最怕订单池取出来了却看不见字段。PrintOrdersHistory() 就是干这件事的:给定起止时间,它先调 GetHistoryData 把订单塞进 orderInfo 数组,再逐条把符号、开仓价、SL/TP、成交时间与状态打印到 MT5 Experts 日志。 函数头带 export 修饰,说明它不是闷在库里自用的,外部 EA 或脚本能直接点名调用,用法和 PrintDealsHistory() 一个路数。 代码里有个硬判断:ArraySize(orderInfo) 小于等于 0 就直接 Print 提示并 return,不会进循环刷屏。实际跑下来,若你填的 fromDateTime 到 toDateTime 区间内经纪商没任何挂单或成交流水,终端只会吐一行「No orders history found」,而不是空跑几十行横线。 循环部分用 r 从 0 到 totalOrders-1 遍历,每条订单打印 20 个左右字段,包括 typeFilling、typeTime、reason 这些容易在策略调试时忽略的枚举。想验证,就在脚本里写 PrintOrdersHistory(D'2024.01.01', TimeCurrent()),开 MT5 看日志里订单总数是否符合你那段时间的真实操作频次。外汇与贵金属杠杆品种波动剧烈,历史订单分析仅作风控复盘参考,不预示后续盈亏。

MQL5 / C++
class="type">void PrintOrdersHistory(class="type">class="kw">datetime fromDateTime, class="type">class="kw">datetime toDateTime) class="kw">export
  {
class=class="str">"cmt">//- Get and save the orders history for the specified period
   GetHistoryData(fromDateTime, toDateTime, GET_ORDERS_HISTORY_DATA);
   class="type">int totalOrders = ArraySize(orderInfo);
   if(totalOrders <= class="num">0)
     {
       Print("");
       Print(__FUNCTION__, ": No orders history found for the specified period.");
       class="kw">return; class=class="str">"cmt">//-- Exit the function
     }
   Print("");
   Print(__FUNCTION__, "-------------------------------------------------------------------------------");
   Print(
     "Found a total of ", totalOrders,
     " orders filled or cancelled between(", fromDateTime, ") and(", toDateTime, ")."
   );
   for(class="type">int r = class="num">0; r < totalOrders; r++)
     {
       Print("---------------------------------------------------------------------------------------------------");
       Print("Order #", (r + class="num">1));
       Print("Symbol: ", orderInfo[r].symbol);
       Print("Time Setup: ", orderInfo[r].timeSetup);
       Print("Type: ", EnumToString(orderInfo[r].type));
       Print("Ticket: ", orderInfo[r].ticket);
       Print("Position ID: ", orderInfo[r].positionId);
       Print("State: ", EnumToString(orderInfo[r].state));
       Print("Type Filling: ", EnumToString(orderInfo[r].typeFilling));
       Print("Type Time: ", EnumToString(orderInfo[r].typeTime));
       Print("Reason: ", EnumToString(orderInfo[r].reason));
       Print("Volume Initial: ", orderInfo[r].volumeInitial);
       Print("Price Open: ", orderInfo[r].priceOpen);
       Print("Price Stop Limit: ", orderInfo[r].priceStopLimit);
       Print("SL Price: ", orderInfo[r].slPrice);
       Print("TP Price: ", orderInfo[r].tpPrice);
       Print("Time Done: ", orderInfo[r].timeDone);
       Print("Expiration Time: ", orderInfo[r].expirationTime);
       Print("Comment: ", orderInfo[r].comment);
       Print("Magic: ", orderInfo[r].magic);
       Print("");
     }
  }

◍ 用自定义函数拼出历史头寸全生命周期

MT5 标准库里没有 HistoryPositionSelect() 或 HistoryPositionsTotal() 这类直接读历史头寸的接口,想复盘已平掉的头寸,只能自己写函数把成交(deal)和订单(order)按 POSITION_ID 拼起来。SavePositionsData() 就是干这个的:它只在 EX5 内核里调用,不导出,专门把零散交易记录重建成一条条从开仓到平仓的审计链。 函数先算 dealInfo 的总条数,把 positionInfo 数组预扩容到同样大小;如果 totalDealInfo == 0 直接 return,避免空转。随后逆序扫 dealInfo(从最新往旧翻),靠 dealInfo[x].entry == DEAL_ENTRY_OUT 抓退出成交——只记录已平仓的历史头寸,活仓一律跳过。 逮到退出成交后,用 positionId 反查对应的 DEAL_ENTRY_IN 入场成交,把开仓时间、价格、成交量、magic、注释写进 positionInfo。退出侧则补收盘价、平仓时间、利润、库存费、佣金,并用 MathAbs 算持有时长(秒),净利润 = profit + swap - commission。注意代码里有个反直觉点:退出成交是 DEAL_TYPE_BUY 时,头寸方向存为 POSITION_TYPE_SELL,反之亦然。 最后拿 orderInfo 按 POSITION_ID 且 ORDER_STATE_FILLED 匹配出原始订单,记下单号和类型,以区分是市价单还是挂单发起;全部跑完后对 positionInfo 缩容,剔掉空元素。整套逻辑在 MT5 里接上真实账户历史,能直接复算出每笔仓位的点值盈亏结构,外汇与贵金属品种杠杆高、滑点跳空频繁,复盘结果仅反映历史概率,不构成方向暗示。

MQL5 / C++
class="type">void SavePositionsData()
  {
class=class="str">"cmt">//-- Our function&class="macro">#x27;s code will go here
  }
class="type">int totalDealInfo = ArraySize(dealInfo);
ArrayResize(positionInfo, totalDealInfo);
class="type">int totalPositionsFound = class="num">0, posIndex = class="num">0;
if(totalDealInfo == class="num">0)
  {
   class="kw">return;
  }
for(class="type">int x = totalDealInfo - class="num">1; x >= class="num">0; x--)
  {
   if(dealInfo[x].entry == DEAL_ENTRY_OUT)
    {
     class=class="str">"cmt">// Process exit deal
    }
  }
for(class="type">int k = ArraySize(dealInfo) - class="num">1; k >= class="num">0; k--)
  {
   if(dealInfo[k].positionId == positionId)
    {
     if(dealInfo[k].entry == DEAL_ENTRY_IN)
      {
       exitDealFound = true;
       totalPositionsFound++;
       posIndex = totalPositionsFound - class="num">1;
       class=class="str">"cmt">// Save the entry deal data
       positionInfo[posIndex].openingDealTicket = dealInfo[k].ticket;
       positionInfo[posIndex].openTime = dealInfo[k].time;
       positionInfo[posIndex].openPrice = dealInfo[k].price;
       positionInfo[posIndex].volume = dealInfo[k].volume;
       positionInfo[posIndex].magic = dealInfo[k].magic;
       positionInfo[posIndex].comment = dealInfo[k].comment;
      }
    }
  }
if(exitDealFound)
  {
   if(dealInfo[x].type == DEAL_TYPE_BUY)
    {
     positionInfo[posIndex].type = POSITION_TYPE_SELL;
    }
   else
    {
     positionInfo[posIndex].type = POSITION_TYPE_BUY;
    }
   positionInfo[posIndex].positionId = dealInfo[x].positionId;
   positionInfo[posIndex].symbol = dealInfo[x].symbol;
   positionInfo[posIndex].profit = dealInfo[x].profit;
   positionInfo[posIndex].closingDealTicket = dealInfo[x].ticket;
   positionInfo[posIndex].closePrice = dealInfo[x].price;
   positionInfo[posIndex].closeTime = dealInfo[x].time;
   positionInfo[posIndex].swap = dealInfo[x].swap;
   positionInfo[posIndex].commission = dealInfo[x].commission;
   positionInfo[posIndex].duration = MathAbs((class="type">long)positionInfo[posIndex].closeTime -
                                              (class="type">long)positionInfo[posIndex].openTime);
   positionInfo[posIndex].netProfit = positionInfo[posIndex].profit + positionInfo[posIndex].swap -
                                      positionInfo[posIndex].commission;
  }
if(positionInfo[posIndex].type == POSITION_TYPE_BUY)
  {
class=class="str">"cmt">// Calculate TP and SL pip values for buy position

多空仓位的点值与挂单溯源

把持仓的 TP、SL 和实盈都换算成「点(pips)」时,买和卖的公式方向正好相反。买仓的 tpPips 用 (tpPrice - openPrice) 除点值,卖仓则反过来用 (openPrice - tpPrice),SL 同理;pipProfit 也是 close 减 open 或 open 减 close 的区别,符号方向错了面板就会显示亏损为正的怪象。 下面这段是买仓分支里计算挂单衍生信息的核心片段,注意 symbolPoint 每次都现取,避免跨品种点值错位: 代码中先判断 tpPrice>0 才计算 tpPips,再判断 slPrice>0 算 slPips,最后无条件用 closePrice 与 openPrice 算 pipProfit;卖仓分支仅调换了减数和被减数。 紧接着用一个 for 循环扫 orderInfo 数组,拿 positionId 匹配且 state==ORDER_STATE_FILLED 的订单,把 openingOrderTicket 赋给持仓记录。 switch 里只列了 ORDER_TYPE_BUY_LIMIT 和 ORDER_TYPE_BUY_STOP 两个 case,说明这一步在判定该买仓是否由限价或止损挂单触发,而不是直接市价入场。外汇与贵金属杠杆高,点值计算错一位可能让风控阈值偏离真实风险,建议开 MT5 把这段塞进持仓扫描函数里跑一遍 EURUSD 与 XAUUSD 的实盘 tick 验证。

MQL5 / C++
  if(positionInfo[posIndex].tpPrice > class="num">0)
    {
      class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
      positionInfo[posIndex].tpPips = class="type">int((positionInfo[posIndex].tpPrice -
                                          positionInfo[posIndex].openPrice) / symbolPoint);
    }
  if(positionInfo[posIndex].slPrice > class="num">0)
    {
      class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
      positionInfo[posIndex].slPips = class="type">int((positionInfo[posIndex].openPrice -
                                          positionInfo[posIndex].slPrice) / symbolPoint);
    }
class=class="str">"cmt">// Calculate pip profit for buy position
  class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
  positionInfo[posIndex].pipProfit = class="type">int((positionInfo[posIndex].closePrice -
                                         positionInfo[posIndex].openPrice) / symbolPoint);
 }
else
  {
class=class="str">"cmt">// Calculate TP and SL pip values for sell position
  if(positionInfo[posIndex].tpPrice > class="num">0)
    {
      class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
      positionInfo[posIndex].tpPips = class="type">int((positionInfo[posIndex].openPrice -
                                          positionInfo[posIndex].tpPrice) / symbolPoint);
    }
  if(positionInfo[posIndex].slPrice > class="num">0)
    {
      class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
      positionInfo[posIndex].slPips = class="type">int((positionInfo[posIndex].slPrice -
                                          positionInfo[posIndex].openPrice) / symbolPoint);
    }
class=class="str">"cmt">// Calculate pip profit for sell position
  class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
  positionInfo[posIndex].pipProfit = class="type">int((positionInfo[posIndex].openPrice -
                                         positionInfo[posIndex].closePrice) / symbolPoint);
  }
for(class="type">int k = class="num">0; k < ArraySize(orderInfo); k++)
  {
  if(
      orderInfo[k].positionId == positionInfo[posIndex].positionId &&
      orderInfo[k].state == ORDER_STATE_FILLED
   )
    {
      positionInfo[posIndex].openingOrderTicket = orderInfo[k].ticket;
      positionInfo[posIndex].ticket = positionInfo[posIndex].openingOrderTicket;
      class=class="str">"cmt">//- Determine if the position was initiated by a pending order or direct market entry
      class="kw">switch(orderInfo[k].type)
        {
         case ORDER_TYPE_BUY_LIMIT:
         case ORDER_TYPE_BUY_STOP:

「挂单触发的持仓要单独打标」

在 MT5 的持仓归因逻辑里,限价单和止损单转化来的仓位不能和市价单混为一谈。下面这段分支把 SELL_LIMIT、SELL_STOP、BUY_STOP_LIMIT、SELL_STOP_LIMIT 四类挂单成交后的持仓,统一标记 initiatedByPendingOrder = true,并记下 initiatingOrderType,其余类型走 default 标为 false。 区分这个字段的意义在于:回测或实盘统计时,挂单触发的仓位往往伴随跳空或流动性缺口,外汇与贵金属品种在高波动时段这种触发滑点可能明显放大,风险属性与市价单不同。 代码里用 break 提前退出 orderInfo 循环,说明一旦匹配到对应挂单类型就锁定来源,避免重复扫描。你在 EA 里接这套结构时,可直接用 positionInfo 数组的这两个字段做分组统计。

MQL5 / C++
   case ORDER_TYPE_SELL_LIMIT:
   case ORDER_TYPE_SELL_STOP:
   case ORDER_TYPE_BUY_STOP_LIMIT:
   case ORDER_TYPE_SELL_STOP_LIMIT:
         positionInfo[posIndex].initiatedByPendingOrder = true;
         positionInfo[posIndex].initiatingOrderType = orderInfo[k].type;
         break;
   class="kw">default:
         positionInfo[posIndex].initiatedByPendingOrder = false;
         positionInfo[posIndex].initiatingOrderType = orderInfo[k].type;
         break;
   }
      break; class=class="str">"cmt">//- Exit the orderInfo loop once the required data is found
   }
}
ArrayResize(positionInfo, totalPositionsFound);
class="type">void SavePositionsData()
  {
class=class="str">"cmt">//- Since every transaction is recorded as a deal, we will begin by scanning the deals and link them
class=class="str">"cmt">//- to different orders and generate the positions data using the POSITION_ID as the primary and foreign key
   class="type">int totalDealInfo = ArraySize(dealInfo);
   ArrayResize(positionInfo, totalDealInfo); class=class="str">"cmt">//- Resize the position array to match the deals array
   class="type">int totalPositionsFound = class="num">0, posIndex = class="num">0;
   if(totalDealInfo == class="num">0) class=class="str">"cmt">//- Check if we have any deal history available for processing
     {
      class="kw">return; class=class="str">"cmt">//- No deal data to process found, we can&class="macro">#x27;t go on. exit the function
     }
class=class="str">"cmt">//- Let us loop through the deals array
   for(class="type">int x = totalDealInfo - class="num">1; x >= class="num">0; x--)
     {
      class=class="str">"cmt">//- First we check if it is an exit deal to close a position
      if(dealInfo[x].entry == DEAL_ENTRY_OUT)
        {
         class=class="str">"cmt">//- We begin by saving the position id
         class="type">ulong positionId = dealInfo[x].positionId;
         class="type">bool exitDealFound = false;
         class=class="str">"cmt">//- Now we check if we have an exit deal from this position and save it&class="macro">#x27;s properties
         for(class="type">int k = ArraySize(dealInfo) - class="num">1; k >= class="num">0; k--)
           {
            if(dealInfo[k].positionId == positionId)
              {
               if(dealInfo[k].entry == DEAL_ENTRY_IN)
                 {
                  exitDealFound = true;
                  totalPositionsFound++;
                  posIndex = totalPositionsFound - class="num">1;
                  positionInfo[posIndex].openingDealTicket = dealInfo[k].ticket;
                  positionInfo[posIndex].openTime = dealInfo[k].time;
                  positionInfo[posIndex].openPrice = dealInfo[k].price;
                  positionInfo[posIndex].volume = dealInfo[k].volume;

◍ 从平仓成交反推持仓方向

在 MT5 的成交(deal)历史里,平仓动作本身只记录了一条反向成交。若退出成交是 DEAL_TYPE_BUY,说明原持仓必然是卖单,代码里把 positionInfo[posIndex].type 直接置为 POSITION_TYPE_SELL;反之则为买单。这一反推逻辑是后续统计胜率与持仓时长的前提,错一处就会导致整段回测样本标签反转。 平仓侧还要把 positionId、symbol、profit、swap、commission 等字段落进结构体。净盈利不是 deal 的 profit 字段直接拿来用,而是 profit + swap - commission,外汇与贵金属点差和库存费波动大,不扣这两项净值曲线会虚高。 持仓时长用 MathAbs 把 closeTime 与 openTime 的差转成秒级绝对值,便于后续按‘持仓小于 N 秒’过滤剥头皮单。tpPips 与 slPips 在这段先清零,等拿到 SYMBOL_POINT 后再按买卖方向补算,EURUSD 标准点值 0.0001 时 10 点就是 0.0010 价格差。 让小布替你跑这套 把这段逻辑塞进你自己的持仓扫描脚本,开 MT5 历史中心拉一个月真实成交,验证 positionInfo.type 的反推是否与账户持仓记录一致,不一致就查 deal 的 entry 标志位。

MQL5 / C++
if(exitDealFound) class=class="str">"cmt">//- Continue saving the exit deal data
  {
  class=class="str">"cmt">//- Save the position type
  if(dealInfo[x].type == DEAL_TYPE_BUY)
    {
    class=class="str">"cmt">//- If the exit deal is a buy, then the position was a sell trade
    positionInfo[posIndex].type = POSITION_TYPE_SELL;
    }
  else
    {
    class=class="str">"cmt">//- If the exit deal is a sell, then the position was a buy trade
    positionInfo[posIndex].type = POSITION_TYPE_BUY;
    }
  positionInfo[posIndex].positionId = dealInfo[x].positionId;
  positionInfo[posIndex].symbol = dealInfo[x].symbol;
  positionInfo[posIndex].profit = dealInfo[x].profit;
  positionInfo[posIndex].closingDealTicket = dealInfo[x].ticket;
  positionInfo[posIndex].closePrice = dealInfo[x].price;
  positionInfo[posIndex].closeTime = dealInfo[x].time;
  positionInfo[posIndex].swap = dealInfo[x].swap;
  positionInfo[posIndex].commission = dealInfo[x].commission;
  positionInfo[posIndex].tpPrice = dealInfo[x].tpPrice;
  positionInfo[posIndex].tpPips = class="num">0;
  positionInfo[posIndex].slPrice = dealInfo[x].slPrice;
  positionInfo[posIndex].slPips = class="num">0;
  class=class="str">"cmt">//- Calculate the trade duration in seconds
  positionInfo[posIndex].duration = MathAbs((class="type">long)positionInfo[posIndex].closeTime - (class="type">long)positionInfo[posIndex].openTime);
  class=class="str">"cmt">//- Calculate the net profit after swap and commission
  positionInfo[posIndex].netProfit =
     positionInfo[posIndex].profit + positionInfo[posIndex].swap - positionInfo[posIndex].commission;
  class=class="str">"cmt">//- Get pip values for the position
  if(positionInfo[posIndex].type == POSITION_TYPE_BUY) class=class="str">"cmt">//- Buy position
    {
    class=class="str">"cmt">//- Get sl and tp pip values
    if(positionInfo[posIndex].tpPrice > class="num">0)
      {
      class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
      positionInfo[posIndex].tpPips =

多单空单的止盈止损点数换算

把持仓的 TP、SL 以及实际盈亏统一折算成「pip 整数」,是面板统计的前置步骤。买、卖方向的计算分子正好相反,写错一个减号就会让防护距离变成负数。 买仓部分:TP 点数 = (tpPrice - openPrice) / point,SL 点数 = (openPrice - slPrice) / point,平仓盈亏 pipProfit = (closePrice - openPrice) / point。注意每段都先通过 SymbolInfoDouble 取 SYMBOL_POINT,跨品种(如 XAUUSD 的 point=0.01 与 EURUSD 的 point=0.0001)才不会串味。 卖仓镜像处理:TP 点数 = (openPrice - tpPrice) / point,SL 点数 = (slPrice - openPrice) / point,pipProfit = (openPrice - closePrice) / point。用 int 强制截断直接丢掉小数 pip,若你想保留 0.1 pip 精度就得改 double 并自行四舍五入。 下面这段是买/卖分支里 SL、TP 与盈亏点数落库的原始逻辑,开 MT5 把 positionInfo 结构补齐后即可编译验证。

MQL5 / C++
         class="type">int((positionInfo[posIndex].tpPrice - positionInfo[posIndex].openPrice) / symbolPoint);
         }
         if(positionInfo[posIndex].slPrice > class="num">0)
           {
            class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
            positionInfo[posIndex].slPips =
               class="type">int((positionInfo[posIndex].openPrice - positionInfo[posIndex].slPrice) / symbolPoint);
           }
           class=class="str">"cmt">//- Get the buy profit in pip value
           class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
           positionInfo[posIndex].pipProfit =
              class="type">int((positionInfo[posIndex].closePrice - positionInfo[posIndex].openPrice) / symbolPoint);
         }
         else class=class="str">"cmt">//- Sell position
           {
            class=class="str">"cmt">//- Get sl and tp pip values
            if(positionInfo[posIndex].tpPrice > class="num">0)
              {
               class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
               positionInfo[posIndex].tpPips =
                  class="type">int((positionInfo[posIndex].openPrice - positionInfo[posIndex].tpPrice) / symbolPoint);
              }
            if(positionInfo[posIndex].slPrice > class="num">0)
              {
               class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
               positionInfo[posIndex].slPips =
                  class="type">int((positionInfo[posIndex].slPrice - positionInfo[posIndex].openPrice) / symbolPoint);
              }
              class=class="str">"cmt">//- Get the sell profit in pip value
              class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT);
              positionInfo[posIndex].pipProfit =
                 class="type">int((positionInfo[posIndex].openPrice - positionInfo[posIndex].closePrice) / symbolPoint);
           }
           class=class="str">"cmt">//- Now we scan and get the opening order ticket in the orderInfo array
           for(class="type">int k = class="num">0; k < ArraySize(orderInfo); k++) class=class="str">"cmt">//- Search from the oldest to newest order

「把挂单和市价单从持仓里拆干净」

在 MT5 的 EA 里,持仓(position)和订单(order)是两套对象。想追溯某笔持仓到底是谁开的,就得拿 position 的 positionId 去 order 数组里比对,且只认 state 为 ORDER_STATE_FILLED 的成交单。 下面这段逻辑干的事很直接:命中相同 positionId 且订单已成交,就把开仓单的 ticket 写回 positionInfo,同时用 switch 判断 order type。如果是六种挂单类型(BUY_LIMIT / BUY_STOP / SELL_LIMIT / SELL_STOP / BUY_STOP_LIMIT / SELL_STOP_LIMIT)之一,标 initiatedByPendingOrder = true;其余统统算市价直入,标 false。

MQL5 / C++
{
 if(
  orderInfo[k].positionId == positionInfo[posIndex].positionId &&
  orderInfo[k].state == ORDER_STATE_FILLED
 )
  {
   class=class="str">"cmt">//- Save the order ticket that intiated the position
   positionInfo[posIndex].openingOrderTicket = orderInfo[k].ticket;
   positionInfo[posIndex].ticket = positionInfo[posIndex].openingOrderTicket;
   class=class="str">"cmt">//- Determine if the position was initiated by a pending order or direct market entry
   class="kw">switch(orderInfo[k].type)
   {
    class=class="str">"cmt">//- Pending order entry
    case ORDER_TYPE_BUY_LIMIT:
    case ORDER_TYPE_BUY_STOP:
    case ORDER_TYPE_SELL_LIMIT:
    case ORDER_TYPE_SELL_STOP:
    case ORDER_TYPE_BUY_STOP_LIMIT:
    case ORDER_TYPE_SELL_STOP_LIMIT:
     positionInfo[posIndex].initiatedByPendingOrder = true;
     positionInfo[posIndex].initiatingOrderType = orderInfo[k].type;
     break;
    class=class="str">"cmt">//- Direct market entry
    class="kw">default:
     positionInfo[posIndex].initiatedByPendingOrder = false;
     positionInfo[posIndex].initiatingOrderType = orderInfo[k].type;
     break;
   }
   break; class=class="str">"cmt">//--- We have everything we need, exit the orderInfo loop
  }
 }
}
 else class=class="str">"cmt">//--- Position id not found
 {
  class="kw">continue;class=class="str">"cmt">//- skip to the next iteration
 }
}
class=class="str">"cmt">//- Resize the positionInfo array and class="kw">delete all the indexes that have zero values
 ArrayResize(positionInfo, totalPositionsFound);
}
逐行拆一下:最外层 if 用 positionId 相等 + 订单已填充做过滤;命中后先存 openingOrderTicket,再把 ticket 指回它,保证后续平仓/统计都锁定源头单。switch 里六个 case 故意不写 break 前逻辑,靠穿透(fall-through)统一置 pending 标志,default 兜底市价单。 最后那句 ArrayResize(positionInfo, totalPositionsFound) 很关键——前面循环可能留了空槽,不压缩数组,后面遍历就会读到零值字段。外汇和贵金属杠杆高,这类源头追溯写错,批量平仓可能误杀别的单,实盘前务必在策略测试器用历史数据跑一遍。

MQL5 / C++
{
 if(
  orderInfo[k].positionId == positionInfo[posIndex].positionId &&
  orderInfo[k].state == ORDER_STATE_FILLED
 )
  {
   class=class="str">"cmt">//- Save the order ticket that intiated the position
   positionInfo[posIndex].openingOrderTicket = orderInfo[k].ticket;
   positionInfo[posIndex].ticket = positionInfo[posIndex].openingOrderTicket;
   class=class="str">"cmt">//- Determine if the position was initiated by a pending order or direct market entry
   class="kw">switch(orderInfo[k].type)
   {
    class=class="str">"cmt">//- Pending order entry
    case ORDER_TYPE_BUY_LIMIT:
    case ORDER_TYPE_BUY_STOP:
    case ORDER_TYPE_SELL_LIMIT:
    case ORDER_TYPE_SELL_STOP:
    case ORDER_TYPE_BUY_STOP_LIMIT:
    case ORDER_TYPE_SELL_STOP_LIMIT:
     positionInfo[posIndex].initiatedByPendingOrder = true;
     positionInfo[posIndex].initiatingOrderType = orderInfo[k].type;
     break;
    class=class="str">"cmt">//- Direct market entry
    class="kw">default:
     positionInfo[posIndex].initiatedByPendingOrder = false;
     positionInfo[posIndex].initiatingOrderType = orderInfo[k].type;
     break;
   }
   break; class=class="str">"cmt">//--- We have everything we need, exit the orderInfo loop
  }
 }
}
 else class=class="str">"cmt">//--- Position id not found
 {
  class="kw">continue;class=class="str">"cmt">//- skip to the next iteration
 }
}
class=class="str">"cmt">//- Resize the positionInfo array and class="kw">delete all the indexes that have zero values
 ArrayResize(positionInfo, totalPositionsFound);
}

◍ 空小节无内容可析

本小节原文未提供任何技术正文与代码,无法生成可验证内容。 若需补全,请返回包含价格行为或 MQL5 实现细节的源文本,蒸馏引擎方可输出硬核块。

把平仓历史直接打到日志里

做复盘时最怕手动翻交易记录,PrintPositionsHistory() 就是把这个动作自动化:传入起止时间,它从已缓存的 positionInfo 数组里把每一笔已平头寸的字段逐个 Print 出来。函数带 export 修饰,意味着你写的 EA 或脚本只要引了这套库,就能直接调用,不用重新实现打印逻辑。 调用前它会先跑 GetHistoryData(fromDateTime, toDateTime, GET_POSITIONS_HISTORY_DATA) 把指定区间的订单、成交、头寸历史拉进内存。若 ArraySize(positionInfo) 返回 0,说明这段时间内没有平仓记录,函数打印一句提示就 return,不会往下走空循环。 有数据时,它先报总数和区间,例如 Found a total of 12 positions closed between (2024.01.01 00:00) and (2024.01.31 23:59),然后按序号逐笔输出 Symbol、Ticket、Type、Volume、开平价格、SL/TP 对应的 pip 数、持仓时长、掉期、佣金、利润与 netProfit(保留两位小数)、pipProfit,以及是由什么类型的挂单触发的。外汇和贵金属保证金交易杠杆高、滑点大,历史打印出的净盈利不代表未来倾向重复,仅作样本核对用。 在 MT5 里接好库后,随便写个脚本传两个 datetime 调一下,就能在 Experts 日志里看到完整平仓清单,比在账户历史里肉眼筛效率高出一截。

MQL5 / C++
class="type">void PrintPositionsHistory(class="type">class="kw">datetime fromDateTime, class="type">class="kw">datetime toDateTime) class="kw">export
  {
class=class="str">"cmt">//- Get and save the deals, orders, positions history for the specified period
   GetHistoryData(fromDateTime, toDateTime, GET_POSITIONS_HISTORY_DATA);
   class="type">int totalPositionsClosed = ArraySize(positionInfo);
   if(totalPositionsClosed <= class="num">0)
     {
       Print("");
       Print(__FUNCTION__, ": No position history found for the specified period.");
       class="kw">return; class=class="str">"cmt">//- Exit the function
     }
   Print("");
   Print(__FUNCTION__, "-------------------------------------------------------------------------------");
   Print(
     "Found a total of ", totalPositionsClosed,
     " positions closed between(", fromDateTime, ") and(", toDateTime, ")."
   );
   for(class="type">int r = class="num">0; r < totalPositionsClosed; r++)
     {
       Print("---------------------------------------------------------------------------------------------------");
       Print("Position #", (r + class="num">1));
       Print("Symbol: ", positionInfo[r].symbol);
       Print("Time Open: ", positionInfo[r].openTime);
       Print("Ticket: ", positionInfo[r].ticket);
       Print("Type: ", EnumToString(positionInfo[r].type));
       Print("Volume: ", positionInfo[r].volume);
       Print("0pen Price: ", positionInfo[r].openPrice);
       Print("SL Price: ", positionInfo[r].slPrice, " (slPips: ", positionInfo[r].slPips, ")");
       Print("TP Price: ", positionInfo[r].tpPrice, " (tpPips: ", positionInfo[r].tpPips, ")");
       Print("Close Price: ", positionInfo[r].closePrice);
       Print("Close Time: ", positionInfo[r].closeTime);
       Print("Trade Duration: ", positionInfo[r].duration);
       Print("Swap: ", positionInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY));
       Print("Commission: ", positionInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY));
       Print("Profit: ", positionInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY));
       Print("Net profit: ", DoubleToString(positionInfo[r].netProfit, class="num">2), " ", AccountInfoString(ACCOUNT_CURRENCY));
       Print("pipProfit: ", positionInfo[r].pipProfit);
       Print("Initiating Order Type: ", EnumToString(positionInfo[r].initiatingOrderType));
       Print("Initiated By Pending Order: ", positionInfo[r].initiatedByPendingOrder);

「把持仓注释和魔数打到日志里」

在遍历持仓数组的循环体内,用 Print 把每笔持仓的 comment 与 magic 字段输出到专家日志,是定位策略实例最直接的方法。 当同时跑多个 EA 或手工单时,magic 不同却同品种同方向,光看终端持仓列表容易混淆;日志里逐行打印就能在回测或实盘排查时快速对应到具体策略。 外汇与贵金属杠杆高、滑点跳空频繁,日志只是排查手段,不代表任何收益倾向,实盘验证请先在模拟盘跑通。

MQL5 / C++
      Print("Comment: ", positionInfo[r].comment);
      Print("Magic: ", positionInfo[r].magic);
      Print("");
    }
  }

◍ 自建历史挂单扫描器

MT5 标准库里没有 HistoryPendingOrderSelect() 或 HistoryPendingOrdersTotal() 这类直接读历史挂单的接口,想复盘一笔 Buy Stop 从挂出到取消的全过程,只能自己写函数扫订单历史。SavePendingOrdersData() 就是干这个的:它不导出,只在 EX5 核心里被调用,把 orderInfo 里六种挂单类型(限价、止损及 StopLimit 双向)筛出来,塞进 pendingOrderInfo。 函数先按 orderInfo 总数给 pendingOrderInfo 预分配空间,totalOrderInfo 为 0 就直接 return,避免空转。随后用倒序循环 x 从 totalOrderInfo-1 到 0,保证最近的挂单先处理;命中挂单类型后 totalPendingOrdersFound 累加,pendingIndex 定位写入位。 TP/SL 点数不是直接存的,要用 SymbolInfoDouble(symbol, SYMBOL_POINT) 取点值,再拿 abs(tpPrice-priceOpen)/symbolPoint 强转 int。外汇与贵金属杠杆高、滑点可能让挂单不按原价成交,这套点数只反映挂出时的静态距离,实盘回测时别当实际盈亏基准。 最后 ArrayResize(pendingOrderInfo, totalPendingOrdersFound) 把尾部空元素砍掉,数组里只剩有效挂单。你开 MT5 把下面代码贴进库工程,跑完用 ArrayPrint 瞄一眼 pendingOrderInfo,就能确认某段历史里究竟漏没漏掉止损单。

MQL5 / C++
class="type">void SavePendingOrdersData()
  {
class=class="str">"cmt">//-- Function&class="macro">#x27;s code will go here
  }
class="type">int totalOrderInfo = ArraySize(orderInfo);
ArrayResize(pendingOrderInfo, totalOrderInfo);
class="type">int totalPendingOrdersFound = class="num">0, pendingIndex = class="num">0;
if(totalOrderInfo == class="num">0)
  {
   class="kw">return;
  }
for(class="type">int x = totalOrderInfo - class="num">1; x >= class="num">0; x--)
  {
   if(
       orderInfo[x].type == ORDER_TYPE_BUY_LIMIT || orderInfo[x].type == ORDER_TYPE_BUY_STOP ||
       orderInfo[x].type == ORDER_TYPE_SELL_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP ||
       orderInfo[x].type == ORDER_TYPE_BUY_STOP_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP_LIMIT
   )
     {
       totalPendingOrdersFound++;
       pendingIndex = totalPendingOrdersFound - class="num">1;
class=class="str">"cmt">//-- Save the pending order properties into the pendingOrderInfo array
     }
pendingOrderInfo[pendingIndex].type = orderInfo[x].type;
pendingOrderInfo[pendingIndex].state = orderInfo[x].state;
pendingOrderInfo[pendingIndex].positionId = orderInfo[x].positionId;
pendingOrderInfo[pendingIndex].ticket = orderInfo[x].ticket;
pendingOrderInfo[pendingIndex].symbol = orderInfo[x].symbol;
pendingOrderInfo[pendingIndex].timeSetup = orderInfo[x].timeSetup;
pendingOrderInfo[pendingIndex].expirationTime = orderInfo[x].expirationTime;
pendingOrderInfo[pendingIndex].timeDone = orderInfo[x].timeDone;
pendingOrderInfo[pendingIndex].typeTime = orderInfo[x].typeTime;
pendingOrderInfo[pendingIndex].priceOpen = orderInfo[x].priceOpen;
pendingOrderInfo[pendingIndex].tpPrice = orderInfo[x].tpPrice;
pendingOrderInfo[pendingIndex].slPrice = orderInfo[x].slPrice;
if(pendingOrderInfo[pendingIndex].tpPrice > class="num">0)
  {
   class="type">class="kw">double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT);
   pendingOrderInfo[pendingIndex].tpPips =
       (class="type">int)MathAbs((pendingOrderInfo[pendingIndex].tpPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint);
  }
if(pendingOrderInfo[pendingIndex].slPrice > class="num">0)
  {
   class="type">class="kw">double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT);
   pendingOrderInfo[pendingIndex].slPips =
       (class="type">int)MathAbs((pendingOrderInfo[pendingIndex].slPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint);
  }
pendingOrderInfo[pendingIndex].magic = orderInfo[x].magic;
pendingOrderInfo[pendingIndex].reason = orderInfo[x].reason;
pendingOrderInfo[pendingIndex].typeFilling = orderInfo[x].typeFilling;
pendingOrderInfo[pendingIndex].comment = orderInfo[x].comment;
pendingOrderInfo[pendingIndex].volumeInitial = orderInfo[x].volumeInitial;
pendingOrderInfo[pendingIndex].priceStopLimit = orderInfo[x].priceStopLimit;
ArrayResize(pendingOrderInfo, totalPendingOrdersFound);
class="type">void SavePendingOrdersData()
  {
class=class="str">"cmt">//- Let us begin by scanning the orders and link them to different deals
   class="type">int totalOrderInfo = ArraySize(orderInfo);
   ArrayResize(pendingOrderInfo, totalOrderInfo);
   class="type">int totalPendingOrdersFound = class="num">0, pendingIndex = class="num">0;
   if(totalOrderInfo == class="num">0)
     {
       class="kw">return; class=class="str">"cmt">//- No order data to process found, we can&class="macro">#x27;t go on. exit the function
     }

把挂单从总订单堆里筛出来

这段代码干的事很直接:倒序遍历已经拿到的全部订单数组 orderInfo,把六种挂单类型(BUY_LIMIT、BUY_STOP、SELL_LIMIT、SELL_STOP、BUY_STOP_LIMIT、SELL_STOP_LIMIT)挑出来,塞进专门的 pendingOrderInfo 数组。倒序从 totalOrderInfo-1 跑到 0,是为了避免删除或收缩数组时下标错位。 每个挂单被命中后,totalPendingOrdersFound 先自增,再用「计数减一」当新数组下标,把类型、状态、持仓ID、ticket、品种、挂单时间、到期时间、成交时间、时间类型、开仓价、TP、SL、magic、原因、填充类型、注释、初始手数、StopLimit价全部搬过去。 TP 和 SL 若大于 0,会调 SymbolInfoDouble 取该品种 SYMBOL_POINT,用 MathAbs 算 (tp/sl价 - 开仓价) / point 并强转 int,得到以「点」为单位的 tpPips / slPips。例如黄金 point 通常是 0.01,若挂单开价 1900、TP 1910,tpPips 就是 1000 点。 最后用 ArrayResize 把 pendingOrderInfo 截到真实挂单数量,多余零值下标直接砍掉。开 MT5 把这段接在你自己的订单枚举后面,打印 totalPendingOrdersFound 就能立刻看当前账户挂单总数。

MQL5 / C++
for(class="type">int x = totalOrderInfo - class="num">1; x >= class="num">0; x--)
  {
  class=class="str">"cmt">//- Check if it is a pending order and save its properties
  if(
     orderInfo[x].type == ORDER_TYPE_BUY_LIMIT || orderInfo[x].type == ORDER_TYPE_BUY_STOP ||
     orderInfo[x].type == ORDER_TYPE_SELL_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP ||
     orderInfo[x].type == ORDER_TYPE_BUY_STOP_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP_LIMIT
    )
    {
     totalPendingOrdersFound++;
     pendingIndex = totalPendingOrdersFound - class="num">1;
     pendingOrderInfo[pendingIndex].type = orderInfo[x].type;
     pendingOrderInfo[pendingIndex].state = orderInfo[x].state;
     pendingOrderInfo[pendingIndex].positionId = orderInfo[x].positionId;
     pendingOrderInfo[pendingIndex].ticket = orderInfo[x].ticket;
     pendingOrderInfo[pendingIndex].symbol = orderInfo[x].symbol;
     pendingOrderInfo[pendingIndex].timeSetup = orderInfo[x].timeSetup;
     pendingOrderInfo[pendingIndex].expirationTime = orderInfo[x].expirationTime;
     pendingOrderInfo[pendingIndex].timeDone = orderInfo[x].timeDone;
     pendingOrderInfo[pendingIndex].typeTime = orderInfo[x].typeTime;
     pendingOrderInfo[pendingIndex].priceOpen = orderInfo[x].priceOpen;
     pendingOrderInfo[pendingIndex].tpPrice = orderInfo[x].tpPrice;
     pendingOrderInfo[pendingIndex].slPrice = orderInfo[x].slPrice;
     if(pendingOrderInfo[pendingIndex].tpPrice > class="num">0)
       {
        class="type">class="kw">double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT);
        pendingOrderInfo[pendingIndex].tpPips =
          (class="type">int)MathAbs((pendingOrderInfo[pendingIndex].tpPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint);
       }
     if(pendingOrderInfo[pendingIndex].slPrice > class="num">0)
       {
        class="type">class="kw">double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT);
        pendingOrderInfo[pendingIndex].slPips =
          (class="type">int)MathAbs((pendingOrderInfo[pendingIndex].slPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint);
       }
     pendingOrderInfo[pendingIndex].magic = orderInfo[x].magic;
     pendingOrderInfo[pendingIndex].reason = orderInfo[x].reason;
     pendingOrderInfo[pendingIndex].typeFilling = orderInfo[x].typeFilling;
     pendingOrderInfo[pendingIndex].comment = orderInfo[x].comment;
     pendingOrderInfo[pendingIndex].volumeInitial = orderInfo[x].volumeInitial;
     pendingOrderInfo[pendingIndex].priceStopLimit = orderInfo[x].priceStopLimit;
     }
  }
class=class="str">"cmt">//--Resize the pendingOrderInfo array and class="kw">delete all the indexes that have zero values
  ArrayResize(pendingOrderInfo, totalPendingOrdersFound);
}

「把挂单历史打印到日志里」

做回测复盘或实盘巡检时,光看交易账户面板不够直观,把指定时段内已成交或已撤掉的挂单完整拉出来才方便核对。下面这个函数从 pendingOrderInfo 数组读数据,按时间区间把每一笔挂单的明细打到 MT5 Experts 日志,并且用 export 修饰,外部 EA 或脚本直接能调。 调用前务必先跑一次 GetHistoryData(fromDateTime, toDateTime, GET_PENDING_ORDERS_HISTORY_DATA),否则数组是空的,函数只会提示 No pending orders history found。数组长度用 ArraySize 取,小于等于 0 就直接 return,不会误打印表头。 循环里逐行输出 symbol、ticket、type、state、timeDone、volumeInitial、priceOpen、slPrice、tpPrice、expirationTime、positionId 等字段。注意原代码在 TP Price 行误写了 slPips 而不是 tpPips,复制后建议手动改掉,否则日志里止盈行会显示止损点数,排查时容易看错。外汇与贵金属挂单受滑点和流动性影响,历史状态仅作概率参考,实操请认清高风险。

MQL5 / C++
class="type">void PrintPendingOrdersHistory(class="type">class="kw">datetime fromDateTime, class="type">class="kw">datetime toDateTime) class="kw">export
  {
class=class="str">"cmt">//- Get and save the pending orders history for the specified period
   GetHistoryData(fromDateTime, toDateTime, GET_PENDING_ORDERS_HISTORY_DATA);
   class="type">int totalPendingOrders = ArraySize(pendingOrderInfo);
   if(totalPendingOrders <= class="num">0)
     {
       Print("");
       Print(__FUNCTION__, ": No pending orders history found for the specified period.");
       class="kw">return; class=class="str">"cmt">//- Exit the function
     }
   Print("");
   Print(__FUNCTION__, "-------------------------------------------------------------------------------");
   Print(
     "Found a total of ", totalPendingOrders,
     " pending orders filled or cancelled between(", fromDateTime, ") and(", toDateTime, ")."
   );
   for(class="type">int r = class="num">0; r < totalPendingOrders; r++)
     {
       Print("---------------------------------------------------------------------------------------------------");
       Print("Pending Order #", (r + class="num">1));
       Print("Symbol: ", pendingOrderInfo[r].symbol);
       Print("Time Setup: ", pendingOrderInfo[r].timeSetup);
       Print("Type: ", EnumToString(pendingOrderInfo[r].type));
       Print("Ticket: ", pendingOrderInfo[r].ticket);
       Print("State: ", EnumToString(pendingOrderInfo[r].state));
       Print("Time Done: ", pendingOrderInfo[r].timeDone);
       Print("Volume Initial: ", pendingOrderInfo[r].volumeInitial);
       Print("Price Open: ", pendingOrderInfo[r].priceOpen);
       Print("SL Price: ", pendingOrderInfo[r].slPrice, " (slPips: ", pendingOrderInfo[r].slPips, ")");
       Print("TP Price: ", pendingOrderInfo[r].tpPrice, " (slPips: ", pendingOrderInfo[r].slPips, ")");
       Print("Expiration Time: ", pendingOrderInfo[r].expirationTime);
       Print("Position ID: ", pendingOrderInfo[r].positionId);
       Print("Price Stop Limit: ", pendingOrderInfo[r].priceStopLimit);
       Print("Type Filling: ", EnumToString(pendingOrderInfo[r].typeFilling));
       Print("Type Time: ", EnumToString(pendingOrderInfo[r].typeTime));
       Print("Reason: ", EnumToString(pendingOrderInfo[r].reason));
       Print("Comment: ", pendingOrderInfo[r].comment);
       Print("Magic: ", pendingOrderInfo[r].magic);
       Print("");
     }
  }

◍ 把这条线请下神坛

这套历史管理 EX5 库目前只搭好了地基:检索、保存、分类订单与成交数据的核心函数都写完了,但大多属于预备性质,真正面向使用者的排序和分析接口还没接上。附带的 HistoryManager.mq5 源文件约 33.95 KB,你开 MT5 直接丢进本地 Include 目录就能看内部实现。 下一篇会补上按常见需求拉数据的导出函数——比如查最近平仓属性、算当天平仓利润、按交易品种过滤每周点利润,还会做接近 MT5 策略测试器的真实历史报告。外汇与贵金属杠杆高、滑点无常,这类统计只帮你看清过去,不预示下一笔盈亏。 库文档和分步 EA 示例会一并放出,集成进你自己的项目后,调一个过滤参数就能跑出某幻数下的净盈亏分布。写到这为止,剩下的留给你在终端里敲出来验证。

把历史扫描交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到成交与持仓的历史切片,你只需专注策略层面的参数调优。

常见问题

两者执行后都拿到唯一单号和 POSITION_ID,但挂单先有无效态再转市价,历史检索时需回溯原挂单触发记录,不能只按成交时间排序。
不会,POSITION_ID 保持不变,系统只是在交易历史里追加退出交易条目,分析模块应按标识符聚合而非按单号切分。
目前小布盯盘内置的是平台标准历史接口的可视化,自写库若输出为 CSV 或全局变量,可借助其 AIGC 面板做二次解析,无需重复造轮子。
反转被记为退出交易且可能伴随新开仓,若只遍历 POSITION 历史而不联合 DEAL 历史,就会丢掉中间态,需用跨表关联查询。
视评估目标而定,品种维度看单一标的绩效,EA 维度看策略稳定性,灵活标准应在分析模块里做成可切换过滤器。