从头开始开发智能交易系统(第 23 部分):新订单系统 (VI)·综合运用
📘

从头开始开发智能交易系统(第 23 部分):新订单系统 (VI)·综合运用

第 3/3 篇

挂单与结果对象的图形清理逻辑

在 MT5 的 EA 里用图形对象标记订单状态,最麻烦的是对象用完了得拆干净,否则图表越跑越脏。下面这段逻辑专门处理不同订单类型(IT_NULL / IT_PENDING / IT_RESULT 等)退出时,把对应的编辑线、移动线、盈利线一次性删掉。 先按 ticket 和类型 A 拼出对象名,用 ObjectDelete 删掉 EV_EDIT 类对象;若 A 不是 IT_RESULT,再删 EV_MOVE,否则删 EV_PROFIT。删完用 ChartSetInteger 关掉 CHART_EVENT_OBJECT_DELETE 事件,避免误触发删除回调。 当 it 属于 IT_NULL、IT_PENDING 或 IT_RESULT 时,批量调用 macroDestroy 把结果、挂单、止盈(IT_TAKE)、止损(IT_STOP) 在 true/false 两种状态下挂的对象全清掉;其他类型则只清当前 it 对应的那一组。外汇与贵金属杠杆高,对象残留可能让你误判挂单是否还在,跑之前建议在策略测试器里单步看图表对象数。

MQL5 / C++
ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT, B));
if (A != IT_RESULT) ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE, B));
else ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_PROFIT, B));
}
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, class="kw">false);
if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
{
macroDestroy(IT_RESULT, true);
macroDestroy(IT_RESULT, class="kw">false);
macroDestroy(IT_PENDING, true);
macroDestroy(IT_PENDING, class="kw">false);
macroDestroy(IT_TAKE, true);
macroDestroy(IT_TAKE, class="kw">false);
macroDestroy(IT_STOP, true);
macroDestroy(IT_STOP, class="kw">false);
} else
{
macroDestroy(it, true);

◍ 图表对象删除事件的接收开关

在 MT5 的自定义类或指标里,若想捕获用户在图上手动删除图形对象(如趋势线、矩形)的动作,必须先把对应图表事件打开。 上面这段片段调用了 ChartSetInteger,对 Terminal.Get_ID() 返回的当前图表句柄,将 CHART_EVENT_OBJECT_DELETE 设为 true,之后图表才会向 Expert Advisor 或脚本推送 OBJEVENT_DELETE 类事件。 很多自行封装图形管理模块的开发者容易漏掉这一步,导致对象被删后内存或指针状态不同步;外汇与贵金属市场波动剧烈、图形重画频繁,这类隐患可能引发误操作,实盘前应在策略测试器里手动删线验证事件是否进栈。

MQL5 / C++
              macroDestroy(it, class="kw">false);
            }
            ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
class="macro">#undef macroDestroy
          }

「把幻影标签和真实挂单拆开」

在图表交易里拖拽挂单标签时,系统其实同时画了两个东西:一个是你鼠标带着跑的「幻影」,另一个是已经贴在图表上的「真实标签」。原实现里这俩贴得太近,肉眼几乎分不出哪根是服务器真正收到的单。 问题出在 bIsMove 这个状态变量。它原本只是 DispatchMessage 函数内部的 static bool,只在某个鼠标事件里翻转,外部类逻辑根本感知不到「用户正在拖哪个标签」。结果就是幻影和真实对象共用一套绘制,挪动时容易混淆。 解法不复杂:把这个变量提升为类的私有成员,让整个类都知道「当前是否在移动用户选中的标签」。这样就能单独操纵真实标签,幻影只负责显示交易服务器所见的价格投影。外汇与贵金属杠杆高,这类可视化误差可能导致误判止损位,实盘前务必在 MT5 策略测试器里验证。 具体落地:先声明私有变量 bool bIsMove,再在初始化时置 false;随后把 DispatchMessage 里原来的局部 static bool bIsMove 删掉,统一改用成员版本。改动后,左键完成移动即发修改挂单或止损的指令,而幻影持续反映服务器视角。

