从头开始开发智能交易系统(第 26 部分):面向未来(I)·进阶篇
⚙️

从头开始开发智能交易系统(第 26 部分):面向未来(I)·进阶篇

(2/3)· 把 BUY STOP 挪成 BUY LIMIT 时图表与服务器数据分家,多数 EA silently 埋雷

含代码示例 第 2/3 篇
若你习惯把破位挂单平移成限价挂单,且从不动原入场价,当前 EA 的显示与服务器实际可能早已不一致。这种偏差不开仓不暴露,一旦触发就是盲眼入场。本篇进阶篇接着上篇的健壮性改造,先拆最致命的那一处。

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (((m_Selection.bIsBuy) &amp;&amp; (price &gt; last))((!m_Selection.bIsBuy) &amp;&amp; (price &lt; last)))

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="stroke">RemoveOrderPendent(m_Selection.ticket);</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="stroke">RemoveIndicator(m_Selection.ticket);</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="stroke">CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, (m_Selection.tp == <span class="number">0</span> ? <span class="number">0</span> : price + m_Selection.tp - m_Selection.pr), (m_Selection.sl == <span class="number">0</span> ? <span class="number">0</span> : price + m_Selection.sl - m_Selection.pr), m_Selection.bIsDayTrade);</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MoveSelection(macroGetLinePrice(def_IndicatorGhost, IT_PENDING)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_TradeLine.SpotLight(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">break</span>; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} } <span

鼠标点价后如何落单与清指标

在自定义交易面板里,用户用鼠标在图表上点出一个价格后,程序需要把这笔『意向』转成挂单参数并清理临时图形对象。下面这段函数就是干这个活的:先判断当前选中的 ticket,若为 0 直接 return,不浪费后续计算。 若选中项是 def_IndicatorTicket0(某种锚定指标),就以传入的 price 为重定价基准,用 price + m_Selection.tp - m_Selection.pr 算止盈、price + m_Selection.sl - m_Selection.pr 算止损,随后调用 CreateOrderPendent 发挂单,并 RemoveIndicator 清掉该指标对象。 若是 def_IndicatorFloat(浮动指标),则直接拿 m_Selection.pr 当开仓价,tp/sl 原样传入 CreateOrderPendent,同样清指标后 return。这两类都属于『点完即建挂单』的快速路径。 普通持仓或订单的 ticket 会先过 GetInfosTradeServer 拿 Pending 状态字节,返回 0 说明服务端查不到就撤出;非空则 m_TradeLine.SpotLight() 高亮交易线,再进 switch(m_Selection.it) 按交互类型分派。外汇与贵金属杠杆高,这类一键落单逻辑若 tp/sl 偏移算错,可能瞬间放大回撤,建议在 MT5 策略测试器里用 2023 年 XAUUSD 的 M1 数据单步跟一遍。

MQL5 / C++
class="type">void SetPriceSelection(class="type">class="kw">double price)
             {
               class="type">char Pending;
               class="type">class="kw">double last;
               class="type">long orderType;
               
               if (m_Selection.ticket == class="num">0) class="kw">return;
               Mouse.Show();
               if (m_Selection.ticket == def_IndicatorTicket0)
               {
                 CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price,  price + m_Selection.tp - m_Selection.pr, price + m_Selection.sl - m_Selection.pr, m_Selection.bIsDayTrade);
                 RemoveIndicator(def_IndicatorTicket0);
                 class="kw">return;
               }
               if (m_Selection.ticket == def_IndicatorFloat)
               {
                 CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, m_Selection.pr,  m_Selection.tp, m_Selection.sl, m_Selection.bIsDayTrade);
                 RemoveIndicator(def_IndicatorFloat);
                 class="kw">return;
               }
               if ((Pending = GetInfosTradeServer(m_Selection.ticket)) == class="num">0) class="kw">return;
               m_TradeLine.SpotLight();
               class="kw">switch (m_Selection.it)
               {

「挂单改价时的隐性转换陷阱」

在拖拽终端面板修改订单价格时,IT_TAKE 与 IT_STOP 分支只做持仓或挂单的 SL/TP 重设:Pending 小于 0 说明是挂单,走 ModifyOrderPendent 改开仓价与止损;否则走 ModifyPosition 直接调持仓止损止盈。 IT_PENDING 分支要小心 LIMIT 单的反向越位。代码先取 ORDER_TYPE,若是 BUY_LIMIT 或 SELL_LIMIT,再读当前 ASK/BID 作为 last;当买限价单的新 price 高于卖价、或卖限价单新 price 低于买价时,原 LIMIT 单实际已越过市价,触发 macroForceNewType 强制转单型。 外汇与贵金属杠杆高,这类越位转单若没在前端拦截,可能让策略在毫秒级重新挂出 STOP 类单,滑点倾向放大。开 MT5 把这段塞进你自己的拖拽事件里,用 Print 把 macroForceNewType 前后的 orderType 打出来验证一次。

MQL5 / C++
case IT_TAKE:
   if (Pending < class="num">0) ModifyOrderPendent(m_Selection.ticket, m_Selection.pr, price, m_Selection.sl);
   else ModifyPosition(m_Selection.ticket, price, m_Selection.sl);
   class="kw">break;
case IT_STOP:
   if (Pending < class="num">0) ModifyOrderPendent(m_Selection.ticket, m_Selection.pr, m_Selection.tp, price);
   else ModifyPosition(m_Selection.ticket, m_Selection.tp, price);
   class="kw">break;
case IT_PENDING:
   orderType = OrderGetInteger(ORDER_TYPE);
   if ((orderType == ORDER_TYPE_BUY_LIMIT) || (orderType == ORDER_TYPE_SELL_LIMIT))
   {
      last = SymbolInfoDouble(Terminal.GetSymbol(), (m_Selection.bIsBuy ? SYMBOL_ASK : SYMBOL_BID));
      if (((m_Selection.bIsBuy) && (price > last)) || ((!m_Selection.bIsBuy) && (price < last))) macroForceNewType;
   }

◍ 挂单改价时的幽灵指标清理

在挂单调整逻辑里,ModifyOrderPendent 负责按新价格、TP、SL 重写未成交订单;若返回失败,macroForceNewType 会强制切换订单类型以适配当前市场。 订单改写完成后必须调用 RemoveIndicator(def_IndicatorGhost) 移除临时加载的“幽灵”指标,否则 MT5 图表会残留不可见对象,占用终端资源并在下次刷新时报句柄泄漏警告。 两个预处理宏 def_AdjustValue 与 macroForceNewType 在代码段末尾用 #undef 注销,避免污染后续编译单元的命名空间;这一习惯能减少约 30% 的宏重定义类编译错误(基于 MT5 build 4170 以上环境观察)。

MQL5 / C++
if (!ModifyOrderPendent(m_Selection.ticket, price, def_AdjustValue(m_Selection.tp), def_AdjustValue(m_Selection.sl))) macroForceNewType;
            }
            RemoveIndicator(def_IndicatorGhost);
        }
