MQL5 交易工具包(第 4 部分):开发历史管理 EX5 库·进阶篇
📘

MQL5 交易工具包(第 4 部分):开发历史管理 EX5 库·进阶篇

第 2/3 篇

「把成交与挂单拆成可回溯的结构体」

做历史回测或复盘时,最麻烦的是 MT5 标准接口把持仓、成交、订单分散在三个池子里。直接读 PositionSelect / HistorySelect 容易漏掉由挂单触发的仓位,以及被取消的 pending 本身。 下面这三组结构体把一笔交易的生命周期压平了:PositionData 存已平仓的完整属性,从开仓单号 openingOrderTicket 到平仓成交 closingDealTicket,再到以点为单位的 tpPips / slPips 和净收益 netProfit,一共 28 个字段。 PendingOrderData 则专门收挂单,state 用 ENUM_ORDER_STATE 区分是已成交还是过期取消,expirationTime 留着算挂单存活时长。外汇与贵金属杠杆高,复盘时若漏掉取消的挂单,对胜率和滑点统计的偏差可能超过 10%。 开 MT5 把这段 struct 贴进 EA 头文件,编译后用一个简单循环把 HistoryDealsTotal 灌进去,就能直接按 magic 或 symbol 拉出每笔净盈亏,不用再反复调 OrderGet 系列函数。

MQL5 / C++
class="type">class="kw">double tpPrice;
 class="type">class="kw">double slPrice;
 };
class=class="str">"cmt">//- Data structure to store closed position/trade properties
class="kw">struct PositionData
 {
 class="type">ENUM_POSITION_TYPE type;
 class="type">ulong ticket;
 ENUM_ORDER_TYPE initiatingOrderType;
 class="type">ulong positionId;
 class="type">bool initiatedByPendingOrder;
 class="type">ulong openingOrderTicket;
 class="type">ulong openingDealTicket;
 class="type">ulong closingDealTicket;
 class="type">class="kw">string symbol;
 class="type">class="kw">double volume;
 class="type">class="kw">double openPrice;
 class="type">class="kw">double closePrice;
 class="type">class="kw">datetime openTime;
 class="type">class="kw">datetime closeTime;
 class="type">long duration;
 class="type">class="kw">double commission;
 class="type">class="kw">double swap;
 class="type">class="kw">double profit;
 class="type">class="kw">double tpPrice;
 class="type">class="kw">double slPrice;
 class="type">int tpPips;
 class="type">int slPips;
 class="type">int pipProfit;
 class="type">class="kw">double netProfit;
 class="type">ulong magic;
 class="type">class="kw">string comment;
 };
class=class="str">"cmt">//- Data structure to store executed or canceled pending order properties
class="kw">struct PendingOrderData
 {
 class="type">class="kw">string symbol;
 ENUM_ORDER_TYPE type;
 ENUM_ORDER_STATE state;
 class="type">class="kw">double priceOpen;
 class="type">class="kw">double tpPrice;
 class="type">class="kw">double slPrice;
 class="type">int tpPips;
 class="type">int slPips;
 class="type">ulong positionId;
 class="type">ulong ticket;
 class="type">class="kw">datetime timeSetup;
 class="type">class="kw">datetime expirationTime;

订单结构体里那些容易漏的字段

在 MT5 的 MQL5 里,自定义订单相关结构体时,有几个字段常被忽略但直接影响回测与实盘一致性。比如 typeFilling 决定市价单的成交模式,不显式赋值就可能用券商默认策略,滑点表现和你想的不一样。 timeDone 与 typeTime 组合能还原订单有效期逻辑,pending 单的 GTC 和 IOC 行为就藏在这里;magic 与 reason 则用于区分策略来源与下单动机,做多策略同账户运行时缺一不可。 下面这段结构体声明把订单核心字段收拢在一起,开 MT5 新建 include 文件粘贴即可编译验证,重点看 priceStopLimit 在 stop limit 单外的默认值为 0。

MQL5 / C++
class="type">class="kw">datetime              timeDone;
ENUM_ORDER_TYPE_TIME   typeTime;
class="type">ulong                  magic;
ENUM_ORDER_REASON      reason;
ENUM_ORDER_TYPE_FILLING typeFilling;
class="type">class="kw">string                 comment;
class="type">class="kw">double                 volumeInitial;
class="type">class="kw">double                 priceStopLimit;
};
OrderData orderInfo[];
DealData dealInfo[];
PositionData positionInfo[];
PendingOrderData pendingOrderInfo[];

◍ 历史数据检索的底层支柱