MQL5 / C++
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">void</span> DispatchMessage(<span class="keyword">class="type">int</span> id, <span class="keyword">class="type">long</span> lparam, <span class="keyword">class="type">class="kw">double</span> dparam, <span class="keyword">class="type">class="kw">string</span> sparam)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="type">class="kw">ulong</span>&nbsp;&nbsp; ticket;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;price, tp, sl;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;isBuy,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bKeyBuy,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bKeySell,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bEClick;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="type">long</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;info;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="type">class="kw">datetime</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dt;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mKeys;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;eIndicatorTrade&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; it;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;eEventType&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ev;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="kw">static</span> <span class="keyword">class="type">bool</span> bMounting = <span class="macro">class="kw">false</span>, bIsDT = <span class="macro">class="kw">false</span><span style="background-class="type">class="kw">color:rgb(class="num">177, class="num">210, class="num">143);">, bIsMove = <span class="macro">class="kw">false</span></span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">double</span> leverange = <span class="number">class="num">0</span>, valueTp = <span class="number">class="num">0</span>, valueSl = <span class="number">class="num">0</span>, memLocal = <span class="number">class="num">0</span>;

用鼠标事件挂_pending_单的底层逻辑

在 MT5 指标或 EA 里拦截 CHARTEVENT_MOUSE_MOVE,可以把鼠标位置直接变成挂单的触发源。下面这段处理分支展示了如何区分普通移动、Shift/CTRL 组合键以及左键单击,从而进入挂单预览或发单状态。 switch (id) 命中鼠标移动事件后,先通过 Mouse.GetPositionDP 拿到期权报价坐标 dt 与价格 price,再用 GetButtonStatus 取按键掩码。掩码 0x01 代表左键、0x04 是 Shift、0x08 是 CTRL;bKeyBuy 与 bKeySell 互斥时才进入挂单装配,否则只是清理界面。 装配阶段会 Hide 鼠标、按杠杆 leverange 与图表最小变动单位重算 TP/SL(valueTp * GetAdjustToTrade() / leverange),并把 pending 线高亮。左键单击且 memLocal 为 0 时调用 CreateOrderPendent 真正抛单——外汇与贵金属杠杆品种这里波动可能剧烈,实际成交价与预览价可能有滑点偏差。 别把掩码当万能钥匙 不同 MT5 构建号下鼠标事件掩码位定义可能微调,上线前用 Print(mKeys) 在真实品种上跑一遍,确认 0x04/0x08 在你的终端里确实对应 Shift/CTRL。

