EA 交易中采用OnTrade() 函数处理交易事件·综合运用
⚙️

EA 交易中采用OnTrade() 函数处理交易事件·综合运用

(3/3)·从模板搭建到订单持仓联动,把MQL5交易事件一次讲透,省去反复试错

新手友好 第 3/3 篇

很多EA开发者只知道OnTrade()会在交易后触发,却没留意一次市价开仓可能连跑四遍警报,导致通知刷屏、逻辑错乱。本文作为系列收尾,把持仓与挂单各类事件的触发机制和应对方式一并理清,让你写的EA不再误报漏报。

◍ 盯住 TP 被偷偷改掉的仓

在 MT5 的 EA 里,持仓的止盈价(TP)有时会被手动或别的脚本改掉,而主循环未必第一时间察觉。上面这段逻辑的做法是:拿当前持仓列表和上一 tick 的持仓列表做比对,一旦发现某个货币对的 tp 字段和 PrevPositionList[i].tp 不一致,就把变动写进 _alerts 字符串,并置 modify = true。 一旦本次扫描里有任意一条 TP 被改动,modify 为真就会触发 Alert(_alerts) 弹窗,把“货币对 XXX 获利价位从 A 改为 B”逐条播报出来,随后把 modify 复位避免下一 tick 重复报警。 实际验证时可以把这段接在 OnTradeTransaction 之外、放在 OnTick 末尾,用 EURUSD 开两笔 0.01 手限价单,手动改其中一笔的 TP,MT5 终端会在下一个 tick 弹出包含具体旧值新值的提示——外汇与贵金属杠杆高,TP 被动过可能意味着策略外干预,需自行核对风险。

MQL5 / C++
_alerts += "货币对 "+PositionList[i].symbol+" 获利价位从 "+ PrevPositionList[i].tp +" 改为 "+ PositionList[i].tp +"\n";
modify = true;
}
}
if (modify == true)
{
Alert(_alerts);
modify = false;
}
}
if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))

挂单与改单的事件钩子怎么写

新挂单在 MT5 里只触发一次 Trade 事件,抓住这一次就够驱动后续逻辑。先判订单状态是否为 ORDER_STATE_PLACED(已接受),再用 switch 按 ORDER_TYPE 分支弹提示,类型 2~7 分别对应限价买、限价卖、止损买、止损卖、限价止损买、限价止损卖。 订单修改和持仓修改走同一思路:建一个 _orders 结构,字段逐一映射订单属性;再声明 int 型总数变量与 OrderList[] / PrevOrderList[] 两个数组,分别存当前和上次 OnTrade() 的快照。对应的抓取函数要贴在 EA 末尾,OnInit() 与 OnTrade() 末处调用取数,OnTrade() 开头刷新前次快照。 改单处理是一个补在持仓循环后的订单循环,逐单比对本次与前次的 sl / tp。若不一致就写进 _alerts,循环结束用 Alert() 一口气流式弹出。外汇与贵金属杠杆高,这类实时提示只降低漏看概率,不预示行情方向。 下面这段是从挂单捕获到结构声明的核心片段,直接丢进 OnTrade() 操作符主体即可验证:

MQL5 / C++
if (OrdersPrev < OrdersTotal())
if (OrdersPrev < OrdersTotal())
{
OrderGetTicket(OrdersTotal()-class="num">1);class=class="str">"cmt">// 选择最后一笔订单
_GetLastError=GetLastError();
Print("错误 #",_GetLastError);ResetLastError();
class=class="str">"cmt">//--
if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED)
{
Alert(OrderGetTicket(OrdersTotal()-class="num">1),"订单到达,有待处理");
LastOrderTicket = OrderGetTicket(OrdersTotal()-class="num">1);    class=class="str">"cmt">// 保存订单号,未来使用
}
state = OrderGetInteger(ORDER_STATE);
if (state == ORDER_STATE_PLACED)
{
class="kw">switch(OrderGetInteger(ORDER_TYPE))
{
case class="num">2:
Alert("挂限价买单 #", OrderGetTicket(OrdersTotal()-class="num">1)," 被接受!");
break;
case class="num">3:
Alert("挂限价卖单 #", OrderGetTicket(OrdersTotal()-class="num">1)," 被接受!");
break;
case class="num">4:
Alert("挂止损买单 #", OrderGetTicket(OrdersTotal()-class="num">1)," 被接受!");
break;
case class="num">5:
Alert("挂止损卖单 #", OrderGetTicket(OrdersTotal()-class="num">1)," 被接受!");
break;
case class="num">6:
Alert("挂限价止损买单 #", OrderGetTicket(OrdersTotal()-class="num">1)," 被接受!");
break;
case class="num">7:
Alert("挂限价止损卖单  #", OrderGetTicket(OrdersTotal()-class="num">1)," 被接受!");
break;
}
}
}
state = OrderGetInteger(ORDER_STATE);
if (state == ORDER_STATE_PLACED)
{
class=class="str">"cmt">/*
*
* 保存订单信息的结构
*
*/
class="kw">struct _orders
{
class="type">class="kw">datetime time_setup,       class=class="str">"cmt">// 下单时间
time_expiration,  class=class="str">"cmt">// 订单过期时间
time_done;        class=class="str">"cmt">// 订单执行或取消的时间
class="type">long     type,             class=class="str">"cmt">// 订单类型
state,            class=class="str">"cmt">// 订单状态
type_filling,     class=class="str">"cmt">// 剩余的执行类型
type_time,        class=class="str">"cmt">// 订单生命周期
ticket;           class=class="str">"cmt">// 订单号
class="type">long     magic,            class=class="str">"cmt">// 下单的EA幻数
class=class="str">"cmt">// (用处是确保每个EA必须 
class=class="str">"cmt">// 有它自己的唯一编号)
position_id;      class=class="str">"cmt">// 下单的仓位编号
class=class="str">"cmt">// 当它被执行的时候. 每一个被执行的订单都引发
class=class="str">"cmt">// 一个交易,会建立或者修改已有 
class=class="str">"cmt">// 仓位. 仓位的编号 
class=class="str">"cmt">// 当时执行订单.
class="type">class="kw">double volume_initial,     class=class="str">"cmt">// 下单的初始交易量
volume_current,     class=class="str">"cmt">// 未处理交易量
price_open,         class=class="str">"cmt">// 订单中指定的价位
sl,                 class=class="str">"cmt">// 止损价位
tp,                 class=class="str">"cmt">// 获利价位
price_current,      class=class="str">"cmt">// 订单中交易品种的当前价位
price_stoplimit;    class=class="str">"cmt">// 限价止损单被触发时的限价单价位
class="type">class="kw">string symbol;             class=class="str">"cmt">// 下单针对的交易品种

「用订单快照比对抓止损止盈改动」

盯盘时最怕漏看 EA 或手动改了挂单的 SL/TP。这段逻辑的做法是:先各跑一次 GetOrders 把当前和上一 tick 的订单塞进 OrderList 和 PrevOrderList,两帧都存了 ticket、sl、tp 等 20 个字段。 核心比对落在末尾循环:用 _ExpertOrdersTotal 控制遍历长度,只要 PrevOrderList[i].sl 不等于 OrderList[i].sl,就拼一条“订单 xxx 修改止损价位从 a 到 b”的提示串,并把 modify 置真。TP 同理。 实测在 EURUSD 上跑 1 分钟周期,若一秒内连续改两次 SL,警报串会累加两条记录,不会覆盖。外汇与贵金属杠杆高,SL 被错改可能直接放大回撤,这类比对适合接小布告警而不是肉眼盯。 下面这段是上述 GetOrders 与比对的 MQL5 骨架,复制进 EA 的 OnTick 前段即可验证。

MQL5 / C++
class="type">int _ExpertOrdersTotal = class="num">0;
_orders OrderList[],
PrevOrderList[];
class="type">void GetOrders(_orders &OrdersList[])
{
 class="type">int _GetLastError=class="num">0,_OrdersTotal=OrdersTotal();
 class="type">int temp_value=(class="type">int)MathMax(_OrdersTotal,class="num">1);
 ArrayResize(OrdersList,temp_value);
 _ExpertOrdersTotal=class="num">0;
 for(class="type">int z=_OrdersTotal-class="num">1; z>=class="num">0; z--)
 {
  if(!OrderGetTicket(z))
  {
   _GetLastError=GetLastError();
   Print("GetOrders() - Error #",_GetLastError);
   class="kw">continue;
  }
  else
  {
   OrdersList[z].ticket          = OrderGetTicket(z);
   OrdersList[z].time_setup      = OrderGetInteger(ORDER_TIME_SETUP);
   OrdersList[z].time_expiration = OrderGetInteger(ORDER_TIME_EXPIRATION);
   OrdersList[z].time_done       = OrderGetInteger(ORDER_TIME_DONE);
   OrdersList[z].type            = OrderGetInteger(ORDER_TYPE);
   OrdersList[z].state           = OrderGetInteger(ORDER_STATE);
   OrdersList[z].type_filling    = OrderGetInteger(ORDER_TYPE_FILLING);
   OrdersList[z].type_time       = OrderGetInteger(ORDER_TYPE_TIME);
   OrdersList[z].magic           = OrderGetInteger(ORDER_MAGIC);
   OrdersList[z].position_id     = OrderGetInteger(ORDER_POSITION_ID);
   OrdersList[z].volume_initial  = OrderGetDouble(ORDER_VOLUME_INITIAL);
   OrdersList[z].volume_current  = OrderGetDouble(ORDER_VOLUME_CURRENT);
   OrdersList[z].price_open      = OrderGetDouble(ORDER_PRICE_OPEN);
   OrdersList[z].sl              = OrderGetDouble(ORDER_SL);
   OrdersList[z].tp              = OrderGetDouble(ORDER_TP);
   OrdersList[z].price_current   = OrderGetDouble(ORDER_PRICE_CURRENT);
   OrdersList[z].price_stoplimit = OrderGetDouble(ORDER_PRICE_STOPLIMIT);
   OrdersList[z].symbol          = OrderGetString(ORDER_SYMBOL);
   OrdersList[z].comment         = OrderGetString(ORDER_COMMENT);
   _ExpertOrdersTotal++;
  }
 }
 temp_value=(class="type">int)MathMax(_ExpertOrdersTotal,class="num">1);
 ArrayResize(OrdersList,temp_value);
}
GetOrders(PrevOrderList);
GetOrders(OrderList);
for (class="type">int i = class="num">0;i<_ExpertOrdersTotal;i++)
{
 if (PrevOrderList[i].sl != OrderList[i].sl)
 {
  _alerts += "订单 "+OrderList[i].ticket+" 修改止损价位从 "+ PrevOrderList[i].sl +" 到 "+ OrderList[i].sl +"\n";
  modify = true;
 }
 if (PrevOrderList[i].tp != OrderList[i].tp)
 {
  _alerts += "订单 "+OrderList[i].ticket+" 修改获利价位从 "+ PrevOrderList[i].tp +" 到 "+ OrderList[i].tp +"\n";
  modify = true;
 }
}
if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))
{

◍ 画得少,看得清

把交易事件处理收进 MQL5 语言层,订单校验与成交报告能用更少代码跑起来,系统资源占用和源码体积都往下走,这对常年和 EA 维护成本较劲的人是直接利好。 早期版本里止损、止盈平仓原因没法在回调里细分,build 1625 之后补上了 ENUM_DEAL_REASON,DEAL_REASON_SL 和 DEAL_REASON_TP 可在 OnTradeTransaction 里直接识别,老文章没覆盖这段,现在补上就不算知识缺口。 外汇与贵金属杠杆高、滑点随机,事件驱动逻辑只降低开发复杂度,不抵消品种本身风险,参数和过滤条件仍要在 MT5 里实盘前回测。 打开你手头那个 tradecontrol.mq5,对照本文说的事件结构改一版,比反复读文档来得快。

把事件日志交给小布盯盘
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到EA触发与订单变动的实时标注,你只需专注策略本身。

常见问题

因为建仓同时涉及订单、持仓、订单历史和交易历史列表的变更,每次变更都产生交易事件并触发一次调用,需在函数内做去重或状态比对。
挂单修改属于交易事件,会触发OnTrade(),同时订单属性变化也可能伴随订单事件,建议在OnTrade()中统一处理持仓与挂单状态。
可以,小布盯盘的品种页会聚合账户成交与订单变动,EA通过交易事件产生的动作能在时间线上对应查看,便于排查重复执行。
通过对比事件前后的持仓Volume与Direction,volume缩减且无反向为新仓即为部分平仓,方向反转且旧仓消失一般为反向开仓,建议缓存上笔持仓快照做差分。