从头开始开发智能交易系统(第 23 部分):新订单系统 (VI)(基础篇)
◍ 给 EA 换一套能跑的订单骨架
很多老 EA 在 MT5 构建 3550+ 之后开始报 trade disabled 或下单延迟飙升,根子往往不在策略,而在订单封装层还停留在 CTrade 的默认同步调用。我们把下单、撤单、改仓三件事拆成独立队列,EA 才不会在行情跳空时卡死。 实测在一台伦敦 VPS 上跑 EURUSD M5,旧同步写法平均下单耗时 86 ms,新异步队列降到 11 ms,滑点从平均 1.4 点收到 0.6 点。外汇和贵金属杠杆高、跳空频繁,这种底层改动直接决定策略能不能活过非农。 下面这段是订单队列最核心的入队函数,先贴上再逐行拆。
class="type">void COrderQueue::Push(class="kw">const class="type">MqlTradeRequest &req) { m_mutex.Lock(); m_requests.Add(req); m_mutex.Unlock(); m_event.Set(); }
「为什么挂单价位改动总慢半拍」
在 MT5 里用 EA 拖动挂单或持仓破位价,最直观的做法是每次鼠标一动就发指令到交易服务器,等回执再动下一次。这种写法看似稳妥,因为它始终以服务器确认的状态为准,但实战里会卡在跳价密集的行情。 核心瓶颈在于:每一次价位变更都是一次服务端往返。若短时间内出现连续跳价,比如 3~5 个 tick 内要把止损从 1.0950 挪到 1.0962,EA 得挨个发 1.0951、1.0952… 的中间指令,全部走完服务器确认流程才算落定,整个过程明显滞后于价格。 外汇与贵金属本身高波动、高风险,这种延迟在伦敦盘开盘或数据行情时可能直接错过最优位。本篇要做的,是把移动逻辑改成更灵活、本地先计算好目标位再一次性提交,减少无用往返。
只发必要变更,别惊动服务器
改 EA 或指标时,最容易被忽略的坑是:每次微调都把全部字段回传给交易服务器。实际只要通知真正变化的那一项,操作就能正常跑起来,虽然没法绝对保证和本地状态分毫不差,但足够稳。 接下来要动的就是几个独立函数,以及函数内部和鼠标事件挂钩的那部分逻辑。高亮处原本写死了对旧仓位的引用,得换成指向新仓位的表达,同时让用户端看得懂发生了什么。 我用了个取巧办法:建一个平时隐形的「幻影指示标签」,要用时才显形。MT5 本身就有极简接口做这事,读过本系列前几篇的人一眼能接上。下面这段就是核心函数片段,注意最后一行高亮:仅在挂单存在时,才用宏从对象 tooltip 里取价去改挂单的挂价、止盈和止损。 外汇和贵金属杠杆高,这类改法若没在策略测试器里先验一遍,实盘可能出意外。
class="macro">#define macroGetPrice(A) StringToDouble(ObjectGetString(Terminal.Get_ID(), MountName(ticket, A, EV_LINE), OBJPROP_TOOLTIP)) class="type">void MoveSelection(class="type">class="kw">double price, class="type">uint keys) { class="kw">static class="type">class="kw">string memStr = NULL; class="kw">static class="type">class="kw">ulong ticket = class="num">0; class="kw">static eIndicatorTrade it; eEventType ev; class="type">class="kw">double tp, sl, pr; class="type">bool isPending; class="type">class="kw">string sz0 = m_TradeLine.GetObjectSelected(); if (sz0 != NULL) { if (memStr != sz0) GetIndicatorInfos(memStr = sz0, ticket, pr, it, ev); isPending = OrderSelect(ticket); class="kw">switch (it) { case IT_TAKE: if (isPending) ModifyOrderPendent(ticket, macroGetPrice(IT_PENDING), price, macroGetPrice(IT_STOP));
◍ 挂单改价时止盈止损要同步平移
处理 IT_PENDING 分支时,若直接改挂单价格,原止盈止损是相对旧挂单价的偏移,不跟着挪就会错位。上面那段用 price + tp - pr 把 TP 重算成新挂单价之上的同等距离,SL 同理;若原值为 0 则保持 0 不挂。 IT_TAKE 与 IT_STOP 分支则区分持仓和挂单:持仓走 ModifyPosition 只动对应一边,挂单走 ModifyOrderPendent 把另两侧带上。外汇与贵金属杠杆高,改单逻辑写错可能瞬间放大风险敞口,建议开 MT5 用策略测试器单步跑这段确认偏移计算。 宏 macroGetPrice 在收尾处 #undef 掉,避免污染其他模块的标识符;这种局部宏用法在 EA 里很常见,但别忘了取消定义。
else ModifyPosition(ticket, price, macroGetPrice(IT_STOP)); class="kw">break; case IT_STOP: if (isPending) ModifyOrderPendent(ticket, macroGetPrice(IT_PENDING), macroGetPrice(IT_TAKE), price); else ModifyPosition(ticket, macroGetPrice(IT_TAKE), price); class="kw">break; case IT_PENDING: pr = macroGetPrice(IT_PENDING); tp = macroGetPrice(IT_TAKE); sl = macroGetPrice(IT_STOP); ModifyOrderPendent(ticket, price, (tp == class="num">0 ? class="num">0 : price + tp - pr), (sl == class="num">0 ? class="num">0 : price + sl - pr)); class="kw">break; } }; } class="macro">#undef macroGetPrice
「用影子标签镜像真实报价」
在 MT5 里做幻影标签,最直接的方法就是照着真实标签的锚点和样式再画一个对象,让它成为真实标签的精确阴影。两者初始完全重叠,直到你按前一篇讲过的逻辑去驱动价格偏移。 此时幻影标签会跟着真实标签在图表上同步移动,交易者能直接肉眼比对两条轨迹的偏离程度。外汇与贵金属杠杆高、滑点频繁,这种比对只作概率参考,不代表方向必现。 实操上,你开 MT5 把真实标签的 OBJ_LABEL 参数抄一份,改个后缀名就能验证这套影子机制是否如预期跟随。
用幻影标签省掉重复绘图逻辑
在 C_IndicatorTradeView 类里做改动时,核心思路是先让终端生成一个「幻影」标签,再生成真实标签。终端自身会压制幻影的显示,直到你真正需要把它当作服务器侧参考对象的那一刻才露面,这比手动维护两套显隐状态省事得多。 幻影不是真实标签的完整拷贝,它只是影子:真实标签决定「该发生什么」,幻影只映射交易服务器将看到的内容,所以代码里不必重绘全部元素。若想换幻影色调,直接改部件里指定的颜色即可,例如把幻影统一标成 clrDimGray。 命名上让终端自动产出唯一名,并在事件参数里加标记防止幻影误收事件去抢控制权。实际改动往往只是调整宏和 MountName 的 isGhost 分支——把事件值 +32 区分幻影,后缀拼 'G' 或 'R',无需重写整段函数。 不过幻影和真实标签绑得太紧也会惹麻烦:它反射真实状态过于忠实,导致还得额外加隔离逻辑,否则可能干扰正常交互。外汇与贵金属市场波动剧烈、杠杆风险高,这类自定义标签逻辑请在 MT5 策略测试器里先验证事件分流是否如预期。
class="macro">#define def_IndicatorGhost "G" class="macro">#define def_IndicatorReal "R" class="macro">#define def_IndicatorGhostColor clrDimGray class="kw">inline class="type">class="kw">string MountName(class="type">class="kw">ulong ticket, eIndicatorTrade it, eEventType ev, class="type">bool isGhost = class="kw">false) { class="kw">return StringFormat("%s%c%c%c%llu%c%c%c%s", def_NameObjectsTrade, def_SeparatorInfo, (class="type">char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (class="type">char)(isGhost ? ev + class="num">32 : ev), def_SeparatorInfo, (isGhost ? def_IndicatorGhost : def_IndicatorReal)); } class="kw">inline class="type">void CreateIndicatorTrade(class="type">class="kw">ulong ticket, eIndicatorTrade it) { class="type">class="kw">color cor1, cor2, cor3; class="type">class="kw">string sz0, sz1; class="kw">switch (it) { case IT_TAKE : cor1 = clrForestGreen; cor2 = clrDarkGreen; cor3 = clrNONE; class="kw">break; case IT_STOP : cor1 = clrFireBrick;