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

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

(3/3)· 高波动下挂单时有时无、持仓指令失灵?这套速度兼可靠的收口方案给你答案

含代码示例实战向 第 3/3 篇
很多交易者以为 EA 卡顿只是电脑慢,其实多半是事件调度和对象管理没分层。高波动时服务器响应变慢,系统若反复无效轮询,就会在“有单/无单”间反复横跳,逼你误操作。把调用次数压下来,比堆硬件更管用。

挂单面板上的点击交互怎么分流

这段逻辑来自一个挂在图表上的订单管理面板,它接管了 MT5 的 CHARTEVENT_OBJECT_CLICK,把用户在对象上的点击翻译成具体交易动作。 当图表尺寸或缩放变化(CHARTEVENT_CHART_CHANGE)时,只做重绘,不碰仓位;点击对象时才进入 GetIndicatorInfos 解析 sparam、ticket、it、ev 四个字段。 EV_CLOSE 分支里先用 GetInfosTradeServer(ticket) 拿服务端返回码 cRet,非 0 才继续。cRet 小于 0 走 RemoveOrderPendent 删挂单,否则 ClosePosition 平掉持仓——这套判定能避免对已经失效的 ticket 重复发指令。 IT_TAKE 与 IT_STOP 被归到同一处理块:把 ticket 和类型写进 m_InfoSelection,置 bIsMovingSelect 为 true,再调 SetPriceSelection(0)。也就是说,点止盈或止损线不是直接改价,而是先进入“选中可拖拽”状态,实际改价留在鼠标移动事件里。外汇与贵金属杠杆高,这类面板误操作可能瞬间触发平仓,实盘前务必在策略测试器里用历史 tick 跑一遍点击流。

MQL5 / C++
            break;
            case CHARTEVENT_CHART_CHANGE:
                ReDrawAllsIndicator();
                break;
            case CHARTEVENT_OBJECT_CLICK:
                if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev)
                {
                    case EV_CLOSE:
                        if ((cRet = GetInfosTradeServer(ticket)) != class="num">0) class="kw">switch (it)
                        {
                            case IT_PENDING:
                            case IT_RESULT:
                                if (cRet < class="num">0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                                break;
                            case IT_TAKE:
                            case IT_STOP:
                                m_InfoSelection.ticket = ticket;
                                m_InfoSelection.it = it;
                                m_InfoSelection.bIsMovingSelect = true;
                                SetPriceSelection(class="num">0);
                                break;
                        }

「鼠标拖动时的持仓高亮逻辑」

在图表事件回调里,EV_MOVE 分支负责处理鼠标移动或拖拽产生的选中动作。若对象正处于移动选中状态,先调用 SpotLight() 取消高亮并把标志位复位;否则就记录当前 ticket 与迭代器,并试图从交易服务器拉取信息。 这里有个容易踩的坑:条件判断里用了赋值号而非等号——m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0),这行在判断的同时把查询结果写回了标志位。若服务器返回非零(约 0 以外任意值)才视为可移动选中,并顺手用 macroMountName 拼出名称再高亮一次。 开 MT5 把这段塞进你的 C_Graph 事件分发里,故意改一下那个赋值符看看:误用等号会让拖动高亮彻底失效,持仓线在拖拽时可能不再响应。外汇与贵金属品种波动剧烈,这类交互 bug 在实盘高波动时段容易让人误判持仓边界。

MQL5 / C++
              break;
            case EV_MOVE:
              if (m_InfoSelection.bIsMovingSelect)
              {
                m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
              }else
              {
                m_InfoSelection.ticket = ticket;
                m_InfoSelection.it = it;
                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != class="num">0))
                  m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
              }
              break;
            }
            break;
          }
}

◍ DispatchMessage 里鼠标与指标的交互细节

