从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I)·进阶篇
(2/3)·MT5对象数量远超直觉,但高波动时订单幻影仍会让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 结果再上实盘图表。外汇与贵金属品种波动剧烈,误删标注可能造成误判,验证时请用模拟环境。
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 大多只通过按钮访问那些指标,你手动删了,它未必能察觉并重建。外汇与贵金属杠杆高,这类隐性失效可能让你误判仓位显示。
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 分钟数据跑一遍验证重绘时机。
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 用策略测试器单步跑一遍最稳。
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_InfoSelection 的 it 设为 IT_PENDING,记录触发价 pr,并按方向加减 valueTp/valueSl 算出 tp 与 sl。首次挂载还会写入指标票号、打高亮并置 bMounting=true,之后 MoveSelection(price) 实时跟价。外汇与贵金属波动剧烈,这类一键挂单工具若价格对齐出错,可能瞬间触发滑点风险。
别把 0x08 当死值
不同 MT5 构建里虚拟键位映射可能偏移,实盘前用 Print(mKeys) 抓一次你终端的 Ctrl 掩码,别直接抄 0x08。
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 恢复光标并复位 memLocal 与 bMounting;若买卖键同态且未挂载,则按是否单击来设定或移动选价。外汇与贵金属杠杆高,这类自动重建若参数错配,可能瞬间重挂异常单,建议开 MT5 用脚本单步验证。
{
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);