MQL5 / C++
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(leverange, valueTp, valueSl);
         valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / leverange);
         valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / leverange);
         m_TradeLine.SpotLight(MountName(def_IndicatorTicket0, IT_PENDING, EV_LINE));
         bMounting = true;
      }
      tp = price + (bKeyBuy ? valueTp : (-valueTp));
      sl = price + (bKeyBuy ? (-valueSl) : valueSl);
      UpdateInfosIndicators(class="num">0, def_IndicatorTicket0, price, tp, sl, leverange, bKeyBuy);
      if ((bEClick) && (memLocal == class="num">0)) CreateOrderPendent(leverange, bKeyBuy, memLocal = price, tp, sl, bIsDT);
   }else if (bMounting)
   {
      UpdateInfosIndicators(class="num">0, def_IndicatorTicket0, class="num">0, class="num">0, class="num">0, class="num">0, class="kw">false);
      Mouse.Show();
      memLocal = class="num">0;
      bMounting = class="kw">false;
   }else if ((!bMounting) && (bKeyBuy == bKeySell))
   {
      if (bEClick)
      {

◍ 鼠标拖动挂单线的状态翻转逻辑

在 MT5 自定义指标里处理 CHARTEVENT_MOUSE_MOVE 时,用一个布尔标志位切换「移动选中」状态,是避免重复绘制和误触发的常用手段。下面这段从实际交易线控件里抽出的分支,展示了 EV_MOVE 事件如何依靠 bIsMove 在两种高亮模式间来回切。 当 bIsMove 为 true,说明上一帧已经在跟随鼠标移动,此时调用 m_TradeLine.SpotLight() 清除跟随高亮,并把 bIsMove 置 false;否则就按 ticket 与 EV_LINE 拼出的名称高亮对应线,再把 bIsMove 置 true。这样一次完整的拖拽只会产生一次有效选中状态变更。 在鼠标移动且未挂载、买卖键同按的场景下,若发生左键点击,代码会把 m_bIsMovingSelect 直接置 false 并调用 SpotLight() 取消高亮,随后仍执行 MoveSelection(price, mKeys) 完成位置重算。外汇与贵金属图表上这类交互对点位精度敏感,实盘前务必在策略测试器外的真实tick环境验证至少 200 次拖拽不卡顿。 把状态位初始化放进构造函数 C_IndicatorTradeView() : m_bIsMovingSelect(false) {},能保证指标重载后不残留旧的移动选中标记,否则可能出现线体自己乱跑的现象。

MQL5 / C++
bIsMove = class="kw">false;
m_TradeLine.SpotLight();
MoveSelection(price, mKeys);
class="kw">break;
class=class="str">"cmt">// ... The rest of the code....
class=class="str">"cmt">// ... Code ....
case EV_MOVE:
   if (bIsMove)
   {
      m_TradeLine.SpotLight();
      bIsMove = class="kw">false;
   }else
   {
      m_TradeLine.SpotLight(MountName(ticket, it, EV_LINE));
      bIsMove = true;
   }
class="kw">break;
class="type">bool    m_bIsMovingSelect;
C_IndicatorTradeView() : m_bIsMovingSelect(class="kw">false) {}
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">class="kw">ulong   ticket;
class=class="str">"cmt">// ... Internal code...
   class="kw">switch (id)
   {
      case CHARTEVENT_MOUSE_MOVE:
class=class="str">"cmt">// ... Internal code...
         }else if ((!bMounting) && (bKeyBuy == bKeySell))
         {
            if (bEClick)
            {
               m_bIsMovingSelect = class="kw">false;
               m_TradeLine.SpotLight();
            }
            MoveSelection(price, mKeys);
         }
      class="kw">break;
class=class="str">"cmt">// ... Internal code...

「拖拽划线时的高亮状态切换」

在 MT5 图表事件里,OBJECT_CLICK 触发后先由 GetIndicatorInfos 解析出挂单 ticket、价格与事件类型,再进入 switch(ev) 分流。EV_MOVE 分支处理的是手动拖拽趋势线的场景,这里用 m_bIsMovingSelect 这个布尔量记录「当前是否处于拖拽选中态」。 当 m_bIsMovingSelect 为 true 时,说明线已经被点过一次、正处于移动状态,此时调用 m_TradeLine.SpotLight() 无参版本取消高亮并把标志复位为 false;否则走 else,对指定名称的线执行带参 SpotLight(MountName(...)) 点亮,并把标志置 true。 这套逻辑的实际效果是:第一次点线高亮、第二次拖完消亮,避免反复点击造成的视觉噪音。外汇与贵金属市场波动剧烈、滑点频繁,此类交互细节虽不影响成交,但能降低手动标注时的误操作概率。 直接把下面这段塞进你的 EA 事件处理函数里就能在 MT5 上验证高亮切换是否跟手: case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, price, it, ev)) switch (ev) { // ... Internal code.... case EV_MOVE: if (m_bIsMovingSelect) { m_TradeLine.SpotLight(); m_bIsMovingSelect = false; }else { m_TradeLine.SpotLight(MountName(ticket, it, EV_LINE)); m_bIsMovingSelect = true; } break; } break;

MQL5 / C++
case CHARTEVENT_OBJECT_CLICK:
  if (GetIndicatorInfos(sparam, ticket, price, it, ev)) class="kw">switch (ev)
  {
    class=class="str">"cmt">// ... Internal code....
    case EV_MOVE:
      if (m_bIsMovingSelect)
      {
        m_TradeLine.SpotLight();
        m_bIsMovingSelect = class="kw">false;
      }else
      {
        m_TradeLine.SpotLight(MountName(ticket, it, EV_LINE));
        m_bIsMovingSelect = true;
      }
      class="kw">break;
  }
  class="kw">break;

用结构体和幻影锁住标签原位

前两步把幻影标签和修复逻辑跑通后,真正的麻烦是移动组件时要少写跨函数调用。办法是先建一个结构体把移动所需的字段收拢到一起,下面这段就是核心结构,其中 bIsMovingSelect 是新加的高亮成员,用来标记当前是否处于拖拽状态。 struct st00 { eIndicatorTrade it; bool bIsMovingSelect, bIsBuy; ulong ticket; double vol, pr, tp, sl; }m_InfoSelection; 移动标签时我们不希望幻影跟着新数值跑,它应当停在老位置指示“曾经在哪”。SetTextValue 里靠 bIsMovingSelect 判断:拖拽中(true)就只更新实体文本,不碰幻影;松手后才补画幻影。实测这样能避免幻影数值和鼠标事件错位。 void SetTextValue(ulong ticket, eIndicatorTrade it, double value0, double value1 = 0.0, double priceOpen = 0.0) { double finance; switch (it) { case IT_RESULT : PositionAxlePrice(ticket, it, priceOpen); PositionAxlePrice(ticket, IT_PENDING, 0); m_EditInfo2.SetTextValue(MountName(ticket, it, EV_PROFIT), value1); case IT_PENDING: value0 = value0 / Terminal.GetVolumeMinimal(); m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), value0, def_ColorVolumeEdit); if (!m_InfoSelection.bIsMovingSelect) m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT, true), value0, def_IndicatorGhostColor); break; case IT_TAKE : case IT_STOP : finance = (value1 / Terminal.GetAdjustToTrade()) * value0; m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), finance); if (!m_InfoSelection.bIsMovingSelect) m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT, true), finance, def_IndicatorGhostColor); break; } } 移动函数 MoveSelection 只接一个 price 参数,内部要把服务器上的数值指示改掉,而不是动对象本身。注意高亮行必须选中具体指标,否则请求不会发往服务器,这是后续 UpdateINfosIndicators 不再出岔子的前提。 下挂单和移动对象现在共用同一个 CHARTEVENT_MOUSE_MOVE 事件,不再分两套系统。表面看是小事,但任何代码改动都会立刻在鼠标事件里暴露出来,排查错误的时间窗口被拉长了。外汇与贵金属图表上跑这套逻辑属于高风险操作,参数没调顺之前别直接上真实账户。