DispatchMessage 是 EA 与 MT5 图表事件衔接的核心。函数开头用 static 变量记住上一次交互状态,若不加 static,函数退出后 bMounting、valueTp 等会清零,再次移动鼠标时挂单逻辑就断了。 处理 CHARTEVENT_MOUSE_MOVE 时,代码先取鼠标价位和按键状态:0x01 是左键、0x04 是 SHIFT、0x08 是 CTRL。SHIFT 和 CTRL 不能同时按,也不能都不按,否则不进入挂单分支。 按下 SHIFT 或 CTRL 且未装配指标时,隐藏鼠标并调用 Chart.GetBaseFinance 取 TP/SL 点数,再乘 Terminal.GetAdjustToTrade() 除以成交量转成点数差。IT_PENDING 标记挂单,pr 记当前价。 TP/SL 在循环外计算,因为移动鼠标要同步拖线;但这段不能放进装配测试里——若释放 SHIFT 改按 CTRL 而不动鼠标,TP/SL 会错位,所以 IndicatorAdd 单独触发一次。 左键点击且 memLocal==0 才发挂单,保证同一次调用只在目标价下一张单;删除指标 0 的同时订单发往服务器。若只按了键却不满足挂单,高亮段会移除指标 0、显示鼠标、重置静态变量,系统回到干净态。 对象删除事件里,MT5 通过 sparam 传对象名,EA 查是否是指标 0 或基础指标,有未平单就重建。CLOSE 事件按单号平仓并删指标;MOVE 事件只选中指标,真正拖动在鼠标移动里完成,选中后线宽会变粗。

MQL5 / C++
class="kw">static class="type">bool bMounting = false, bIsDT = false;
class="kw">static class="type">class="kw">double valueTp = class="num">0, valueSl = class="num">0, memLocal = class="num">0;
case CHARTEVENT_MOUSE_MOVE:
    Mouse.GetPositionDP(dt, price);
    mKeys   = Mouse.GetButtonStatus();
    bEClick = (mKeys & 0x01) == 0x01;    class=class="str">"cmt">//Left mouse click
    bKeyBuy = (mKeys & 0x04) == 0x04;    class=class="str">"cmt">//SHIFT pressed
    bKeySell= (mKeys & 0x08) == 0x08;    class=class="str">"cmt">//CTRL pressed
if (bKeyBuy != bKeySell)
if (!bMounting)
{
    Mouse.Hide();
    bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
    valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
    valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
    m_InfoSelection.it = IT_PENDING;
    m_InfoSelection.pr = price;
}
m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
m_InfoSelection.bIsBuy = bKeyBuy;
if (!bMounting)
{
    IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
    m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
    m_InfoSelection.bIsMovingSelect = bMounting = true;
}
MoveSelection(price);
if ((bEClick) && (memLocal == class="num">0))
{
    RemoveIndicator(def_IndicatorTicket0);
    CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
}
if (bKeyBuy != bKeySell)
{
class=class="str">"cmt">// ... code described so far ....
}else if (bMounting)
{
    RemoveIndicator(def_IndicatorTicket0);
    Mouse.Show();

图表事件里的挂单与止损拖拽逻辑

这段 MT5 图表事件处理代码管的是挂单、止损止盈线在界面上的增删与拖拽响应。当 CHARTEVENT_OBJECT_DELETE 触发,先通过 GetIndicatorInfos 拿到对象信息,若 GetInfosTradeServer(ticket) 返回 0 就直接 break,说明服务器端已无此单;否则重建指标线并针对挂单(IT_PENDING)或结果线(IT_RESULT)调用 PositionAxlePrice 对齐价格轴,最后 ChartRedraw 重绘。 EV_CLOSE 分支依据 GetInfosTradeServer 返回值 cRet 分流:cRet<0 意味着订单被拒或已消失,走 RemoveOrderPendent 清理界面挂单;否则对持仓调用 ClosePosition。对于 IT_TAKE / IT_STOP 类对象,则把 bIsMovingSelect 置 true 并 SetPriceSelection(0),进入手动拖移状态。 在 EV_MOVE 里,如果正处于拖移中,先 SpotLight 高亮交易线,再把 bIsMovingSelect 复位 false 完成一次落点;若不在拖移态则进入 else 预备逻辑。外汇与贵金属市场杠杆高、滑点突发行情多,这类界面同步代码只解决“看得准”,不预示任何成交概率。 打开 MT5 的 EA 源码,把上面 case EV_CLOSE 的 cRet 判断接进你自己的订单管理类,能在服务器端已平单时避免客户端残留假线。

MQL5 / C++
memLocal = class="num">0;
bMounting = false;
}
if (bKeyBuy != bKeySell)
{
class=class="str">"cmt">// ... previously described code ...
}else if (bMounting)
{
class=class="str">"cmt">// ... previously described code ...
}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
   if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}
