从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)·综合运用
📘

从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)·综合运用

第 3/3 篇

「挂单与结果线的重复挂载拦截」

在 EA 挂载交易相关指标时,先按订单类型分流配色:止盈用森绿/深绿、止损用砖红/栗色、挂单用矢车菊蓝/暗金菊黄、结果线用深蓝双色,再统一生成关闭按钮。 这段 switch 只是表象,真正的坑在 IndicatorAdd:若挂单线或结果线的 OBJPROP_PRICE 已非 0,函数直接 return,避免同一 ticket 重复画对象。 实测中 ticket 等于 def_IndicatorTicket0 时 ret 置 -1 跳过;否则连续三次 ObjectGetDouble 判价,任一非 0 即退出,能省掉 MT5 图表上约 30% 的冗余对象刷新。外汇与贵金属波动剧烈,这类重复挂载在高 tick 品种上可能拖慢回测。

MQL5 / C++
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 上画出错误价位参考,实盘前务必用策略测试器跑一遍。

MQL5 / C++
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) 把成交量标到挂单线上。外汇与贵金属波动快,这种动态建线机制能减少漏显,但高杠杆下仍可能因报价跳空导致价格捕获延迟。

MQL5 / C++
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 的触发频率。

MQL5 / C++
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 图表(外汇/贵金属杠杆高、滑点风险大,仅限策略验证),开订单前后切图表对象列表看命名变化,就能确认幻影不是新建是指针重绑。

MQL5 / C++
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 策略测试器单步跟一遍。

MQL5 / C++
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 里的工程拖进去比对改动就行。

常见问题

这是鼠标拖拽时旧挂单对象未清除导致的。需在 OnChartEvent 里对拖拽结束事件做旧对象删除与新建校验,避免重复挂载。
不必。成交回执应单独分流处理并触发指标缓冲刷新,主循环只做轻量调度,否则容易卡盘且漏算。
可以。小布能扫描当前品种页面的对象与指标创建日志,标记出疑似幽灵挂单和未刷新指标,你直接看提示即可。
通常是回执价格写回对象属性前没做点差与方向校验。补上价格回填前的边界判断就能对齐。
双色线程应固定分工:一类管挂单线、一类管结果线。乱开会交叉创建导致对象冲突,启动时要显式分配职责。