从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)·进阶篇
「把指标搬出 EA 才跑得顺」
EA 只该碰订单系统,原先塞在里面的指标逻辑最好挪出去。高波动时 OnTick 被频繁触发,若 EA 同时跑 C_TimesAndTrade::Update 和 C_IndicatorTradeView::SecureChannelPosition,就会和 OnTimer 里的 VolumeAtPrice 重绘撞车——后者在单跳成交量超阈值时,每约 1 秒把整个价量分布删掉重算重绘,直接拖垮执行。 看 OnTick 这段:在 def_INTEGRATION_WITH_EA 宏下每次跳价都调 TimesAndTrade.Update()。而 Update 内部那行 CopyTicks 会向服务器要全量 tick 并返回成交单号,高亮部分就是隐患源头。 [CODE] void OnTick() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, TradeView.SecureChannelPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]); #ifdef def_INTEGRATION_WITH_EA TimesAndTrade.Update(); #endif } inline void Update(void) { MqlTick Tick[]; MqlRates Rates[def_SizeBuff]; int i0, p1, p2 = 0; int iflag; long lg1; static int nSwap = 0; static long lTime = 0; if (m_ConnectionStatus < 3) return; if ((i0 = CopyTicks(Terminal.GetFullSymbol(), Tick, COPY_TICKS_ALL, m_MemTickTime, def_SizeBuff)) > 0) { // ... The rest of the code... } } #ifdef def_INTEGRATION_WITH_EA void OnTimer() { VolumeAtPrice.Update(); TimesAndTrade.Connect(); } #endif inline virtual void Update(void) { MqlTick Tick[]; int i1, p1; if (macroCheckUsing == false) return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0) { if (m_Infos.CountInfos == 0) [/CODE] 逐行拆一下关键处:
#ifdef def_INTEGRATION_WITH_EA/TimesAndTrade.Update();:编译开关打开时,每 tick 强制更新交易时间轴,无脑发服务器请求。CopyTicks(Terminal.GetFullSymbol(), Tick, COPY_TICKS_ALL, m_MemTickTime, def_SizeBuff):拉全量 tick,高亮行就是高波动期请求堆积点。OnTimer里VolumeAtPrice.Update():定时触发价量重绘,和 OnTick 重合就卡顿。CopyTicksRange(..., COPY_TICKS_TRADE, m_Infos.memTimeTick):只取成交 tick,但重绘仍可能每 1 秒发生一次。
OnTradeTransaction 比 OnTrade 更灵:服务器主动通知成交,不用自己轮询持仓。旧版处理代码疯狂产生无用调用,模拟账户跑下来找不到消息范式,但靠资产名一致性校验把废消息挡掉一大半。绿色高亮行在订单转持仓或平仓时删指标(净持模式),红色行核对响应资产名再转发,无用调用骤减。外汇贵金属程序化高风险,这种改动只降低卡顿概率,不保证滑点可控。
class="type">void OnTick() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, class="num">0, TradeView.SecureChannelPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]); class="macro">#ifdef def_INTEGRATION_WITH_EA TimesAndTrade.Update(); class="macro">#endif } class="kw">inline class="type">void Update(class="type">void) { class="type">MqlTick Tick[]; class="type">MqlRates Rates[def_SizeBuff]; class="type">int i0, p1, p2 = class="num">0; class="type">int iflag; class="type">long lg1; class="kw">static class="type">int nSwap = class="num">0; class="kw">static class="type">long lTime = class="num">0; if (m_ConnectionStatus < class="num">3) class="kw">return; if ((i0 = CopyTicks(Terminal.GetFullSymbol(), Tick, COPY_TICKS_ALL, m_MemTickTime, def_SizeBuff)) > class="num">0) { class=class="str">"cmt">// ... The rest of the code... } } class="macro">#ifdef def_INTEGRATION_WITH_EA class="type">void OnTimer() { VolumeAtPrice.Update(); TimesAndTrade.Connect(); } class="macro">#endif class="kw">inline class="kw">virtual class="type">void Update(class="type">void) { class="type">MqlTick Tick[]; class="type">int i1, p1; if (macroCheckUsing == false) class="kw">return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > class="num">0) { if (m_Infos.CountInfos == class="num">0)
成交与挂单如何实时喂给指标层
在 MT5 的 EA 或指标里,OnTradeTransaction 是捕捉账户动作的核心回调。只处理当前图表品种(Terminal.GetSymbol())的事件,避免其他符号的干扰,这是多品种同开终端时少踩坑的第一步。 下面这段逻辑用宏 def_IsBuy 把四种买单类型(市价买、买限、买停、买停限)归为一类,方便后续统一判断方向;trans.order 为 0 时回退到 trans.position 取 ticket,覆盖成交与挂单两种源头。 TRADE_TRANSACTION_DEAL_ADD 与 ORDER_ADD 共用分支,调用 TradeView.IndicatorInfosAdd 把 ticket 塞进可视化容器,再用 UpdateInfosIndicators 写入价格、TP、SL、成交量与方向。ORDER_DELETE 时若 order 不等于 position 就直接移除挂单标记;否则刷新持仓信息,并检测 PositionSelectByTicket 失效后清理残留指标——这一行能避免平仓后箭头滞留图表的概率。 外汇与贵金属杠杆高,这类实时映射若漏掉删除分支,图形对象会越积越多拖慢终端,建议开 MT5 用脚本故意挂单再撤单验证一次。
class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction &trans, class="kw">const class="type">MqlTradeRequest &request, class="kw">const class="type">MqlTradeResult &result) { class="macro">#define def_IsBuy(A) ((A == ORDER_TYPE_BUY_LIMIT) || (A == ORDER_TYPE_BUY_STOP) || (A == ORDER_TYPE_BUY_STOP_LIMIT) || (A == ORDER_TYPE_BUY)) class="type">class="kw">ulong ticket; if (trans.symbol == Terminal.GetSymbol()) class="kw">switch (trans.type) { case TRADE_TRANSACTION_DEAL_ADD: case TRADE_TRANSACTION_ORDER_ADD: ticket = trans.order; ticket = (ticket == class="num">0 ? trans.position : ticket); TradeView.IndicatorInfosAdd(ticket); TradeView.UpdateInfosIndicators(class="num">0, ticket, trans.price, trans.price_tp, trans.price_sl, trans.volume, (trans.position > class="num">0 ? trans.deal_type == DEAL_TYPE_BUY : def_IsBuy(trans.order_type))); class="kw">break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order != trans.position) TradeView.RemoveIndicator(trans.order); else TradeView.UpdateInfosIndicators(class="num">0, trans.position, trans.price, trans.price_tp, trans.price_sl, trans.volume, trans.deal_type == DEAL_TYPE_BUY); if (!PositionSelectByTicket(trans.position)) TradeView.RemoveIndicator(trans.position); } }
◍ 用 OnTradeTransaction 接管挂单与持仓的界面刷新
在 MT5 里想让自定义指标实时跟住交易动作,核心入口是 OnTradeTransaction 回调。它拿到 MqlTradeTransaction、MqlTradeRequest、MqlTradeResult 三件套,你可以借此判断哪些事件该重绘、哪些该清掉。 下面这段用宏 def_IsBuy 把四种买入类订单类型(ORDER_TYPE_BUY_LIMIT / BUY_STOP / BUY_STOP_LIMIT / ORDER_TYPE_BUY)归并成布尔,避免在每个 case 里写一长串或判断。注意宏在函数尾用 #undef 撤掉,防止污染其他编译单元。 请求类事件(trans.type == TRADE_TRANSACTION_REQUEST 且 result.retcode == TRADE_RETCODE_DONE)才进 switch:挂单 TRADE_ACTION_PENDING 调 IndicatorAdd,改挂单 TRADE_ACTION_MODIFY 和改仓 TRADE_ACTION_SLTP 都走 UpdateIndicators,成交与删单则 RemoveIndicator。历史追加(TRADE_TRANSACTION_HISTORY_ADD)且同品种时直接 RemoveIndicator(position)。 另一处 trans.type 非请求的分支里,ORDER_UPDATE 与 POSITION 走 UpdateInfosIndicators,用 trans 里的 price / tp / sl / volume 填视图。外汇与贵金属杠杆高,回测前先在策略测试器用 0.01 手验证这些刷新调用不会在高频重绘时卡死主图。
class="kw">break; case TRADE_TRANSACTION_ORDER_UPDATE: TradeView.UpdateInfosIndicators(class="num">0, trans.order, trans.price, trans.price_tp, trans.price_sl, trans.volume, def_IsBuy(trans.order_type)); class="kw">break; case TRADE_TRANSACTION_POSITION: TradeView.UpdateInfosIndicators(class="num">0, trans.position, trans.price, trans.price_tp, trans.price_sl, trans.volume, trans.deal_type == DEAL_TYPE_BUY); class="kw">break; } class="macro">#undef def_IsBuy } class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction &trans, class="kw">const class="type">MqlTradeRequest &request, class="kw">const class="type">MqlTradeResult &result) { class="macro">#define def_IsBuy(A) ((A == ORDER_TYPE_BUY_LIMIT) || (A == ORDER_TYPE_BUY_STOP) || (A == ORDER_TYPE_BUY_STOP_LIMIT) || (A == ORDER_TYPE_BUY)) if (trans.type == TRADE_TRANSACTION_HISTORY_ADD) if (trans.symbol == Terminal.GetSymbol()) TradeView.RemoveIndicator(trans.position); if (trans.type == TRADE_TRANSACTION_REQUEST) if ((request.symbol == Terminal.GetSymbol()) && (result.retcode == TRADE_RETCODE_DONE)) class="kw">switch (request.action) { case TRADE_ACTION_PENDING: TradeView.IndicatorAdd(request.order); class="kw">break; case TRADE_ACTION_SLTP: TradeView.UpdateIndicators(request.position, request.tp, request.sl, request.volume, def_IsBuy(request.type)); class="kw">break; case TRADE_ACTION_DEAL: TradeView.RemoveIndicator(request.position); class="kw">break; case TRADE_ACTION_REMOVE: TradeView.RemoveIndicator(request.order); class="kw">break; case TRADE_ACTION_MODIFY: TradeView.UpdateIndicators(request.order, request.tp, request.sl, request.volume, def_IsBuy(request.type)); class="kw">break; }
「历史成交与下单请求的分流处理」
这段代码片段出现在交易事件回调的收尾阶段,先以 #undef 取消之前定义的 def_IsBuy 宏,避免宏污染后续逻辑。 当 trans.type 等于 TRADE_TRANSACTION_HISTORY_ADD 且 trans.symbol 与当前终端品种一致时,直接调用 TradeView.RemoveIndicator(trans.position) 把该持仓对应的指标画线清除,说明历史成交入库即意味着仓位已不在活跃列表。 另一分支处理 TRADE_TRANSACTION_REQUEST:仅当 request.symbol 匹配本品种且 result.retcode 为 TRADE_RETCODE_DONE 时,才进入 switch(request.action) 按动作类型细化。背景标红的两层 if 把「非本品种」和「未成交」的请求挡在开关之外,减少无谓分支。 在 MT5 里接 OnTradeTransaction 时,可照搬这个「先按 transaction 类型、再按 symbol 过滤」的双层判断,把指标清理和下单后续动作解耦,避免每次成交都全量重绘。
class="macro">#undef def_IsBuy } if (trans.type == TRADE_TRANSACTION_HISTORY_ADD) if (trans.symbol == Terminal.GetSymbol()) TradeView.RemoveIndicator(trans.position); if (trans.type == TRADE_TRANSACTION_REQUEST) if ((request.symbol == Terminal.GetSymbol()) && (result.retcode == TRADE_RETCODE_DONE)) class="kw">switch (request.action) { class=class="str">"cmt">//... inner code ... }
给幻影指标单独编索引,砍掉冗余对象
交易视图类早先会在图表上堆出大量由终端托管的对象,其中多数从不被实际用到。EA 持续创建并保留这些无用对象,会拖慢整体运行效率,尤其在外汇与贵金属这种高波动、需快速响应的品种上,前端卡顿可能让你错过挂单位置微调的时机。 这次改动的核心,是给“幻影”(即服务器视角下的预移价格投影)单独分配索引 def_IndicatorGhost = 2,并把原选择结构换成带 eIndicatorTrade 枚举和买卖/日内标记的结构体。宏 macroMountName 据此拼接对象名,当 ticket 小于等于 2 时,事件字符自动 +32 偏移,从而把幻影指标和真实挂单、指标 0 在命名层彻底分开。 价格读写宏 macroSetLinePrice / macroGetLinePrice 现在直接走宏替换,不再重复写 ObjectSetDouble 调用。IndicatorAdd 被削掉创建挂单与指标 0 的片段,改由 UpdateIndicators 里一处高亮检查接管——只有非幻影才允许进原流程,幻影改在别处生成。 鼠标移动事件里也加了判断:操控幻影时拦截位移片段,落点后整组幻影对象从列表移除。想验证的话,开 MT5 加载改完的 C_IndicatorTradeView,用脚本触发一次幻影平移,再打开对象列表数一遍——应比旧版少掉至少挂单与 0 号两条常驻项。
class="kw">struct st00 { eIndicatorTrade it; class="type">bool bIsBuy, bIsDayTrade; class="type">class="kw">ulong ticket; class="type">class="kw">double vol, pr, tp, sl; }m_Selection; class="macro">#define def_IndicatorGhost class="num">2 class="macro">#define macroMountName(ticket, it, ev) StringFormat("%s%c%llu%c%c%c%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, def_SeparatorInfo, \ (class="type">char)it, def_SeparatorInfo, \ (class="type">char)(ticket <= def_IndicatorGhost ? ev + class="num">32 : ev)) class="macro">#define macroSetLinePrice(ticket, it, price) ObjectSetDouble(Terminal.Get_ID(), macroMountName(ticket, it, EV_LINE), OBJPROP_PRICE, price) class="macro">#define macroGetLinePrice(ticket, it) ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, it, EV_LINE), OBJPROP_PRICE) class="macro">#define macroCreateIndicator(A, B, C, D) { \ m_TradeLine.Create(ticket, sz0 = macroMountName(ticket, A, EV_LINE), C);
◍ 挂单面板的控件尺寸与分支
这段逻辑负责在图表上挂载交易面板时,按控件类型生成背景、编辑框与按钮,并依据「是否是结果型指标」走不同分支。 m_BackGround.Create 先用 macroMountName 拼出唯一名,再建背景;随后 Size 按 A 的值切尺寸:结果型给 84×34,其余给 92×22。 m_EditInfo1 始终创建,宽高锁在 60×14,用来显示基础信息文本。 当 A != IT_RESULT 时,额外建一个 Wingdings 字体、字符 'u'、字号 17 的 m_BtnMove 移动按钮,尺寸 21×23;否则改建 m_EditInfo2 利润编辑框,同样 60×14。外汇与贵金属波动剧烈,面板参数仅辅助盯盘,实际下单仍受滑点与市场流动性影响,可能产生预期外偏差。 直接把下面代码贴进 MT5 的面板类方法里,改 A 的枚举值就能看到背景块在 84×34 与 92×22 之间切换。
m_BackGround.Create(ticket, sz0 = macroMountName(ticket, A, EV_GROUND), B); m_BackGround.Size(sz0, (A == IT_RESULT ? class="num">84 : class="num">92), (A == IT_RESULT ? class="num">34 : class="num">22)); m_EditInfo1.Create(ticket, sz0 = macroMountName(ticket, A, EV_EDIT), D, class="num">0.0); m_EditInfo1.Size(sz0, class="num">60, class="num">14); if (A != IT_RESULT) { m_BtnMove.Create(ticket, sz0 = macroMountName(ticket, A, EV_MOVE), "Wingdings", "u", class="num">17, C); m_BtnMove.Size(sz0, class="num">21, class="num">23); }else { m_EditInfo2.Create(ticket, sz0 = macroMountName(ticket, A, EV_PROFIT), clrNONE, class="num">0.0); m_EditInfo2.Size(sz0, class="num">60, class="num">14); } class="type">void CreateIndicator(class="type">class="kw">ulong ticket, eIndicatorTrade it) { class="type">class="kw">string sz0;