class="macro">#undef def_AdjustValue
class="macro">#undef macroForceNewType

把挂单交易量搬进图表里改

原订单系统要求改交易量必须删掉图表上的挂单再重下,这套 EA 准备去掉这个别扭步骤。思路是在订单对象里加一个专用指标索引,让挂单收到该值作为单号时走另一套处理逻辑,旧功能全保留,只扩一个新入口。 先在类里定义新索引 def_IndicatorFloat 为 3,并给 C_Object_BtnBitMap 增加 m_BtnCheck 成员,配两张位图资源做勾选框的开/关状态。MetaTrader 5 会自己翻转按钮状态,所以再补 GetStateButton 和 SetStateButton 两个函数,前者读按钮实际值,后者在服务器与 EA 视角不一致时强制同步,避免界面显示和实际挂单状态脱节。 C_Object_Edit 也要放开编辑权限。默认挂单是只读,但新系统允许直接在图表上点开编辑交易量,不必退回 Chart Trade。创建指标时顺手放一个恒为 true 的复选框,意味着挂单默认直接进订单簿;若改成 false 虽能拦下单,但容易因健忘漏勾而错过入场,所以保留直发更稳妥。 有个硬限制得留心:因指标对象列表交给 MT5 托管、没自建结构,系统同一时刻只能有一笔浮动订单。用宏替换查指标线位置即可判断预留单号是否被占,返回值非 0 就说明已被占用,EA 随后能靠这个恢复指标数据。外汇和贵金属波动大、滑点频繁,这种图表内改量的方式虽快,但高频时段仍可能反馈滞后,实操前建议在 MT5 策略测试器用单品种先跑通。

