从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I)·进阶篇
🛡️

从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I)·进阶篇

(2/3)·MT5对象数量远超直觉,但高波动时订单幻影仍会让EA误判,本篇拆解速度与可靠性的平衡

含代码示例 第 2/3 篇
高波动时挂单发到服务器响应变慢,EA一会儿显示有单一会儿显示没单,交易者常被逼出错误操作。很多人以为图表对象够用就安全,其实订单和持仓状态不同步才是隐形雷。把系统当黑盒信任,不如先搞清楚它什么时候会骗你。

「对象销毁的分支清理逻辑」

在指标或 EA 退出图形对象时,先判断传入的 it 类型。若为 IT_NULL、IT_PENDING 或 IT_RESULT 这类聚合类型,则需要对 RESULT、PENDING、TAKE、STOP 四个子类分别调用 macroDestroy 的正反两次清理,确保挂单、成交线、止盈止损标注全部移除。 若 it 为具体单一类型,则只对该类型做 true / false 两次 macroDestroy,避免无差别遍历拖慢图表。清理完后用 ChartSetInteger 打开 CHART_EVENT_OBJECT_DELETE,让图表能收到对象删除事件,便于后续联动。 末尾的 ObjectsDeleteAll 用 StringFormat 拼出对象前缀:def_NameObjectsTrade + def_SeparatorInfo + ticket,当 ticket>1 时插入 '*' 通配符,可一次性删掉该 ticket 下所有衍生图形。在 MT5 里把 ticket 设为 2 以上跑这段,能直观看到整组对象被清掉。 别把宏当万能橡皮 macroDestroy 只是封装删除,若对象名拼错或前缀不一致,聚合分支也会漏删,建议先在脚本里 Print 出 StringFormat 结果再上实盘图表。外汇与贵金属品种波动剧烈,误删标注可能造成误判,验证时请用模拟环境。

MQL5 / C++
if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
{
   macroDestroy(IT_RESULT, true);
   macroDestroy(IT_RESULT, false);
   macroDestroy(IT_PENDING, true);
   macroDestroy(IT_PENDING, false);
   macroDestroy(IT_TAKE, true);
   macroDestroy(IT_TAKE, false);
   macroDestroy(IT_STOP, true);
   macroDestroy(IT_STOP, false);
} else
{
   macroDestroy(it, true);
   macroDestroy(it, false);
}
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
class="macro">#undef macroDestroy
}
if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
   ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > class="num">1 ? &class="macro">#x27;*&class="macro">#x27; : def_SeparatorInfo)));

◍ 少建对象换来执行确定性

EA 初始化阶段最大的拖慢因素,是重复创建本可复用的图表对象。把往图表挂指标的逻辑收拢到同一个函数里调用,初始化只跑一遍 OrdersTotal 与 PositionsTotal 的循环,对象数量直接降下来,MT5 主线程的绘制负担也轻了。 IndicatorAdd 里那两行高亮检查,用的是 ObjectGetDouble 读 OBJPROP_PRICE。对象若真实贴在图表上,价格值必然非零;若对象不存在或脱离图表,返回值就是 0。靠这个判据,函数能立刻 return,避免重复建线。 很多人觉得频繁调 ChartRedraw 是浪费,其实是反的。OnTick 在高波动品种(如 XAUUSD)上可能一秒触发几十次,若不强制刷新,创建请求会堆进 MT5 的对象队列,部分挂单线可能滞后甚至丢失。主动 ChartRedraw + ObjectGetDouble 验证,等于把队列积压概率压下去,系统更稳。 对象 0 只在 Shift 或 CTRL+鼠标定位订单时用,创建前另有一次独立检查,确保不生成平台不支持的冗余对象。普通交易者删 TradeLine 之外的对象要慎重:EA 大多只通过按钮访问那些指标,你手动删了,它未必能察觉并重建。外汇与贵金属杠杆高,这类隐性失效可能让你误判仓位显示。

