MetaTrader 5 中的订单、持仓和成交·进阶篇
(2/3)· 搞不清活动订单和历史成交的读取边界,MQL5 交易代码就会在实盘里静默出错
◍ 缓存不刷新,属性函数读到的就是旧账
在 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。
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 做输入,扫全列表只处理匹配魔法的仓。外汇与贵金属杠杆高,持仓字段读错可能触发误平,跑之前先在策略测试器用演示账户验证。
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。
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 建个脚本把起止改成自己账户真实区间,能直观看到耗时差。
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(),这种防御写法在真实账户排查时很实用。
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">// 在缓存中得到历史订单,并处理它 {