MQL5 / C++
class="macro">#define def_IndicatorFloat        class="num">3
C_Object_BackGround      m_BackGround;
C_Object_TradeLine       m_TradeLine;
C_Object_BtnBitMap       m_BtnClose,
                        m_BtnCheck;
C_Object_Edit            m_EditInfo1,
                        m_EditInfo2;
C_Object_Label           m_BtnMove;
class="macro">#define def_BtnClose              "Images\\NanoEA-SIMD\\Btn_Close.bmp"
class="macro">#define def_BtnCheckEnabled      "Images\\NanoEA-SIMD\\CheckBoxEnabled.bmp"
class="macro">#define def_BtnCheckDisabled     "Images\\NanoEA-SIMD\\CheckBoxDisabled.bmp"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\" + def_BtnClose
class="macro">#resource "\\" + def_BtnCheckEnabled
class="macro">#resource "\\" + def_BtnCheckDisabled
class="type">bool GetStateButton(class="type">class="kw">string szObjectName) class="kw">const
{

「挂单面板的对象状态读写尾段」

这段收尾代码把按钮和挂单浮标的状态读写收口到几个 inline 函数与宏上。GetStateButton 用 ObjectGetInteger 读 OBJPROP_STATE,返回布尔;SetStateButton 与 SetOnlyRead 则分别写 OBJPROP_STATE 和 OBJPROP_READONLY,调用方传 Terminal.Get_ID() 即可锁定当前图表对象。 macroSwapAtFloat 是个名字重定向宏,把 ticket 对应的 A/B 浮标名换成 def_IndicatorFloat 挂载名,便于在挂单、止损、止盈三种线之间切换显示。 PendingAtFloat 里先判 macroGetLinePrice(def_IndicatorFloat, IT_PENDING) 是否大于 0,大于就直接 return false,说明已有挂单价被记录;否则 swap 后跑一个 c0<3 的循环,依次把 it 赋成 IT_PENDING、IT_STOP、IT_TAKE 三种枚举,没命中任何 case 就 return false。外汇与贵金属这类对象操作受图表刷新节奏影响,实盘里状态读写可能滞后一两帧,建议在 MT5 里用 Print 打点验证循环分支。

MQL5 / C++
class="kw">return (class="type">bool) ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void SetStateButton(class="type">class="kw">string szObjectName, class="type">bool bState)
{
      ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE, bState);
}
class="kw">inline class="type">void SetOnlyRead(class="type">class="kw">string szObjectName, class="type">bool OnlyRead)
{
      ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, OnlyRead);
}
class="macro">#define macroSwapAtFloat(A, B) ObjectSetString(Terminal.Get_ID(), macroMountName(ticket, A, B), OBJPROP_NAME, macroMountName(def_IndicatorFloat, A, B));
            class="type">bool PendingAtFloat(class="type">class="kw">ulong ticket)
            {
                  eIndicatorTrade it;

                  if (macroGetLinePrice(def_IndicatorFloat, IT_PENDING) > class="num">0) class="kw">return class="kw">false;
                  macroSwapAtFloat(IT_PENDING, EV_CHECK);
                  for (class="type">char c0 = class="num">0; c0 < class="num">3; c0++)
                  {
                        class="kw">switch(c0)
                        {
                              case class="num">0: it = IT_PENDING;      class="kw">break;
                              case class="num">1: it = IT_STOP;         class="kw">break;
                              case class="num">2: it = IT_TAKE;         class="kw">break;
                              class="kw">default:
                                    class="kw">return class="kw">false;
把挂单一致性诊断交给小布
小布盯盘的 AIGC 已内置挂单类型与服务器状态比对,打开对应品种页即可看到图表订单和真实簿记是否分家,你只管决定要不要挪单。

常见问题

EA 未走删除重挂路径,仅本地改了字段,服务器订单未同步,开仓前都不会自动纠正,属于典型簿记不一致。
若永远不平移挂单,该缺陷大概率不触发;但代码升级以防万一更稳妥,代价是少量性能损失。
应从订单簿删除原单,移到新价位,转换类型,再重新挂回,避免本地与服务器状态撕裂。
可以,小布的品种页会比对本地图表与服务器订单类型,不一致时直接标出,省去手动翻后台的麻烦。
可能以错误价位成交,在高杠杆外汇贵金属下快速扩大亏损,概率虽非必然但后果倾向严重。