case CHARTEVENT_OBJECT_DELETE:
   if (GetIndicatorInfos(sparam, ticket, it, ev))
   {
      if (GetInfosTradeServer(ticket) == class="num">0) break;
      CreateIndicatorTrade(ticket, it);
      if ((it == IT_PENDING) || (it == IT_RESULT))
         PositionAxlePrice(ticket, it, m_InfoSelection.pr);
      ChartRedraw();
		 m_TradeLine.SpotLight();
      m_InfoSelection.bIsMovingSelect = false;
      UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
   }
   break;
case CHARTEVENT_CHART_CHANGE:
   ReDrawAllsIndicator();
   break;
case CHARTEVENT_OBJECT_CLICK:
   if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev)
   {
class=class="str">"cmt">//....
   }
   break;
case EV_CLOSE:
   if ((cRet = GetInfosTradeServer(ticket)) != class="num">0) class="kw">switch (it)
   {
      case IT_PENDING:
      case IT_RESULT:
         if (cRet < class="num">0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
         break;
      case IT_TAKE:
      case IT_STOP:
         m_InfoSelection.ticket = ticket;
         m_InfoSelection.it = it;
         m_InfoSelection.bIsMovingSelect = true;
         SetPriceSelection(class="num">0);
         break;
   }
   break;
case EV_MOVE:
   if (m_InfoSelection.bIsMovingSelect)
   {
      m_TradeLine.SpotLight();
      m_InfoSelection.bIsMovingSelect = false;
   }else
   {

「把选中订单挂上高亮线」

在 MT5 自定义指标里,想让当前选中的挂单或持仓在图表上被一眼看到,可以借用一个轻量的选择结构体来记录 ticket 与类型。 下面这段逻辑先把 ticket 和 it(订单类型枚举)写进 m_InfoSelection,随后用 GetInfosTradeServer 的返回值判断该订单是否处于可移动选中状态,并把结果赋给 bIsMovingSelect。 若返回值不为 0,说明服务端确认了这笔订单的存在与可操作性,此时调用 m_TradeLine.SpotLight 并以 macroMountName 拼出的名称高亮画线;最后 break 退出分支。 实际验证时,把这段代码塞进 OnChartEvent 的 SELECT 事件分支,在 EURUSD 的 M15 上点选任意挂单,图表应即时出现一条命名含 EV_LINE 的高亮线,外汇与贵金属品种波动剧烈,高亮仅作辅助观察,不构成方向判断。

MQL5 / C++
      m_InfoSelection.ticket = ticket;
      m_InfoSelection.it = it;
      if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != class="num">0))
      m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
      }
      break;

◍ 鼠标对象类的自愈与轻量重构