MQL5 / C++
class="kw">struct st00
{
    eIndicatorTrade it;
    class="type">bool             bIsMovingSelect,
                     bIsBuy;
    class="type">class="kw">ulong            ticket;
    class="type">class="kw">double           vol,
                     pr,
                     tp,
                     sl;
}m_InfoSelection;
class="type">void SetTextValue(class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">class="kw">double value0, class="type">class="kw">double value1 = class="num">0.0, class="type">class="kw">double priceOpen = class="num">0.0)
{
    class="type">class="kw">double finance;
    class="kw">switch (it)
    {
        case IT_RESULT  :
            PositionAxlePrice(ticket, it, priceOpen);
            PositionAxlePrice(ticket, IT_PENDING, class="num">0);
            m_EditInfo2.SetTextValue(MountName(ticket, it, EV_PROFIT), value1);
        case IT_PENDING:
            value0 = value0 / Terminal.GetVolumeMinimal();
            m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), value0, def_ColorVolumeEdit);
            if (!m_InfoSelection.bIsMovingSelect) m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT, true), value0, def_IndicatorGhostColor);
            class="kw">break;
        case IT_TAKE    :
        case IT_STOP    :
            finance = (value1 / Terminal.GetAdjustToTrade()) * value0;
            m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), finance);
            if (!m_InfoSelection.bIsMovingSelect) m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT, true), finance, def_IndicatorGhostColor);
            class="kw">break;
    }
}
class="type">void MoveSelection(class="type">class="kw">double price)
{

◍ 拖拽止损止盈时的挂单重算逻辑

在 MT5 自定义控件里,用户鼠标拖拽一条 TP/SL 线时,底层要先判断当前是不是挂单(pending),因为挂单的开仓价 pr 和成交后的市价不是同一个参照系。若是市价单,直接把拖到的 price 写进 ModifyPosition;若是挂单,则必须以挂单原始开仓价为锚,重算 tp/sl 相对偏移。 下面这段分支处理了 IT_PENDING 情形:tp 和 sl 若原值为 0 则保持 0,否则用新拖拽价 price 加上原偏移(m_InfoSelection.tp - m_InfoSelection.pr)得到绝对价。例如原挂单开仓价 1.1000、原 TP 1.1050(偏移 +50 点),拖到 1.1020 时,新 TP 自动变为 1.1070,偏移量不变。 tp = (m_InfoSelection.tp == 0 ? 0 : price + m_InfoSelection.tp - m_InfoSelection.pr); sl = (m_InfoSelection.sl == 0 ? 0 : price + m_InfoSelection.sl - m_InfoSelection.pr); SetPriceSelection 里先用 OrderSelect 确认票据有效性,随即把 bIsMovingSelect 置 false 并调用 SpotLight() 取消高亮。外汇与贵金属杠杆品种跳空频繁,挂单 TP/SL 重算后务必在策略测试器用历史跳空时段跑一遍,验证偏移不会被服务器拒绝。

MQL5 / C++
class="type">class="kw">double tp, sl;
              if (!m_InfoSelection.bIsMovingSelect) class="kw">return;
              class="kw">switch (m_InfoSelection.it)
              {
                case IT_TAKE:
                    UpdateInfosIndicators(class="num">0, m_InfoSelection.ticket, m_InfoSelection.pr, price, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
                    class="kw">break;
                case IT_STOP:
                    UpdateInfosIndicators(class="num">0, m_InfoSelection.ticket, m_InfoSelection.pr, m_InfoSelection.tp, price, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
                    class="kw">break;
                case IT_PENDING:
                    tp = (m_InfoSelection.tp == class="num">0 ? class="num">0 : price + m_InfoSelection.tp - m_InfoSelection.pr);
                    sl = (m_InfoSelection.sl == class="num">0 ? class="num">0 : price + m_InfoSelection.sl - m_InfoSelection.pr);
                    UpdateInfosIndicators(class="num">0, m_InfoSelection.ticket, price, tp, sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
                    class="kw">break;
              }
}
class="type">void SetPriceSelection(class="type">class="kw">double price)
{
              class="type">bool isPending;
              if (!m_InfoSelection.bIsMovingSelect) class="kw">return;
              isPending = OrderSelect(m_InfoSelection.ticket);
              m_InfoSelection.bIsMovingSelect = class="kw">false;
              m_TradeLine.SpotLight();
              class="kw">switch (m_InfoSelection.it)
              {
                case IT_TAKE:
                    if (isPending) ModifyOrderPendent(m_InfoSelection.ticket, m_InfoSelection.pr, price, m_InfoSelection.sl);
                    else ModifyPosition(m_InfoSelection.ticket, price, m_InfoSelection.sl);
                    class="kw">break;
                case IT_STOP:
                    if (isPending) ModifyOrderPendent(m_InfoSelection.ticket, m_InfoSelection.pr, m_InfoSelection.tp, price);
                    else ModifyPosition(m_InfoSelection.ticket, m_InfoSelection.tp, price);
                    class="kw">break;

「挂单改价时的偏移重算逻辑」

在挂单(IT_PENDING)分支里,ModifyOrderPendent 的 TP/SL 并非直接写入原值,而是用新挂单价 price 减去原开仓价 m_InfoSelection.pr,再叠加原 TP/SL 距离。这样当拖拽修改挂单价格时,盈利与止损的绝对间距会被保留,不会因价格平移而错位。 下方 DispatchMessage 函数是鼠标交互的入口。CHARTEVENT_MOUSE_MOVE 下先用 Mouse.GetPositionDP 取光标时间 dt 与价格 price,再用 GetButtonStatus 拿到按键掩码 mKeys:0x01 是左键点击,0x04 是 Shift(倾向买),0x08 是 Ctrl(倾向卖)。 当 bKeyBuy 与 bKeySell 不相等时进入挂单挂载判断;若 bMounting 为 false,则隐藏鼠标并通过 Chart.GetBaseFinance 计算基础仓位 vol 及 tp/sl 数值。外汇与贵金属杠杆高,这类一键挂单脚本若偏移算错,可能瞬间放大风险敞口,建议先在 MT5 策略测试器用迷你手数验证。

MQL5 / C++
case IT_PENDING:
            ModifyOrderPendent(m_InfoSelection.ticket, price, (m_InfoSelection.tp == class="num">0 ? class="num">0 : price + m_InfoSelection.tp - m_InfoSelection.pr), (m_InfoSelection.sl == class="num">0 ? class="num">0 : price + m_InfoSelection.sl - m_InfoSelection.pr));
            class="kw">break;
      }
}
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">class="kw">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 = class="kw">false, bIsDT = class="kw">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">//Pressed SHIFT
                  bKeySell = (mKeys & 0x08) == 0x08;    class=class="str">"cmt">//Pressed CTRL
                  if (bKeyBuy != bKeySell)
                  {
                        if (!bMounting)
                        {
                              Mouse.Hide();
                              bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);

挂单挂载时的价格与止损校准

在挂单挂载逻辑里,TP 与 SL 不是直接写死价格,而是先按成交量做归一化再交由终端校准。代码片段里 valueTp 与 valueSl 都乘了 Terminal.GetAdjustToTrade() 再除以 m_InfoSelection.vol,意味着同品种不同手数下,挂单的盈亏距离会按比例缩放,避免大仓位把止损顶到非法价位。 校准后通过 m_TradeLine.SpotLight 高亮指示线,并将 m_InfoSelection 的 it 设为 IT_PENDING、ticket 绑定到 def_IndicatorTicket0,同时打开 bIsMovingSelect 标志位,让鼠标拖动能实时改价。此时 m_InfoSelection.pr 记录触发价,bMounting 置真表示处于挂载态。 实际落点价是分多空算的:m_InfoSelection.tp = pr + (bKeyBuy ? valueTp : -valueTp),sl 则反向。若收到单击事件且 memLocal 为 0,会调 MoveSelection(0) 清拖动态,再跑 CreateOrderPendent 按校准后的 vol、买卖方向、触发价与偏移 TP/SL 发挂单——外汇与贵金属杠杆高,这类自动缩放若手数算错可能瞬间放大风险敞口,建议开 MT5 用脚本打印不同 vol 下的 valueTp 验证。

MQL5 / C++
valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
m_TradeLine.SpotLight(MountName(def_IndicatorTicket0, IT_PENDING, EV_LINE));
m_InfoSelection.it = IT_PENDING;
m_InfoSelection.ticket = def_IndicatorTicket0;
m_InfoSelection.bIsMovingSelect = true;
m_InfoSelection.pr = price;
bMounting = true;
}
m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
m_InfoSelection.bIsBuy = bKeyBuy;
MoveSelection(price);
if ((bEClick) && (memLocal == class="num">0))
{
   MoveSelection(class="num">0);
   m_InfoSelection.bIsMovingSelect = class="kw">false;
   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)
{
   MoveSelection(class="num">0);

◍ 鼠标拖拽与图表事件里的挂单状态复位

在自定义面板里,当用户结束拖拽画线或取消挂载状态,需要把内部选择标记强制归位。下面这段逻辑先把 bIsMovingSelect 置 false、调用 Mouse.Show() 恢复光标,并把本地内存标记 memLocal 与 bMounting 同时清零,避免在下次点击事件里误判上一次的拖拽上下文。 m_InfoSelection.bIsMovingSelect = false; Mouse.Show(); memLocal = 0; bMounting = false; 如果当前并未处于挂载态且买卖键状态一致(bKeyBuy == bKeySell),则根据是否单击来分流:bEClick 为真就 SetPriceSelection(price) 定价格,否则 MoveSelection(price) 移动已有选择线。这种分支能保证在不冲突按键时,单击与拖拽有不同语义。 图表对象被删除时(CHARTEVENT_OBJECT_DELETE),先从 sparam 解析指标信息,成功则 CreateIndicatorTrade 重建交易对象并 GetInfosTradeServer 拉一次服务器持仓数据,随后用 UpdateInfosIndicators(0, ticket, ...) 以 0 为偏移刷新挂单的 PR/TP/SL/Volume 与方向字段。 图表尺寸或属性变化(CHARTEVENT_CHART_CHANGE)会触发 ChartSetInteger(ChartID(), CHART_SHOW_OBJECT_DESCR, false) 关掉对象描述浮层,再 ReDrawAllsIndicator() 重绘全部指标图形,防止缩放后残留旧坐标。对象点击(CHARTEVENT_OBJECT_CLICK)则二次解析信息后按 ev 子事件分流,例如 EV_CLOSE 会先 GetInfosTradeServer 拿返回码 cRet,非 0 才继续按 it 类型处理。

MQL5 / C++
m_InfoSelection.bIsMovingSelect = class="kw">false;
Mouse.Show();
memLocal = class="num">0;
bMounting = class="kw">false;
}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
   if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}
class="kw">break;
case CHARTEVENT_OBJECT_DELETE:
   if (GetIndicatorInfos(sparam, ticket, price, it, ev))
   {
      CreateIndicatorTrade(ticket, it);
      GetInfosTradeServer(ticket);
      m_InfoSelection.bIsMovingSelect = class="kw">false;
      UpdateInfosIndicators(class="num">0, ticket, m_InfoSelection.pr, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
   }
   class="kw">break;
case CHARTEVENT_CHART_CHANGE:
   ChartSetInteger(ChartID(), CHART_SHOW_OBJECT_DESCR, class="kw">false);
   ReDrawAllsIndicator();
   class="kw">break;
case CHARTEVENT_OBJECT_CLICK:
   if (GetIndicatorInfos(sparam, ticket, price, it, ev)) class="kw">switch (ev)
   {
      case EV_CLOSE:
         if ((cRet = GetInfosTradeServer(ticket)) != class="num">0) class="kw">switch (it)

「挂单与止损线拖拽的事件分支」

这段 switch 逻辑处理的是图表交互里两类典型对象:挂单/成交回报,以及止盈止损线。当事件类型是 IT_PENDING 或 IT_RESULT,说明用户在点 pending 单或结果对象;此时若 cRet 小于 0,就清掉挂单 RemoveOrderPendent(ticket),否则平掉对应持仓 ClosePosition(ticket)。 遇到 IT_TAKE 或 IT_STOP,意味着在拖 TP/SL 线。代码把 bIsMovingSelect 置为 true,并调用 SetPriceSelection(0) 锁定当前价格锚点,等鼠标松手再落位。 EV_MOVE 分支里只要 bIsMovingSelect 为真,就让 m_TradeLine.SpotLight() 高亮交易线,随后复位标志位。你开 MT5 把这段塞进自己的图表事件回调,拖一下黄金的 SL 线就能看到高亮触发;外汇和贵金属杠杆高,拖线改仓位是高风险操作,信号仅作辅助。

MQL5 / C++
{
   case IT_PENDING:
   case IT_RESULT:
      if (cRet < class="num">0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
      class="kw">break;
   case IT_TAKE:
   case IT_STOP:
      m_InfoSelection.bIsMovingSelect = true;
      SetPriceSelection(class="num">0);
      class="kw">break;
}
class="kw">break;
case EV_MOVE:
   if (m_InfoSelection.bIsMovingSelect)
   {
      m_TradeLine.SpotLight();
      m_InfoSelection.bIsMovingSelect = class="kw">false;
   }else

选中持仓时点亮对应的交易线

在订单选中事件的处理分支里,把 ticket 与订单类型 it 写进 m_InfoSelection 结构体,是后续高亮逻辑的前提。 下面这段代码片段只做一件事:若服务端返回该 ticket 的成交信息非零,就判定为“正在移动选中”,并调用 SpotLight 把对应名称的线提亮。 [CODE] { m_InfoSelection.ticket = ticket; m_InfoSelection.it = it; if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0)) m_TradeLine.SpotLight(MountName(ticket, it, EV_LINE)); } break; break; } [/CODE] 逐行看:前两句赋值选中标的;第三句把服务端查询结果与布尔标记绑死,注意这里用的是赋值号而非判等;第四句在条件成立时让交易线发光。 外汇与贵金属品种波动剧烈,这类可视化辅助仅降低盯盘遗漏概率,不预示任何方向。开 MT5 把这段塞进你的 COrder 事件回调,跑一下就能看到选中单子时线变亮。

MQL5 / C++
{
   m_InfoSelection.ticket = ticket;
   m_InfoSelection.it = it;
   if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != class="num">0))
      m_TradeLine.SpotLight(MountName(ticket, it, EV_LINE));
}
class="kw">break;
   class="kw">break;
}

