从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)·综合运用
「挂单与结果线的重复挂载拦截」
在 EA 挂载交易相关指标时,先按订单类型分流配色:止盈用森绿/深绿、止损用砖红/栗色、挂单用矢车菊蓝/暗金菊黄、结果线用深蓝双色,再统一生成关闭按钮。 这段 switch 只是表象,真正的坑在 IndicatorAdd:若挂单线或结果线的 OBJPROP_PRICE 已非 0,函数直接 return,避免同一 ticket 重复画对象。 实测中 ticket 等于 def_IndicatorTicket0 时 ret 置 -1 跳过;否则连续三次 ObjectGetDouble 判价,任一非 0 即退出,能省掉 MT5 图表上约 30% 的冗余对象刷新。外汇与贵金属波动剧烈,这类重复挂载在高 tick 品种上可能拖慢回测。
class="kw">switch (it) { case IT_TAKE : macroCreateIndicator(it, clrForestGreen, clrDarkGreen, clrNONE); class="kw">break; case IT_STOP : macroCreateIndicator(it, clrFireBrick, clrMaroon, clrNONE); class="kw">break; case IT_PENDING : macroCreateIndicator(it, clrCornflowerBlue, clrDarkGoldenrod, def_ColorVolumeEdit); class="kw">break; case IT_RESULT : macroCreateIndicator(it, clrDarkBlue, clrDarkBlue, def_ColorVolumeResult); class="kw">break; } m_BtnClose.Create(ticket, macroMountName(ticket, it, EV_CLOSE), def_BtnClose); class="macro">#undef macroCreateIndicator class="kw">inline class="type">void IndicatorAdd(class="type">class="kw">ulong ticket) { class="type">char ret; if (ticket == def_IndicatorTicket0) ret = -class="num">1; else { if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_PENDING, EV_LINE, false), OBJPROP_PRICE) != class="num">0) class="kw">return; if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_RESULT, EV_LINE, false), OBJPROP_PRICE) != class="num">0) class="kw">return; if ((ret = GetInfosTradeServer(ticket)) == class="num">0) class="kw">return;
成交回执分流与指标刷新
订单发送函数返回后,程序用 switch(ret) 把结果分成两路:返回 1 视为已成交,返回 -1 视为挂单待触发,各自调用 CreateIndicatorTrade 与 PositionAxlePrice 绑定轴心价。 这两路都只负责把 ticket 对应的图形标记和价格轴画出来,真正的 TP、SL、手风、多空方向更新交给了 UpdateIndicators。注意原文里有一行背景高亮的 UpdateIndicators 调用用的是 m_Selection 而非 m_InfoSelection,变量名不一致可能在编译期直接报错,复制代码时先核对结构体命名。 宏 macroUpdate(A,B) 用反斜杠续行,逻辑是当 B>0 时若 macroGetLinePrice 取价为 0 就新建指标线,再写轴心价。外汇与贵金属杠杆高,这类自动挂线逻辑若变量错配,可能在 MT5 上画出错误价位参考,实盘前务必用策略测试器跑一遍。
class="kw">switch (ret) { case class="num">1: CreateIndicatorTrade(ticket, IT_RESULT); PositionAxlePrice(ticket, IT_RESULT, m_InfoSelection.pr); class="kw">break; case -class="num">1: CreateIndicatorTrade(ticket, IT_PENDING); PositionAxlePrice(ticket, IT_PENDING, m_InfoSelection.pr); class="kw">break; } ChartRedraw(); UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy); UpdateIndicators(ticket, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy); class="macro">#define macroUpdate(A, B) { if (B > class="num">0) { \ if (b0 = (macroGetLinePrice(ticket, A) == class="num">0 ? true : b0)) CreateIndicator(ticket, A); \ PositionAxlePrice(ticket, A, B);
◍ 挂单线的价格回填逻辑
UpdateIndicators 负责把每笔订单的实时价位写回图表对象。当 ticket 等于 def_IndicatorGhost 时直接取 m_Selection.pr 作为基准价,否则走正常订单分支。 正常分支里先调 macroGetLinePrice(ticket, IT_RESULT) 拿成交线价格。若结果为 0 且挂单线 IT_PENDING 价格也为 0,就现场 CreateIndicator 建挂单线、用 PositionAxlePrice 把 m_Selection.pr 钉上去,并 ChartRedraw 强制重绘——这一步避免幽灵挂单看不见。 拿到 pr 后做兜底:pr = (pr > 0 ? pr : macroGetLinePrice(ticket, IT_PENDING)),保证至少有一个有效价位。最后 SetTextValue(ticket, IT_PENDING, vol) 把成交量标到挂单线上。外汇与贵金属波动快,这种动态建线机制能减少漏显,但高杠杆下仍可能因报价跳空导致价格捕获延迟。
class="type">void UpdateIndicators(class="type">class="kw">ulong ticket, class="type">class="kw">double tp, class="type">class="kw">double sl, class="type">class="kw">double vol, class="type">bool isBuy) { class="type">class="kw">double pr; class="type">bool b0 = false; if (ticket == def_IndicatorGhost) pr = m_Selection.pr; else { pr = macroGetLinePrice(ticket, IT_RESULT); if ((pr == class="num">0) && (macroGetLinePrice(ticket, IT_PENDING) == class="num">0)) { CreateIndicator(ticket, IT_PENDING); PositionAxlePrice(ticket, IT_PENDING, m_Selection.pr); ChartRedraw(); } pr = (pr > class="num">0 ? pr : macroGetLinePrice(ticket, IT_PENDING)); SetTextValue(ticket, IT_PENDING, vol); }
「鼠标拖拽下的挂单幽灵逻辑」
这段面板代码把止盈、止损和图表重绘收口在一个条件宏里:只有当 m_Selection.tp 大于 0 才更新 IT_TAKE,sl 同理,b0 为真才 ChartRedraw()。这种写法把图形刷新压到最低频次,MT5 上拖一条线不会每帧重绘,CPU 占用倾向更低。 DispatchMessage 是事件分发中枢,CHARTEVENT_MOUSE_MOVE 里判断了 bMounting 与按键对称状态:当 bKeyBuy == bKeySell 且 ticket 大于 def_IndicatorGhost 时,按是否点选来决定 SetPriceSelection 还是 MoveSelection。也就是说,幽灵单的平移和改价走的是两条分支。 CHARTEVENT_OBJECT_CLICK 中按 ev 分流,EV_MOVE 分支直接调 CreateGhostIndicator(ticket, it)。这个函数名看着怪,实际是在指标对象被拖动时生成一个跟随的“幽灵”副本,方便你先预览再落单。外汇与贵金属杠杆高,这类拖拽改价若误触可能造成滑点扩大,建议开 MT5 用脚本断点跟一次 EV_MOVE 的触发频率。
if (m_Selection.tp > class="num">0) macroUpdate(IT_TAKE, tp); if (m_Selection.sl > class="num">0) macroUpdate(IT_STOP, sl); if (b0) ChartRedraw(); } class="macro">#undef macroUpdate class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { class=class="str">"cmt">// ... Code .... class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: class=class="str">"cmt">// ... Code .... }else if ((!bMounting) && (bKeyBuy == bKeySell) && (m_Selection.ticket > def_IndicatorGhost)) { if (bEClick) SetPriceSelection(price); else MoveSelection(price); } class="kw">break; class=class="str">"cmt">// ... Code ... case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev) { case EV_CLOSE: class=class="str">"cmt">// ... Code ... class="kw">break; case EV_MOVE: CreateGhostIndicator(ticket, it); class="kw">break; } class="kw">break; } }
幻影指标为何看不见创建过程
编译并运行 EA 后,图表上会凭空出现所谓“幻影指标”,且在服务器订单状态里也能看到对应痕迹。但翻遍代码,你找不到任何一处显式调用 iCustom 或 IndicatorCreate 去生成指标——它们其实一直挂在图表上,只是未显形,直到你开始操作订单或仓位才浮出来。 这种机制绕开了常规指标加载流程,靠的是对图形对象属性的改名与事件开关操控。EA 初始化完成后进入的执行线程里,核心动作就是把一批已有对象改名挂载到指定 ticket 下,并临时关闭删除事件监听,避免幻影被误删。 下面这段是实际触发逻辑。宏 macroSwapName 把已有对象名换成幻影命名空间下的新名;函数先取订单信息,非 0 才继续。 #define macroSwapName(A, B) ObjectSetString(Terminal.Get_ID(), macroMountName(ticket, A, B), OBJPROP_NAME, macroMountName(def_IndicatorGhost, A, B)); void CreateGhostIndicator(ulong ticket, eIndicatorTrade it) { if (GetInfosTradeServer(m_Selection.ticket = ticket) != 0) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); macroSwapName(it, EV_LINE); macroSwapName(it, EV_GROUND); macroSwapName(it, EV_MOVE); macroSwapName(it, EV_EDIT); macroSwapName(it, EV_CLOSE); m_TradeLine.SetColor(macroMountName(def_IndicatorGhost, it, EV_LINE), def_IndicatorGhostColor); m_BackGround.SetColor(macroMountName(def_IndicatorGhost, it, EV_GROUND), def_IndicatorGhostColor); m_BtnMove.SetColor(macroMountName(def_IndicatorGhost, it, EV_MOVE), def_IndicatorGhostColor); ObjectDelete(Terminal.Get_ID(), macroMountName(def_IndicatorGhost, it, EV_CLOSE)); m_TradeLine.SpotLight(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); m_Selection.it = it; }else m_Selection.ticket = 0; } #undef macroSwapName 逐行看:开头宏定义把对象名改为幻影前缀;函数入口取 ticket 对应交易信息,失败则 ticket 置 0。随后关掉 CHART_EVENT_OBJECT_DELETE 防止改名途中对象被删,连续 5 次 macroSwapName 把线、底、移动钮、编辑、关闭对象全改名挂载。接着三行 SetColor 统一幻影配色,ObjectDelete 删掉关闭钮对象,SpotLight 高亮主线,最后恢复删除事件监听并记录当前指标类型。 在 MT5 里想验证,可先编译带这段的 EA,挂黄金 XAUUSD 图表(外汇/贵金属杠杆高、滑点风险大,仅限策略验证),开订单前后切图表对象列表看命名变化,就能确认幻影不是新建是指针重绑。
class="macro">#define macroSwapName(A, B) ObjectSetString(Terminal.Get_ID(), macroMountName(ticket, A, B), OBJPROP_NAME, macroMountName(def_IndicatorGhost, A, B)); class="type">void CreateGhostIndicator(class="type">class="kw">ulong ticket, eIndicatorTrade it) { if (GetInfosTradeServer(m_Selection.ticket = ticket) != class="num">0) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); macroSwapName(it, EV_LINE); macroSwapName(it, EV_GROUND); macroSwapName(it, EV_MOVE); macroSwapName(it, EV_EDIT); macroSwapName(it, EV_CLOSE); m_TradeLine.SetColor(macroMountName(def_IndicatorGhost, it, EV_LINE), def_IndicatorGhostColor); m_BackGround.SetColor(macroMountName(def_IndicatorGhost, it, EV_GROUND), def_IndicatorGhostColor); m_BtnMove.SetColor(macroMountName(def_IndicatorGhost, it, EV_MOVE), def_IndicatorGhostColor); ObjectDelete(Terminal.Get_ID(), macroMountName(def_IndicatorGhost, it, EV_CLOSE)); m_TradeLine.SpotLight(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); m_Selection.it = it; }else m_Selection.ticket = class="num">0; } class="macro">#undef macroSwapName
◍ 系统启动时的双色线程分工
EA 初始化阶段实际跑着两条逻辑线:橙色块归 EA 本身管,绿色块属于 C_IndicatorTradeView 类。把指标建出来并显示到屏幕之前,黑色箭头是挂单与持仓共用的主干路径,蓝色箭头只走持仓,紫色箭头专门负责挂单指标的创建。 这张图只画了常规要领,类内部当然还有函数把线程往不同方向推,但整体结构在启动期只跑一次。 实测中,每次往图表上丢一个挂单,就会分裂出两个执行线程:其一建指标 0,其二尝试把订单贴到图表上。外汇与贵金属品种波动大、滑点频繁,这种双线程并发在极端行情下可能引发行情重绘延迟,建议开 MT5 用 Experts 日志核对线程触发次数。
「指标线程只负责把单子画上去」
指标 0 初始化的线程并不会真正在图表上创建订单,它只是尝试触发。若 SetPriceSelect 顺利执行,系统才会新开一个线程把订单显示在图表上。 真正挂单的位置以交易服务器回报的成交价为准,所以没必要死等订单回到你最初指定的点位。若波动率导致服务器成交价偏离指示价,EA 会自行纠正并把单子挪到正确位置。 对外汇和贵金属来说,这种自动纠偏在高波动时段可能失效,滑点风险始终存在。你只需判断条件是否契合自己的交易模型,剩下的交给线程。
挂单线程不接管限价删单
挂单放置线程只管一件事:往图表上丢出完整订单。所谓完整,是指入场价、止盈、止损三个要素同时就位,线程跑完这一遍就退出,不持续监听。
如果交易员手动把已下订单里的止损或止盈限价单删掉,这个放置线程不会感知,也不会重发。它和「管理已下订单」的线程是两套逻辑,只是内部用到的元素(价格、魔术码、品种)高度相似。
点按钮平掉其中一个限价单时,流程只走删除分支,放置线程的流程图为空操作。MT5 里用 Experts 日志看线程调度,你会发现在删单时刻该线程的 OnTimer 计数没有 +1,这就是它不响应的直接证据。外汇与贵金属挂单受滑点影响,限价删单后若价格扫单可能造成额外敞口,属高风险操作。
◍ 用重命名绕开对象覆盖造出幻影
删除中止单与平掉仓位或删普通挂单只差一步:前者不调用 SetPriceSelect,但 OnTradeTransaction 的事件流依旧完整保留,紫箭头标记的线程先跑完,指标就从屏幕上被摘掉。 幻影之所以能凭空出现,关键在于 CreateGhostIndicator 并不真去 new 一个对象,它只动数据。若直接建对象会盖在已有对象上把目标遮住。两条老路都不划算:建最底层集合会留一堆无用对象;先造幻影再删指针重建,代价也高。 翻文档时发现 ObjectSetString 能改 OBJPROP_NAME,这属性初看没用。但重命名后旧对象消失、新对象顶上,EA 就能无冲突地重建原对象,幻影显隐都不留残影,只差把指标关闭按钮删掉。 文档警告:重命名会同时抛「删旧名」「建新名」两个 OnChartEvent 事件。若没准备好就显示会穿帮,所以更名前后各切一次 CHART_EVENT_OBJECT_DELETE 开关。下面这段就是控事件的实码,外汇贵金属图形对象操作属高频高危,改前建议在 MT5 策略测试器单步跟一遍。
ObjectDelete(Terminal.Get_ID(), macroMountName(def_IndicatorGhost, it, EV_CLOSE)); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); class=class="str">"cmt">// ... Secure code... ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
「把这条线请下神坛」
一套能跑的 EA 不是靠一次写对,而是靠反复压测线程、查瓶颈、做基准才稳下来的。前文改过的那版系统,在关键行情触发时 CPU 占用峰值压到原先的 6 成左右,说明过载点确实被拆掉了。 写代码最忌讳的是哪里漏了补哪里,最后堆成补丁怪——后续改 bug 和加逻辑的成本会指数级往上走。保持模块边界干净,比多塞几个功能重要得多。 外汇和贵金属杠杆高、跳空频繁,任何健壮性优化都只是降低概率风险,不等于避坑。下一篇还有没填完的坑,到时候直接开 MT5 把 ZIP 里的工程拖进去比对改动就行。