构建自动运行的 EA(第 12 部分):自动化(IV)·进阶篇
(2/3)· 每日交易量上限背后那处看似无害、却能让 100% 自动化 EA 直接失控的缺陷
「挂单状态同步的底层钩子」
在 EA 里跟踪挂单,不能靠定时器轮询,得直接挂到交易事务回调上。下面这段把 ORDER_UPDATE 事件接到了 manager.UpdatePending,订单改价、改手数时结构体内的缓存才会跟着刷。 杠杆字段用当前体积除以终端最小体积算出整数倍:(uint)(OrderGetDouble(ORDER_VOLUME_CURRENT)/GetTerminalInfos().VolMinimal),这一步在微型账户上容易因 VolMinimal 为 0.01 而拿到整十倍数值,复制时先打印验证。 方向判断把四种买类订单全归到 IsBuy,卖类同理留空。外汇与贵金属杠杆放大波动,挂单被扫或拒的概率随点差跳变而上升,回测和实盘要用不同滑点假设。 OnTradeTransaction 里 ORDER_DELETE 分支用 trans.order==trans.position 区分挂单转成仓还是纯删挂单,逻辑若写反会在部分券商成交回报延迟时漏更持仓。
m_Pending.Leverage = (class="type">uint)(OrderGetDouble(ORDER_VOLUME_CURRENT) / GetTerminalInfos().VolMinimal); m_Pending.IsBuy = ((eLocal == ORDER_TYPE_BUY) || (eLocal == ORDER_TYPE_BUY_LIMIT) || (eLocal == ORDER_TYPE_BUY_STOP) || (eLocal == ORDER_TYPE_BUY_STOP_LIMIT)); m_Pending.PriceOpen = OrderGetDouble(ORDER_PRICE_OPEN); m_Pending.SL = OrderGetDouble(ORDER_SL); m_Pending.TP = OrderGetDouble(ORDER_TP); } class="type">void UpdatePending(class="kw">const class="type">class="kw">ulong ticket) { if ((ticket == class="num">0) || (ticket != m_Pending.Ticket) || (m_Pending.Ticket == class="num">0)) class="kw">return; if (OrderSelect(m_Pending.Ticket)) SetInfoPending(); } class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction &trans, class="kw">const class="type">MqlTradeRequest &request, class="kw">const class="type">MqlTradeResult &result) { class="kw">switch (trans.type) { case TRADE_TRANSACTION_POSITION: manager.UpdatePosition(trans.position); class="kw">break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order == trans.position) (*manager).PendingToPosition(); else (*manager).UpdatePosition(trans.position); class="kw">break; case TRADE_TRANSACTION_ORDER_UPDATE: (*manager).UpdatePending(trans.order); class="kw">break;
成交回执里怎么同步持仓状态
EA 在 OnTradeTransaction 里拿到 TRADE_TRANSACTION_REQUEST 事件后,不能无脑刷新,要先过滤是不是自己下的单。下面这段逻辑只处理当前品种、魔数匹配且回执码为 TRADE_RETCODE_DONE 的请求,再按 action 类型分流。 市价成交(TRADE_ACTION_DEAL)和改止盈止损(TRADE_ACTION_SLTP)都走 UpdatePosition,前者用 request.order、后者用 trans.position 作定位;撤掉挂单(TRADE_ACTION_REMOVE)则调 EraseTicketPending 清掉本地 ticket 缓存。这样本地持仓映射和终端实际状态偏差概率更低。 启动阶段也别信历史缓存。LoadPositionValid 用 PositionGetTicket 从尾到头倒序扫 PositionsTotal(),碰到 ticket 为 0 或非本品种就 continue,只把真实存在的本符号持仓载入 manager。外汇和贵金属杠杆高,实盘跑前建议在策略测试器用不同魔数多 EA 同图验证一次事件过滤是否串号。
case TRADE_TRANSACTION_REQUEST: if ((request.symbol == _Symbol) && (result.retcode == TRADE_RETCODE_DONE) && (request.magic == def_MAGIC_NUMBER)) class="kw">switch (request.action) { case TRADE_ACTION_DEAL: (*manager).UpdatePosition(request.order); class="kw">break; case TRADE_ACTION_SLTP: (*manager).UpdatePosition(trans.position); class="kw">break; case TRADE_ACTION_REMOVE: (*manager).EraseTicketPending(request.order); class="kw">break; } class="kw">break; } class="kw">inline class="type">void LoadPositionValid(class="type">void) { class="type">class="kw">ulong value; for (class="type">int c0 = PositionsTotal() - class="num">1; (c0 >= class="num">0) && (_LastError == ERR_SUCCESS); c0--) { if ((value = PositionGetTicket(c0)) == class="num">0) class="kw">continue; if (PositionGetString(POSITION_SYMBOL) != _Symbol) class="kw">continue;
◍ 持仓遍历与挂单创建的魔术码过滤
EA 在扫描账户持仓时,第一道关卡就是魔术码比对:只有 PositionGetInteger(POSITION_MAGIC) 等于本 EA 的 GetMagicNumber() 才进入后续处理,否则直接 continue 跳过。这一步能避免把手动单或其它 EA 的仓位误当成自己的仓位去平仓或统计。
当账户处于对冲模式(m_bAccountHedging 为真)且已有一张挂单 ticket(m_TicketPending > 0)时,代码会调用 C_Orders::ClosePosition(value) 把当前遍历到的持仓平掉并 continue,这常用于先清场再下新单的逻辑。若不是对冲场景,则检查 m_Position.Ticket:大于 0 说明已有持仓却还想建仓,直接 SetUserError(ERR_Unknown) 报错;否则把 value 记进 m_Position.Ticket,刷新持仓信息并把当前杠杆存到 m_StaticLeverage。
真正下单走 CreateOrder():先 IsPossible(true) 做前置校验,不通过直接返回 false;通过后才用 C_Orders::CreateOrder 发单,其中止损止盈金额取决于 m_InfosManager.IsOrderFinish——未完成订单才带 FinanceStop / FinanceTake,杠杆与日内交易标志也从 m_InfosManager 取。外汇与贵金属杠杆可调,但都是高风险品种,参数乱设可能瞬间放大回撤。
把下面这段贴进 MT5 策略测试器,改 GetMagicNumber() 返回值就能看到不同魔术码下单是否被隔离:
if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) class="kw">continue; if ((m_bAccountHedging) && (m_TicketPending > class="num">0)) { C_Orders::ClosePosition(value); class="kw">continue; } if (m_Position.Ticket > class="num">0) SetUserError(ERR_Unknown); else { m_Position.Ticket = value; SetInfoPositions(); m_StaticLeverage = m_Position.Leverage; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price) { class="type">bool bRet = class="kw">false; if (!IsPossible(true)) class="kw">return bRet; m_Pending.Ticket = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
「挂单与市价单的票号回填逻辑」
这段实现里,挂单和市价单在开仓后都做了一次票号有效性校验。若 m_Pending.Ticket 大于 0,才调用 OrderSelect 选中该挂单,选中成功再刷新挂单信息结构体,否则直接返回 false,避免后续对空票号误操作。 市价单走 ToMarket 时,先过 IsPossible(false) 这道闸门,不通过就原样返回。真正下单后把返回的 tmp 票号写进 m_Position.Ticket;在锁仓账户(m_bAccountHedging 为 true)中直接采用 tmp,非锁仓账户则优先保留已有持仓票号,仅在原票号为 0 时才接 tmp。 选中持仓用 PositionSelectByTicket,失败就 ZeroMemory 清空 m_Position 结构体。外汇与贵金属杠杆交易高风险,票号回填失败可能意味着下单未确认,应在 MT5 策略测试器里打印 m_Position.Ticket 验证分支是否按账户类型走对。 GetVolumeInPosition 直接返回 m_Position.Leverage,这个命名容易误读——它吐出的是持仓杠杆字段而非手数,复制代码时注意别把 Leverage 当成交量用。
if (m_Pending.Ticket > class="num">0) bRet = OrderSelect(m_Pending.Ticket); if (bRet) SetInfoPending(); class="kw">return bRet; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ToMarket(class="kw">const ENUM_ORDER_TYPE type) { class="type">class="kw">ulong tmp; class="type">bool bRet = class="kw">false; if (!IsPossible(class="kw">false)) class="kw">return bRet; tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > class="num">0 ? m_Position.Ticket : tmp)); if (m_Position.Ticket > class="num">0) bRet = PositionSelectByTicket(m_Position.Ticket); if (!bRet) ZeroMemory(m_Position); class="kw">return bRet; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">const class="type">uint GetVolumeInPosition(class="type">void) class="kw">const { class="kw">return m_Position.Leverage; } class=class="str">"cmt">//+------------------------------------------------------------------+
挂单与市价单的下单封装差异
在 MT5 的 EA 架构里,把下单逻辑收口到两个成员函数能省掉大量重复判断:CreateOrder 负责挂单,ToMarket 负责即时成交,两者都先走 IsPossible 做杠杆与品种可行性校验。 CreateOrder 多接收一个 Price 参数,用于限价或停止单的触发价;若调用方没传 LeverageArg(默认 0),就回落到 m_InfosManager.Leverage 取账户实际杠杆。订单创建后用 OrderSelect 回选票号,成功才写 SetInfoPending 更新挂单状态。 ToMarket 不收价格参数,直接吃 C_Orders::ToMarket 的返回票号;在 hedging 账户下 m_Position.Ticket 直接等于 tmp,而净仓账户若已有持仓票号则保留原值,避免重复开仓。外汇与贵金属杠杆可调到 1:500 以上,穿仓风险极高,回测时建议先用 IsPossible 拦掉超杠杆请求。 下面两段是原函数的骨架,逐行看更清楚封装思路。
class="type">bool CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price, class="kw">const class="type">uint LeverageArg = class="num">0) { class="type">bool bRet = class="kw">false; if (!IsPossible(type, (LeverageArg > class="num">0 ? LeverageArg : m_InfosManager.Leverage))) class="kw">return bRet; m_Pending.Ticket = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); if (m_Pending.Ticket > class="num">0) bRet = OrderSelect(m_Pending.Ticket); if (bRet) SetInfoPending(); class="kw">return bRet; } class="type">bool ToMarket(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">uint LeverageArg = class="num">0) { class="type">class="kw">ulong tmp; class="type">bool bRet = class="kw">false; if (!IsPossible(type, (LeverageArg > class="num">0 ? LeverageArg : m_InfosManager.Leverage))) class="kw">return bRet; tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > class="num">0 ? m_Position.Ticket : tmp));