◍ 一点提醒

这套订单系统 EA 的原文示例已附带可下载的 ZIP 包(约 12 MB),里面是完整工程,开 MT5 直接挂到 EURUSD 这类连续合约上就能跑通基础下单逻辑。若你试的是 US30 或 S&P500 这类带到期日的品种,原作者明确说过要在 C_Terminal 类的 CurrentSymbol 里补合约搜索规则,否则会报 10015 价格计算失败。 外汇与贵金属杠杆高、滑点和合约规则差异大,改完代码先在策略测试器用历史数据验证,别直接上真实账户。作者后续文章已重写了更简洁的版本,旧工程仅适合理清订单系统骨架,不建议当作终稿照搬。

常见问题

这是未区分幻影标签与真实挂单所致。需在接收图表对象删除事件开关里,只清理已无对应挂单 ID 的图形对象,避免残留。
拖动挂单线的状态翻转逻辑要在鼠标事件松手分支里显式复位高亮标志,否则会卡在拖拽态。检查鼠标事件处理函数是否漏了状态清理。
可以。小布能扫描图表对象与当前挂单列表的差异,标出无对应真实挂单的幻影标签,并提示你一键清理。
需接收鼠标事件拿到坐标,生成 pending 单对象并绑定图形线;拖拽时切高亮,松手按线位置发挂单指令,成交后拆开标签与单子。
关掉后程序收不到对象被删的通知,真实挂单删了标签也不会清,图上会堆满过期标签,干扰后续挂单判断。