GetHistoryData() 是这套 EX5 库的内核函数,只供库内部调用、不导出。它用三个参数锁定数据范围:fromDateTime 与 toDateTime 两个 datetime 变量框定起止,dataToGet 这个 uint 对应文件顶部预定义常量,决定拉取成交、订单、持仓还是全量历史。 MQL5 的 datetime 本质是从 1970-01-01 00:00:00 UTC 起算的秒数长整型,所以日期校验直接比大小即可。注意一点:HistorySelect() 拉历史依据的是交易服务器时间,不是你本地机器时间,跨时区排查问题时常在这里踩坑。 函数先卡 fromDateTime >= toDateTime 的无效区间,命中就 Print 报错并 return false。通过后 ResetLastError() 清掉错误缓存,再进 HistorySelect() 的 if-else:成功就按 dataToGet 走 switch 分发到 SaveDealsData / SaveOrdersData 等具体处理函数;失败则打印错误码并退出。 现在直接编译会报一大堆错——因为 SaveDealsData 那些函数还没写。这是开发早期的正常状态,等缺失函数补齐,库才能零警告过编译。外汇与贵金属交易历史回测涉及高杠杆风险,任何历史函数产出都只是概率参考。

MQL5 / C++
class="type">bool GetHistoryData(class="type">class="kw">datetime fromDateTime, class="type">class="kw">datetime toDateTime, class="type">uint dataToGet)
  {
   class="kw">return(true);
class=class="str">"cmt">//-- Our function&class="macro">#x27;s code will go here
  }
if(fromDateTime >= toDateTime)
    {
      class=class="str">"cmt">//- Invalid time period selected
      Print("Invalid time period provided. Can&class="macro">#x27;t load history!");
      class="kw">return(false);
    }
ResetLastError();
if(HistorySelect(fromDateTime, toDateTime)) class=class="str">"cmt">//- History selected ok
  {
class=class="str">"cmt">//-- Code to process the history data will go here
  }
else class=class="str">"cmt">//- History selecting failed
  {
   Print("Selecting the history failed. Error code = ", GetLastError());
   class="kw">return(false);
  }
class="kw">switch(dataToGet)
  {
   case GET_DEALS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the deals history data
      SaveDealsData();
      break;
   case GET_ORDERS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the orders history data
      SaveOrdersData();
      break;
   case GET_POSITIONS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the positions history data
      SaveDealsData();  class=class="str">"cmt">//- Needed to generate the positions history data
      SaveOrdersData(); class=class="str">"cmt">//- Needed to generate the positions history data
      SavePositionsData();
      break;
   case GET_PENDING_ORDERS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the pending orders history data
      SaveOrdersData(); class=class="str">"cmt">//- Needed to generate the pending orders history data
      SavePendingOrdersData();
      break;
   case GET_ALL_HISTORY_DATA: class=class="str">"cmt">//- Get and save all the history data
      SaveDealsData();
      SaveOrdersData();
      SavePositionsData();
      SavePendingOrdersData();
      break;
   class="kw">default: class=class="str">"cmt">//-- Unknown entry
      Print("-----------------------------------------------------------------------------------------");
      Print(__FUNCTION__, ": Can&class="macro">#x27;t fetch the historical data you need.");
      Print("*** Please specify the historical data you need in the(dataToGet) parameter.");
      break;
  }
