MQL5 交易工具包(第 4 部分):开发历史管理 EX5 库·进阶篇
「把成交与挂单拆成可回溯的结构体」
做历史回测或复盘时,最麻烦的是 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 系列函数。
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。
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 那些函数还没写。这是开发早期的正常状态,等缺失函数补齐,库才能零警告过编译。外汇与贵金属交易历史回测涉及高杠杆风险,任何历史函数产出都只是概率参考。
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 更安全。外汇与贵金属历史含跳空与周末断层,回测前务必确认所选区间已本地缓存。
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 结构体需你自行在头部定义字段。
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 提示无可处理成交并带上计数,省得后面空跑。外汇与贵金属杠杆高,这类历史解析只用于复盘统计,任何从历史盈利推出来的规律都只是概率倾向,不能直接当成下一单的依据。
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); }