MetaTrader 5 中的订单、持仓和成交·综合运用
(3/3)· 多数EA在回测与实盘间数据错位,根源是没搞清订单、持仓和成交的缓存边界
「从成交历史里捞出自己的订单」
在 MT5 的 EA 或脚本里遍历历史订单时,最怕把别的程序挂的单向标志(magic number)混进来。上面这段用 HistoryOrderGetInteger 按 ticket 取 ORDER_TIME_DONE、ORDER_MAGIC、ORDER_POSITION_ID 三个字段,再用 if(order_magic==my_magic) 把自家订单筛出来单独处理。 PrintFormat 那行把订单号、magic、成交时间和关联持仓 ID 打到日志,方便你回看某笔单子到底什么时候平、挂在哪个持仓下。若 HistoryOrderGetInteger 返回失败,else 分支会报出索引和 GetLastError 代码,通常错误 4301 代表历史里没这条 ticket。 实盘或复盘前,先把 my_magic 设成你 EA 专用的非零整数,否则默认 0 会和手动单撞车。外汇与贵金属杠杆高,历史校验只用于排查逻辑,不代表任何收益倾向。
class=class="str">"cmt">//--- 执行时间 class="type">class="kw">datetime time_done=HistoryOrderGetInteger(order_ticket,ORDER_TIME_DONE); class="type">long order_magic =HistoryOrderGetInteger(order_ticket,ORDER_MAGIC); class="type">long pos_ID =HistoryOrderGetInteger(order_ticket,ORDER_POSITION_ID); if(order_magic==my_magic) { class=class="str">"cmt">// 处理设为 ORDER_MAGIC 的订单 } PrintFormat("订单 #%d: ORDER_MAGIC=#%d, time_done %s, ORDER_POSITION_ID=%d", order_ticket,order_magic,TimeToString(time_done),pos_ID); } else class=class="str">"cmt">// 从历史中获取订单没有成功 { PrintFormat("我们无法根据索引 %d 选择订单. 错误 %d", i,GetLastError()); } } }
◍ 从缓存里捞历史成交的正确姿势
MT5 里读历史成交和读历史订单走的是同一套缓存机制。HistoryDealsTotal() 返回的数量,完全取决于你之前用 HistorySelect(start, end) 或 HistorySelectByPosition() 往内存里塞了多少条记录。只想要某一笔,直接 HistoryDealSelect(ticket) 单点载入即可,不必拖整库。 实战里更常见的是循环扫描:先用 HistorySelect 限定时间窗把记录载进缓存,再拿 HistoryDealsTotal 取总数,随后用 HistoryDealGetTicket 按索引拿单证号,最后用 HistoryDealGetDouble / GetInteger / GetString 把字段抠出来分析。重点提醒:别把全量历史无脑载进来——外汇和贵金属交易历史动辄几万条,缓存爆满会直接拖垮 EA 性能,属于典型的高风险低效写法。 下面这段演示了限定 24 小时窗口取最后一笔成交,并打印订单号与 magic: datetime end=TimeCurrent(); // 当前服务器时间 datetime start=end-PeriodSeconds(PERIOD_D1); // 起始时间设为24小时之前 HistorySelect(start,end); int deals=HistoryDealsTotal(); ulong deal_ticket=HistoryDealGetTicket(deals-1); if(deal_ticket>0) { ulong order =HistoryDealGetInteger(deal_ticket,DEAL_ORDER); long order_magic=HistoryDealGetInteger(deal_ticket,DEAL_MAGIC); long pos_ID =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID); PrintFormat("交易 #%d 订单 #%d ORDER_MAGIC=%d 仓位编号",deals-1,order,order_magic,pos_ID); } 把 start 改成 end-PeriodSeconds(PERIOD_W1) 就能切到近一周,跑一遍看 Print 日志,比读文档直观。
class=class="str">"cmt">// --- 决定请求交易历史的时间间隔 class="type">class="kw">datetime end=TimeCurrent(); class=class="str">"cmt">// 当前服务器时间 class="type">class="kw">datetime start=end-PeriodSeconds(PERIOD_D1); class=class="str">"cmt">// 把起始时间设为24小时之前 class=class="str">"cmt">//--- 在程序缓存中根据所需时间间隔请求交易历史 HistorySelect(start,end); class=class="str">"cmt">//--- 获得历史中的交易总数 class="type">int deals=HistoryDealsTotal(); class=class="str">"cmt">//--- 获取列表中最后一个索引的交易单号 class="type">class="kw">ulong deal_ticket=HistoryDealGetTicket(deals-class="num">1); if(deal_ticket>class="num">0) class=class="str">"cmt">// 我们在缓存中得到交易了,处理它 { class=class="str">"cmt">//--- 所达成交易的订单编号 class="type">class="kw">ulong order =HistoryDealGetInteger(deal_ticket,DEAL_ORDER); class="type">long order_magic=HistoryDealGetInteger(deal_ticket,DEAL_MAGIC); class="type">long pos_ID =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID); PrintFormat("交易 #%d 订单 #%d ORDER_MAGIC=%d 仓位编号", deals-class="num">1,order,order_magic,pos_ID); } else class=class="str">"cmt">//尝试获取交易没有成功 { PrintFormat("总之,%d 个交易历史中, 我们无法根据"+ "索引 %d 选择交易. 错误 %d",deals,deals-class="num">1,GetLastError()); } class="kw">input class="type">long my_magic=class="num">111; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序起始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">// --- 决定请求交易历史的时间间隔 class="type">class="kw">datetime end=TimeCurrent(); class=class="str">"cmt">// 当前服务器时间 class="type">class="kw">datetime start=end-PeriodSeconds(PERIOD_D1); class=class="str">"cmt">// 起始时间设为24小时之前 class=class="str">"cmt">//--- 在程序缓存中请求交易历史所需的时间间隔 HistorySelect(start,end); class=class="str">"cmt">//--- 获得历史中的交易数量 class="type">int deals=HistoryDealsTotal(); class="type">int returns=class="num">0; class="type">class="kw">double profit=class="num">0; class="type">class="kw">double loss=class="num">0; class=class="str">"cmt">//--- 扫描历史中的所有交易 for(class="type">int i=class="num">0;i<deals;i++) { class=class="str">"cmt">//--- 根据其在列表中的索引获取交易单号 class="type">class="kw">ulong deal_ticket=HistoryDealGetTicket(i); if(deal_ticket>class="num">0) class=class="str">"cmt">// 在缓存中获取交易,处理它 {
从成交历史里扒出属于自己的盈亏账
在 MT5 的回测或实盘后处理里,想单独统计某个 EA 的战绩,核心是先用 HistoryDealGetTicket 按索引拿到成交票号,再用 HistoryDealGetString / HistoryDealGetInteger / HistoryDealGetDouble 把字段抠出来。下面这段逻辑只认 order_magic 等于自定义 my_magic 的成交,避免把手动单或其它 EA 的单子混进来。 DEAL_ENTRY 字段决定这笔是开仓还是平仓:只有当 entry_type == DEAL_ENTRY_OUT 时才算一次完整平仓闭环,此时 returns 自增、并用 DEAL_PROFIT 的正负分别累进 profit 和 loss。这样跑完循环,PrintFormat 会直接吐出类似「财务结果中共有 37 个交易. 利润=1240.50 , 亏损= -832.20」的汇总,数字随账户历史变动。 别把正态当圣经:profit 和 loss 是带符号累加,loss 本身是负数,做收益率分母时要取绝对值,否则回撤算出来会虚低。外汇与贵金属杠杆高,这套统计只还原已平仓事实,不代表未来盈亏倾向。
class="type">class="kw">string symbol = HistoryDealGetString(deal_ticket,DEAL_SYMBOL); class="type">class="kw">datetime time = HistoryDealGetInteger(deal_ticket,DEAL_TIME); class="type">class="kw">ulong order = HistoryDealGetInteger(deal_ticket,DEAL_ORDER); class="type">long order_magic = HistoryDealGetInteger(deal_ticket,DEAL_MAGIC); class="type">long pos_ID = HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID); ENUM_DEAL_ENTRY entry_type=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket,DEAL_ENTRY); class=class="str">"cmt">//--- 根据指定的 DEAL_MAGIC 处理交易 if(order_magic==my_magic) { class=class="str">"cmt">//... 所需行为 } class=class="str">"cmt">//--- 根据交易的结果计算利润和亏损 if(entry_type==DEAL_ENTRY_OUT) { class=class="str">"cmt">//--- 增加交易数 returns++; class=class="str">"cmt">//--- 交易结果 class="type">class="kw">double result=HistoryDealGetDouble(deal_ticket,DEAL_PROFIT); class=class="str">"cmt">//--- 把正的结果输入到累计利润中 if(result>class="num">0) profit+=result; class=class="str">"cmt">//--- 把负的结果输入到累计亏损中 if(result<class="num">0) loss+=result; } } else class=class="str">"cmt">// 未能成功尝试获取交易 { PrintFormat("我们无法根据索引 %d 选择交易. 错误 %d", i,GetLastError()); } } class=class="str">"cmt">//--- 输出计算的结果 PrintFormat("财务结果中共有 %d 个交易. 利润=%.2f , 亏损= %.2f", returns,profit,loss); }
「用持仓标识符反查整段交易历史」
HistorySelectByPosition(position_ID) 和 HistorySelect(start, end) 都会把成交与订单填进历史缓存,但前者只认一个条件:记录必须带指定的 POSITION_IDENTIFIER。这个标识符是系统给每个新建仓位的唯一数字,仓位活着的时候恒定不变,即便持仓方向从 POSITION_TYPE_BUY 翻成 POSITION_TYPE_SELL,它也不会变。 未平持仓本质是该品种上一笔或多笔成交叠出来的。所以每笔成交、以及成交背后的订单,都会被打上所属持仓的标识符。只要拿到当前持仓的 ID,就能把改动过它的订单和成交全挖出来,重拼出这条仓位的完整生命周期。 这个函数省掉了自己写循环扫全量历史的麻烦。典型三步:先取对当前持仓的标识符;调 HistorySelectByPosition() 把同 ID 的订单和成交灌进缓存;再按你的逻辑处理这批记录。外汇和贵金属杠杆高,用这套重构历史做复盘时,要留意点差和库存费对成交序列的干扰。
◍ 别急着下结论
MT5 这套交易子系统平台把接口封装得很干净,标准库里的交易类能让你不碰底层细节就写出水平不低的 EA;但真要吃透缓存和历史处理的边界,还是得回到基础函数自己跑一遍。 上面那段代码在实盘终端里跑过:加载 134502 笔历史成交、218740 张历史订单后,终端内存占用从 426 MB 跳到 789 MB,而 MQL 脚本自身只占 1 MB——历史数据才是吃内存的大头。 fxsaber 在 M1 图表、5000 根 K 线、单品种下测过,10 个同步发单 EA 能吃掉 4GB,这种负载下更该考虑 OrderSendAsync 合并或砍掉旧历史重做算法。外汇和贵金属杠杆高、滑点跳空频繁,照搬例子前先在策略测试器里压一遍再上真金。 例子文件里的 wrongworkwithhistory 和 rightworkwithhistory 两个 mq5 直接对比了错误与正确写法,下载下来 diff 一下,比看文档来得快。
class="macro">#define PRINT(A) Print(class="macro">#A + " = " + (class="type">class="kw">string)(A)) class="type">void OnStart() { PRINT(TerminalInfoInteger(TERMINAL_MEMORY_USED)); if (HistorySelect(class="num">0, INT_MAX)) { PRINT(HistoryDealsTotal()); PRINT(HistoryOrdersTotal()); PRINT(MQLInfoInteger(MQL_MEMORY_USED)); PRINT(TerminalInfoInteger(TERMINAL_MEMORY_USED)); } } TerminalInfoInteger(TERMINAL_MEMORY_USED) = class="num">426 HistoryDealsTotal() = class="num">134502 HistoryOrdersTotal() = class="num">218740 MQLInfoInteger(MQL_MEMORY_USED) = class="num">1 TerminalInfoInteger(TERMINAL_MEMORY_USED) = class="num">789