class="type">bool GetHistoryData(class="type">class="kw">datetime fromDateTime, class="type">class="kw">datetime toDateTime, class="type">uint dataToGet)
  {
class=class="str">"cmt">//- Check if the provided period of dates are valid
   if(fromDateTime >= toDateTime)
    {
      class=class="str">"cmt">//- Invalid time period selected

「按数据类型分流抓取历史」

在通过时间区间校验并调用 HistorySelect 之后,程序进入按 dataToGet 参数分流的环节。这个 switch 决定了你到底把哪一类账户历史落盘,而不是一股脑全拉。 GET_DEALS_HISTORY_DATA 只调 SaveDealsData,GET_ORDERS_HISTORY_DATA 只调 SaveOrdersData,二者彼此独立。但 GET_POSITIONS_HISTORY_DATA 和 GET_PENDING_ORDERS_HISTORY_DATA 都得先 SaveOrdersData,前者还要先 SaveDealsData——因为持仓与挂单的派生统计依赖成交与订单原始记录。 GET_ALL_HISTORY_DATA 把四个 Save 函数全跑一遍,相当于前面几种的并集。若 dataToGet 不在已知枚举内,default 分支会打印函数名与提示,要求显式指定参数,不会静默返回空数据。 HistorySelect 失败时要看 GetLastError 的具体码,常见如 5201(历史未同步),此时直接 return(false) 比继续跑 Save 更安全。外汇与贵金属历史含跳空与周末断层,回测前务必确认所选区间已本地缓存。

MQL5 / C++
Print("Invalid time period provided. Can&class="macro">#x27;t load history!");
   class="kw">return(false);
   }
class=class="str">"cmt">//- Reset last error and get the history
   ResetLastError();
   if(HistorySelect(fromDateTime, toDateTime)) class=class="str">"cmt">//- History selected ok
   {
   class=class="str">"cmt">//- Get the history data
   class="kw">switch(dataToGet)
     {
      case GET_DEALS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the deals history data
         SaveDealsData();
         break;
      case GET_ORDERS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the orders history data
         SaveOrdersData();
         break;
      case GET_POSITIONS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the positions history data
         SaveDealsData();  class=class="str">"cmt">//- Needed to generate the positions history data
         SaveOrdersData(); class=class="str">"cmt">//- Needed to generate the positions history data
         SavePositionsData();
         break;
      case GET_PENDING_ORDERS_HISTORY_DATA: class=class="str">"cmt">//- Get and save only the pending orders history data
         SaveOrdersData(); class=class="str">"cmt">//- Needed to generate the pending orders history data
         SavePendingOrdersData();
         break;
      case GET_ALL_HISTORY_DATA: class=class="str">"cmt">//- Get and save all the history data
         SaveDealsData();
         SaveOrdersData();
         SavePositionsData();
         SavePendingOrdersData();
         break;
      class="kw">default: class=class="str">"cmt">//-- Unknown entry
         Print("-----------------------------------------------------------------------------------------");
         Print(__FUNCTION__, ": Can&class="macro">#x27;t fetch the historical data you need.");
         Print("*** Please specify the historical data you need in the(dataToGet) parameter.");
         break;
     }
   }
   else
   {
   Print(__FUNCTION__, ": Selecting the history failed. Error code = ", GetLastError());
   class="kw">return(false);
   }
   class="kw">return(true);
}

把成交历史塞进动态数组的落地写法

SaveDealsData() 只在库内部被 GetHistoryData() 调用,不对外导出也不返回值。因为上层已经做过 HistorySelect(),这里直接读缓存即可,省掉重复拉取历史的时间开销。 先拿 HistoryDealsTotal() 拿到缓存里成交总数,存进整型 totalDeals;同时声明 ulong 型 dealTicket 装单据号。若 totalDeals ≤ 0,Print 打一条“无可处理成交”的日志并直接退出,避免空跑循环。 确认有数据后,用 ArrayResize(dealInfo, totalDeals) 把动态结构数组一次性扩到成交总量,容量对齐才不会越界。随后 for 循环从 x = totalDeals-1 倒序跑到 0,即从最近一笔往回扫,每轮先用 HistoryDealGetTicket(x) 取号。 号若大于 0,就把 entry、type、magic、positionId、symbol、volume、price、time、commission、swap、profit 等十余个属性分别通过 HistoryDealGetInteger / GetDouble / GetString 写进 dealInfo[x] 对应字段;取号失败则 Print 附带 GetLastError() 错误码,方便你开 MT5 看日志定位断点。 下面这段是函数骨架与核心填充逻辑,复制进 MQ5 文件即可编译验证;dealInfo 结构体需你自行在头部定义字段。

MQL5 / C++
class="type">void SaveDealsData()
  {
class=class="str">"cmt">//-- Our function&class="macro">#x27;s code will go here
  }
class="type">int totalDeals = HistoryDealsTotal();
class="type">ulong dealTicket;
if(totalDeals > class="num">0)
  {
class=class="str">"cmt">//-- Code to process deal goes here
  }
else
  {
   Print(__FUNCTION__, ": No deals available to be processed, totalDeals = ", totalDeals);
  }
ArrayResize(dealInfo, totalDeals);
for(class="type">int x = totalDeals - class="num">1; x >= class="num">0; x--)
  {
   ResetLastError();
   dealTicket = HistoryDealGetTicket(x);
   if(dealTicket > class="num">0)
    {
     class=class="str">"cmt">//- Deal ticket selected ok, we can now save the deals properties
     dealInfo[x].ticket = dealTicket;
     dealInfo[x].entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
     dealInfo[x].type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);
     dealInfo[x].magic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC);
     dealInfo[x].positionId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
     dealInfo[x].order = HistoryDealGetInteger(dealTicket, DEAL_ORDER);
     dealInfo[x].symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);
     dealInfo[x].comment = HistoryDealGetString(dealTicket, DEAL_COMMENT);
     dealInfo[x].volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);
     dealInfo[x].price = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
     dealInfo[x].time = (class="type">class="kw">datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
     dealInfo[x].tpPrice = HistoryDealGetDouble(dealTicket, DEAL_TP);
     dealInfo[x].slPrice = HistoryDealGetDouble(dealTicket, DEAL_SL);
     dealInfo[x].commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
     dealInfo[x].swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP);
     dealInfo[x].reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON);
     dealInfo[x].profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
    }
   else
    {
     Print(
       __FUNCTION__, " HistoryDealGetTicket(", x, ") failed. (dealTicket = ", dealTicket,
       ") *** Error Code: ", GetLastError()
     );
    }
  }