做基于鼠标交互的指标或 EA 时,最怕交易者手滑删掉构成鼠标系统的关键对象,轻则显示错乱,重则 EA 直接崩。MetaTrader 5 的 EVENT 系统能救场:一旦捕获到对象删除事件,对应类可以当场把对象重建出来,系统稳定性就兜住了。 但对象列表不能无脑堆。作者把 Mouse 类补上了「按需创建、用完即删」的逻辑:常驻的只有一条水平线,其余调研用的临时对象(趋势线、位图、文本等)在鼠标左键按下时创建,松开后立即清除。列表越短,MT5 内部按名检索对象的开销越小,订单系统响应倾向更跟手。 新版的 Init 比旧代码瘦了一大圈,核心就是设好颜色、打开鼠标移动事件、关掉十字光标,然后调一次 Show。Show 里先查水平线价格属性是否为 0 来判断对象是否存在——因为 Hide 只是把水平线染成 clrNONE 隐藏,并不真删,所以必须查了再决定建不建。 Hide 函数则反过来:临时对象用 ObjectsDeleteAll 按前缀 'MOUSE T' 一把清掉,水平线仅改色。作者实测这种临时对象闪建闪删的做法,处理耗时没有明显增减,订单系统略有微妙改善,但基本落在可比较误差范围内,不能说有实际性能提升。 DispatchMessage 开头先接事件,中键会触发一次对不存在对象的移动调用(由 b1 变量控点),左键完成第一步后定位趋势线并推进调研,松开左键就清场并重画鼠标线。高亮那行防误读的设计不完美,但能拦住 EA 在捕获按钮时错判状态。