MQL5 / C++
class="type">void Initilize(class="type">void)
{
    ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_OBJECT_DESCR, false);
    ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false);
    ChartSetInteger(Terminal.Get_ID(), CHART_DRAG_TRADE_LEVELS, false);
    for (class="type">int c0 = OrdersTotal(); c0 >= class="num">0; c0--) IndicatorInfosAdd(OrderGetTicket(c0));
    for (class="type">int c0 = PositionsTotal(); c0 >= class="num">0; c0--) IndicatorInfosAdd(PositionGetTicket(c0));
}
class="kw">inline class="type">void IndicatorAdd(class="type">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;
    }
    class="kw">switch (ret)
    {
        case  class="num">1:
            CreateIndicatorTrade(ticket, IT_RESULT);
            PositionAxlePrice(ticket, IT_RESULT, m_InfoSelection.pr);

挂单与止损止盈的图形同步逻辑

这段 MT5 代码展示了在图表上动态维护挂单、止盈(IT_TAKE)与止损(IT_STOP)可视化元素的做法。核心思路是:根据交易返回码 -1 判定为挂单状态,调用 CreateIndicatorTrade 与 PositionAxlePrice 把挂单线钉在 m_InfoSelection.pr 价格上,随后 ChartRedraw 强制重绘。 UpdateIndicators 函数接收 ticket、tp、sl、vol、isBuy 五个参数,先通过 macroGetLinePrice 取结果线价格,若小于等于 0 则回退到挂单线价格。只有当 tp 或 sl 大于 0 时,才检查对应水平线对象价格是否为 0(即尚未创建),满足则建线并定位,同时用 SetTextValue 写出成交量与盈亏距离——买权用 tp-pr,卖权用 pr-tp。 SecureChannelPosition 开头抓取 SYMBOL_BID 与 SYMBOL_ASK,并逆序遍历 PositionsTotal() 持仓,用 PositionGetSymbol 过滤当前图表品种。外汇与贵金属杠杆品种跳空频繁,这类图形同步若依赖旧 tick 价格,可能让止损线视觉偏移实际成交位,建议在 MT5 策略测试器用 EURUSD 5 分钟数据跑一遍验证重绘时机。

MQL5 / C++
break;
			case -class="num">1:
				CreateIndicatorTrade(ticket, IT_PENDING);
				PositionAxlePrice(ticket, IT_PENDING, m_InfoSelection.pr);
				break;
		}
		ChartRedraw();
		UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
}
class="type">void UpdateIndicators(class="type">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;
		
		pr = macroGetLinePrice(ticket, IT_RESULT);
		pr = (pr > class="num">0 ? pr : macroGetLinePrice(ticket, IT_PENDING));
		SetTextValue(ticket, IT_PENDING, vol);
		if (tp > class="num">0)
		{
			if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_TAKE, EV_LINE, false), OBJPROP_PRICE) == class="num">0 ? true : b0))
				CreateIndicatorTrade(ticket, IT_TAKE);
			PositionAxlePrice(ticket, IT_TAKE, tp);
			SetTextValue(ticket, IT_TAKE, vol, (isBuy ? tp - pr : pr - tp));
		}
		if (sl > class="num">0)
		{
			if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_STOP, EV_LINE, false), OBJPROP_PRICE) == class="num">0 ? true : b0))
				CreateIndicatorTrade(ticket, IT_STOP);
			PositionAxlePrice(ticket, IT_STOP, sl);
			SetTextValue(ticket, IT_STOP, vol, (isBuy ? sl - pr : pr - sl));
		}
		if (b0) ChartRedraw();
}
class="kw">inline class="type">class="kw">double SecureChannelPosition(class="type">void)
{
		class="type">class="kw">double Res = class="num">0, sl, profit, bid, ask;
		class="type">ulong ticket;
		
		bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID);
		ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK);
		for (class="type">int i0 = PositionsTotal() - class="num">1; i0 >= class="num">0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
		{

「用鼠标动作接管持仓平仓判定」

这段逻辑把持仓遍历和鼠标事件绑在一起:先按 ticket 把持仓塞进指标容器,再把成交量、利润、开仓价写进 IT_RESULT 字段,随后读取 SL 准备做破位判定。 对于多单,若实时 ask 低于止损价就直接 ClosePosition;空单则要求 bid 上破 SL 且 SL 大于 0 才平,避免无 SL 的仓位被误杀。利润逐仓累加进 Res 返回。 DispatchMessage 里用 CHARTEVENT_MOUSE_MOVE 抓鼠标,Mouse.GetPositionDP 拿时间价格,GetButtonStatus 的位掩码 0x01 是左键、0x04 是 Shift。左键点击配合 Shift 可能倾向用于触发买侧逻辑,具体挂载动作由后续 case 决定。 外汇与贵金属杠杆高,这种自动平仓链一旦 SL 取错或报价跳空,可能直接砍在不利价位,上 MT5 用策略测试器单步跑一遍最稳。

MQL5 / C++
IndicatorAdd(ticket = PositionGetInteger(POSITION_TICKET));
SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN));
sl = PositionGetDouble(POSITION_SL);
if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
   if (ask < sl) ClosePosition(ticket);
}else
{
   if ((bid > sl) && (sl > class="num">0)) ClosePosition(ticket);
}
Res += profit;
}
class="kw">return Res;
};
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="type">ulong   ticket;
   class="type">class="kw">double  price;
   class="type">bool    bKeyBuy,
           bKeySell,
           bEClick;
   class="type">class="kw">datetime            dt;
   class="type">uint                mKeys;
   class="type">char                cRet;
   eIndicatorTrade it;
   eEventType          ev;

   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;

   class="kw">switch (id)
   {
      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

◍ 用 Ctrl 区分买卖挂单的挂载逻辑

在 MT5 自定义指标的交互里,可以用键盘修饰键来切换下单方向。下面这段代码通过位运算提取 Ctrl 状态:当 mKeys & 0x08 等于 0x08 时,bKeySell 为真,意味着用户按住了 Ctrl,此时与 bKeyBuy 互斥,从而决定是挂 buy 还是 sell _pending。 如果当前不在挂载状态(!bMounting),先隐藏鼠标,再调用 Chart.GetBaseFinance 按成交量换算 TP、SL 基准,并用 Terminal.AdjustPrice 乘以 GetAdjustToTrade()/vol 做合规价格对齐——这一步直接决定挂单偏离市价的点数是否合理。 随后把 m_InfoSelectionit 设为 IT_PENDING,记录触发价 pr,并按方向加减 valueTp/valueSl 算出 tpsl。首次挂载还会写入指标票号、打高亮并置 bMounting=true,之后 MoveSelection(price) 实时跟价。外汇与贵金属波动剧烈,这类一键挂单工具若价格对齐出错,可能瞬间触发滑点风险。 别把 0x08 当死值 不同 MT5 构建里虚拟键位映射可能偏移,实盘前用 Print(mKeys) 抓一次你终端的 Ctrl 掩码,别直接抄 0x08。

MQL5 / C++
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))

挂单删除后的指标重建逻辑

这段分支处理的是图形对象被删除时的回调。当 CHARTEVENT_OBJECT_DELETE 触发,先通过 GetIndicatorInfos 拿到被删对象的 ticket 与类型,若服务端查不到对应交易记录(GetInfosTradeServer 返回 0)就直接跳出,避免对已成历史的数据反复操作。 确认对象仍有效后,CreateIndicatorTrade 会按原 ticket 与类型重建指标;若是挂单或成交结果类(IT_PENDING / IT_RESULT),再用 PositionAxlePrice 把价格轴对齐到 m_InfoSelection.pr。随后 ChartRedraw 重绘、SpotLight 高亮当前交易线,并清掉移动选中标记、刷新止盈止损与手数。 在另一处鼠标事件分支里,若挂载状态 bMounting 为真,会先 RemoveIndicator 卸载临时指标,再 Mouse.Show 恢复光标并复位 memLocalbMounting;若买卖键同态且未挂载,则按是否单击来设定或移动选价。外汇与贵金属杠杆高,这类自动重建若参数错配,可能瞬间重挂异常单,建议开 MT5 用脚本单步验证。

MQL5 / C++
{
  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);
}
}else if (bMounting)
{
  RemoveIndicator(def_IndicatorTicket0);
  Mouse.Show();
  memLocal = class="num">0;
  bMounting = false;
}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
  if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}
break;
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);
把重复诊断交给小布盯盘
这些对象数量监控与订单状态不一致的排查,小布盯盘的AIGC已内置,打开对应品种页即可看到实时告警,你只需专注是否干预。

常见问题

对象上限高不代表调用效率高,频繁不必要的函数调用和服务器响应延迟叠加,仍会拖垮实时系统速度。
订单是请求,持仓是成交结果,服务器处理两者有先后,网络拥堵时EA读取到的快照可能错位。
对象越少渲染和遍历开销越小,在同等逻辑下系统响应倾向更快,但需配合避免多余函数调用才见效。
可以,小布盯盘的品种页会标记订单与持仓状态不一致的时刻,省去你手动比对服务器日志。
涉及实时资金操作,可靠性优先,速度通过精简调用达成,而非牺牲稳定换响应。