class="type">void SaveDealsData()
  {
class=class="str">"cmt">//- Get the number of loaded history deals
   class="type">int totalDeals = HistoryDealsTotal();
   class="type">ulong dealTicket;
class=class="str">"cmt">//-

◍ 把历史成交逐笔塞进结构体数组

拿到总成交数 totalDeals 后,先判空再动手。若大于 0,用 ArrayResize 把 dealInfo 动态数组拉到同样长度,避免越界写入;这一步决定了后面循环能否安全落数据。 循环从 x = totalDeals - 1 倒序跑到 0,每轮先 ResetLastError 清掉上一轮的残留错误码,再调 HistoryDealGetTicket(x) 取第 x 笔成交的 ticket。ticket 大于 0 才继续,否则 Print 出函数名、x 值和 GetLastError 方便查哪一笔断了。 ticket 有效时,依次用 HistoryDealGetInteger / GetDouble / GetString 把入场方向、类型、magic、持仓ID、挂单号、品种、注释、成交量、价格、时间、止盈止损、佣金、库存费、触发原因、盈亏写进 dealInfo[x]。其中 DEAL_TP 和 DEAL_SL 在原片段未走宏包裹,实盘里建议确认终端版本是否认这种裸常量。 若 totalDeals 为 0,直接 Print 提示无可处理成交并带上计数,省得后面空跑。外汇与贵金属杠杆高,这类历史解析只用于复盘统计,任何从历史盈利推出来的规律都只是概率倾向,不能直接当成下一单的依据。

MQL5 / C++
class=class="str">"cmt">//- Check if we have any deals to be worked on
   if(totalDeals > class="num">0)
     {
       class=class="str">"cmt">//- Resize the dynamic array that stores the deals
       ArrayResize(dealInfo, totalDeals);
       class=class="str">"cmt">//- Let us loop through the deals and save them one by one
       for(class="type">int x = totalDeals - class="num">1; x >= class="num">0; x--)
         {
          ResetLastError();
          dealTicket = HistoryDealGetTicket(x);
          if(dealTicket > class="num">0)
            {
             class=class="str">"cmt">//- Deal ticket selected ok, we can now save the deals properties
             dealInfo[x].ticket = dealTicket;
             dealInfo[x].entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
             dealInfo[x].type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);
             dealInfo[x].magic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC);
             dealInfo[x].positionId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
             dealInfo[x].order = HistoryDealGetInteger(dealTicket, DEAL_ORDER);
             dealInfo[x].symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);
             dealInfo[x].comment = HistoryDealGetString(dealTicket, DEAL_COMMENT);
             dealInfo[x].volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);
             dealInfo[x].price = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
             dealInfo[x].time = (class="type">class="kw">datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);
             dealInfo[x].tpPrice = HistoryDealGetDouble(dealTicket, DEAL_TP);
             dealInfo[x].slPrice = HistoryDealGetDouble(dealTicket, DEAL_SL);
             dealInfo[x].commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
             dealInfo[x].swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP);
             dealInfo[x].reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON);
             dealInfo[x].profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
             }
           else
             {
              Print(
               __FUNCTION__, " HistoryDealGetTicket(", x, ") failed. (dealTicket = ", dealTicket,
               ") *** Error Code: ", GetLastError()
              );
             }
         }
     }
   else
     {
       Print(__FUNCTION__, ": No deals available to be processed, totalDeals = ", totalDeals);
     }

常见问题

用两个独立结构体数组分别承接,成交走 deal 结构、挂单走 order 结构,避免字段错位导致统计乱套。
成交价外的手续费、库存费、成交时间毫秒段最常被忽略,漏了会让盈亏复盘偏差明显。
小布可读取你的品种页历史,自动比对成交与挂单分流结果,标出缺字段或类型不匹配的位置。
只取 deal 或只取 order 能少读一半以上冗余数据,大周期回测时延迟下降显著。
用 ArrayResize 按实际笔数扩容,别预设死长度,否则多品种切换时容易越界报错。