从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I)·综合运用
(3/3)· 高波动下挂单时有时无、持仓指令失灵?这套速度兼可靠的收口方案给你答案
挂单面板上的点击交互怎么分流
这段逻辑来自一个挂在图表上的订单管理面板,它接管了 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 跑一遍点击流。
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 在实盘高波动时段容易让人误判持仓边界。
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 事件只选中指标,真正拖动在鼠标移动里完成,选中后线宽会变粗。
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 判断接进你自己的订单管理类,能在服务器端已平单时避免客户端残留假线。
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 的高亮线,外汇与贵金属品种波动剧烈,高亮仅作辅助观察,不构成方向判断。
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 在捕获按钮时错判状态。
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,位图锚点随涨跌切到右上或右下。外汇与贵金属波动快,这种手动测距仅作临场参考,杠杆品种高风险,数值不直接构成下单依据。
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 打点确认调用次数后再上实盘。
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 的创建负载才可能最小化。这类改动不复杂,但需逐篇小修。 本文未附代码改动,因结构仍会变;下篇会带来显著性能提升。外汇与贵金属交易高风险,任何架构优化都只是降低延迟概率,不承诺收益。