MQL5 / C++
class="macro">#define def_MousePrefixName "MOUSE "
class="macro">#define def_NameObjectLineH def_MousePrefixName + "H"
class="macro">#define def_NameObjectLineV def_MousePrefixName + "TMPV"
class="macro">#define def_NameObjectLineT def_MousePrefixName + "TMPT"
class="macro">#define def_NameObjectBitMp def_MousePrefixName + "TMPB"
class="macro">#define def_NameObjectText  def_MousePrefixName + "TMPI"
class="type">void Init(class="type">color c1, class="type">color c2, class="type">color c3)
{
    m_Infos.cor01 = c1;
    m_Infos.cor02 = c2;
    m_Infos.cor03 = c3;
    ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, true);
    ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false);
    Show();
}
class="kw">inline class="type">void Show(class="type">void)
{
    if (ObjectGetDouble(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_PRICE) == class="num">0)
    {
        ObjectCreate(Terminal.Get_ID(), def_NameObjectLineH, OBJ_HLINE, class="num">0, class="num">0, class="num">0);
        ObjectSetString(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_TOOLTIP, "\n");
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_BACK, false);
    }
    ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, m_Infos.cor01);
}
class="kw">inline class="type">void Hide(class="type">void)
{
    ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
    ObjectsDeleteAll(Terminal.Get_ID(), def_MousePrefixName + "T");
    ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
    ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
}
class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{

中键起手与左键拉出价差带

在 MT5 图表事件里,鼠标中键(key & 0x10)被用作「激活测量」的开关:第一次按下中键会调用 CreateObjectsIntern() 重建对象并把静态标记 b1 置 1,此时水平线随鼠标 Y 轴走、竖线也开始跟随时间轴。 真正画出一段区间靠的是左键(key & 0x01)。b1==1 时左键落下,先关掉 CHART_MOUSE_SCROLL 防止图表乱滚,把起点价 memPrice 锁进水平线 T 的第 0 点;b1 改成 2。 b1==2 时再点左键,水平线 T 的第 1 点落到当前价,同时按 memPrice 与当前价大小切换颜色(cor03 / cor02),文本对象显示两位小数价差 Position.price - memPrice,位图锚点随涨跌切到右上或右下。外汇与贵金属波动快,这种手动测距仅作临场参考,杠杆品种高风险,数值不直接构成下单依据。

MQL5 / C++
class="type">int    w = class="num">0;
class="type">uint   key;
class="kw">static class="type">int b1 = class="num">0;
class="kw">static class="type">class="kw">double memPrice = class="num">0;
class="kw">switch (id)
{
   case CHARTEVENT_MOUSE_MOVE:
      Position.X = (class="type">int)lparam;
      Position.Y = (class="type">int)dparam;
      ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
      ObjectMove(Terminal.Get_ID(), def_NameObjectLineH, class="num">0, class="num">0, Position.price = Terminal.AdjustPrice(Position.price));
      if (b1 > class="num">0) ObjectMove(Terminal.Get_ID(), def_NameObjectLineV, class="num">0, Position.dt, class="num">0);
      key = (class="type">uint) sparam;
      if ((key & 0x10) == 0x10)   class=class="str">"cmt">//Middle button....
      {
         CreateObjectsIntern();
         b1 = class="num">1;
      }
if (((key & 0x01) == 0x01) && (b1 == class="num">1))
{
   ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
   ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, class="num">0, Position.dt, memPrice = Position.price);
   b1 = class="num">2;
}
if (((key & 0x01) == 0x01) && (b1 == class="num">2))
{
   ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, class="num">1, Position.dt, Position.price);
   ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
   ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
   ObjectMove(Terminal.Get_ID(), def_NameObjectBitMp, class="num">0, Position.dt, Position.price);
   ObjectSetInteger(Terminal.Get_ID(), def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
   ObjectSetString(Terminal.Get_ID(), def_NameObjectText, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
   ObjectMove(Terminal.Get_ID(), def_NameObjectText, class="num">0, Position.dt, Position.price);

「锚点切换与鼠标滚屏的状态收口」

这段代码负责在持仓线文本对象上做锚点自适应:当记忆价格高于持仓开仓价时,文本贴到右上角,否则落到右下角,避免标签和K线重叠挡视野。 键盘状态位 key 的最低位若为 0 且当前按钮态 b1 等于 2,说明刚结束某种锁定交互,此时把 b1 复位为 0,重新打开图表鼠标滚轮缩放(CHART_MOUSE_SCROLL=true),并 Hide/Show 强制重绘一次界面。 最后一行把按钮状态提交给 Position.ButtonsStatus:仅当 b1 为 0 时才保留 key 的位值,否则写 0,相当于交还控制权给常规热键逻辑。外汇与贵金属品种波动跳空频繁,这类重绘若放在 OnChartEvent 高频触发,可能吃掉低端机帧率,建议用 Print 打点确认调用次数后再上实盘。

MQL5 / C++
ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
}
if (((key & 0x01) != 0x01) && (b1 == class="num">2))
{
   b1 = class="num">0;
   ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
   Hide();
   Show();
}
Position.ButtonsStatus = (b1 == class="num">0 ? key : class="num">0);

◍ 把工具请下神坛

屏幕上的图形对象越多,EA 在实时请求特定对象时的检索效率就越低——这是订单系统跑在 MT5 图形层上的硬伤。系统实时运行,EA 反应越快越稳,所以它该只碰订单,其余交给终端。 下一步会把 EA 操作期间列表里的对象数继续压下去,因为订单系统本身就是大型对象生成器;减少它,MT5 的创建负载才可能最小化。这类改动不复杂,但需逐篇小修。 本文未附代码改动,因结构仍会变;下篇会带来显著性能提升。外汇与贵金属交易高风险,任何架构优化都只是降低延迟概率,不承诺收益。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象负载和调用延迟,你专注决策而不是盯着日志找卡点。

常见问题

瓶颈常在无效函数调用和消息分发逻辑,而非对象总数。减少不必要的 DispatchMessage 触发可明显提速。
优先检查服务器响应耗时与本地状态缓存是否分离,避免用界面对象存在性直接判定订单真实状态。
可以,小布的品种页会呈现对象负载与调用延迟类诊断,帮你快速定位是网络还是逻辑层拖慢系统。
鼠标交互事件频率高且独立,抽离后降低主调度耦合,提升可靠性和后续维护清晰度。
完全兼顾很难,但用分层和按需调用可把崩溃概率压到极低,倾向在关键路径做极简响应。