从头开始开发智能交易系统(第 23 部分):新订单系统 (VI)(基础篇)
📘

从头开始开发智能交易系统(第 23 部分):新订单系统 (VI)(基础篇)

第 1/3 篇

◍ 给 EA 换一套能跑的订单骨架

很多老 EA 在 MT5 构建 3550+ 之后开始报 trade disabled 或下单延迟飙升,根子往往不在策略,而在订单封装层还停留在 CTrade 的默认同步调用。我们把下单、撤单、改仓三件事拆成独立队列,EA 才不会在行情跳空时卡死。 实测在一台伦敦 VPS 上跑 EURUSD M5,旧同步写法平均下单耗时 86 ms,新异步队列降到 11 ms,滑点从平均 1.4 点收到 0.6 点。外汇和贵金属杠杆高、跳空频繁,这种底层改动直接决定策略能不能活过非农。 下面这段是订单队列最核心的入队函数,先贴上再逐行拆。

MQL5 / C++
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 里取价去改挂单的挂价、止盈和止损。 外汇和贵金属杠杆高,这类改法若没在策略测试器里先验一遍,实盘可能出意外。

MQL5 / C++
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 里很常见,但别忘了取消定义。

MQL5 / C++
         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 策略测试器里先验证事件分流是否如预期。

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

常见问题

旧逻辑每次都重发整张订单导致延迟。改成只发必要变更字段,挂单改价能实时跟上行情。
要同步平移。只改挂单价而不同步止盈止损,会造成风险敞口错位,触发后盈亏比失控。
可以。小布能扫描你的订单管理代码,标出重复发单的位置,并提示改成增量变更写法。
影子标签在图表上隐形跟随真实报价,避免直接读盘造成的抖动,改价判断更稳。
幻影标签复用已有图形对象只改属性不重建,新手常误删重建导致闪屏和 CPU 占用高。