从头开始开发智能交易系统(第 26 部分):面向未来(I)·综合运用
📘

从头开始开发智能交易系统(第 26 部分):面向未来(I)·综合运用

第 3/3 篇

◍ 挂单浮标控件的事件绑定与尺寸分配

这段片段在指标面板初始化里集中处理挂单类型(IT_PENDING)的浮标对象,先把关闭、移动、编辑、背景、趋势线五类事件统一交给 macroSwapAtFloat 切换,再解除编辑框只读锁。注意 m_EditInfo1.SetOnlyRead(..., false) 只针对 IT_PENDING 的 EV_EDIT,意味着挂单信息框允许手动改值,而结果类或普通类未必放开。 随后用宏 macroCreateIndicator 批量建对象:趋势线、背景、编辑框分别 Create,背景尺寸按类型分三档——IT_RESULT 给 84×34,IT_PENDING 给 108×22,其余给 92×22;编辑框固定 60×14。这组硬编码像素值直接决定 MT5 图表上控件的占位,若你做多币种适配,108 宽在 EURUSD 可能够用,但在 XAUUSD 高点差行情下编辑框容易被价格标签挤偏。 外汇与贵金属杠杆高、滑点随机,这类 UI 尺寸bug不会爆仓但会误读信号;开 MT5 把这段宏贴进 EA 面板类,改 IT_PENDING 的背景宽为 120 看是否缓解遮挡。

MQL5 / C++
macroSwapAtFloat(it, EV_CLOSE);
macroSwapAtFloat(it, EV_MOVE);
macroSwapAtFloat(it, EV_EDIT);
macroSwapAtFloat(it, EV_GROUND);
macroSwapAtFloat(it, EV_LINE);
m_EditInfo1.SetOnlyRead(macroMountName(def_IndicatorFloat, IT_PENDING, EV_EDIT), class="kw">false);
class="kw">return true;
class="macro">#undef macroSwapAtFloat
class="macro">#if (macroGetLinePrice(def_IndicatorFloat, IT_PENDING) > class="num">0) class="kw">return class="kw">false;
class="macro">#define macroCreateIndicator(A, B, C, D) \
   m_TradeLine.Create(ticket, sz0 = macroMountName(ticket, A, EV_LINE), C); \
   m_BackGround.Create(ticket, sz0 = macroMountName(ticket, A, EV_GROUND), B); \
   m_BackGround.Size(sz0, (A == IT_RESULT ? class="num">84 : (A == IT_PENDING ? class="num">108 : class="num">92)), (A == IT_RESULT ? class="num">34 : class="num">22)); \
   m_EditInfo1.Create(ticket, sz0 = macroMountName(ticket, A, EV_EDIT), D, class="num">0.0); \
   m_EditInfo1.Size(sz0, class="num">60, class="num">14);

「挂单面板上移动按钮与盈利标签的判定分支」

这段逻辑处理的是交易面板里两种界面元素的创建:当订单状态不是 IT_RESULT 时,走移动按钮分支;否则走盈利信息标签分支。外汇与贵金属挂单的高杠杆特性下,这类界面状态错判可能导致误操作,建议先在策略测试器里跑通再上真实账户。 移动按钮用 Wingdings 字体、字符 'u'、字号 17 来画,尺寸锁在 21×23 像素;盈利标签则不设背景色(clrNONE)、字号 0,尺寸 60×14。两个分支都靠 macroMountName 拼出唯一对象名,避免同图表多单重叠。 CreateIndicator 函数按 eIndicatorTrade 枚举做 switch:IT_TAKE 分支直接调 macroCreateIndicator,传入森林绿、深绿、透明三色参数。你可以把这段抄进自己的面板类,改 IT_TAKE 的颜色参数看视觉反馈差异。

MQL5 / C++
if (A != IT_RESULT)      {
   m_BtnMove.Create(ticket, sz0 = macroMountName(ticket, A, EV_MOVE), "Wingdings", "u", class="num">17, C);
   m_BtnMove.Size(sz0, class="num">21, class="num">23);
                 }else               { 
   m_EditInfo2.Create(ticket, sz0 = macroMountName(ticket, A, EV_PROFIT), clrNONE, class="num">0.0); 
   m_EditInfo2.Size(sz0, class="num">60, class="num">14); }
                 }
      class="type">void CreateIndicator(class="type">class="kw">ulong ticket, eIndicatorTrade it)
            {
                  class="type">class="kw">string sz0;
                  
                  class="kw">switch (it)
            {
                        case IT_TAKE     : macroCreateIndicator(it, clrForestGreen, clrDarkGreen, clrNONE); class="kw">break;

挂单与止损的面板着色分支

这段分支逻辑处理的是交易对象在面板上的视觉映射:止损单 IT_STOP 用砖红配栗色画指标线,挂单 IT_PENDING 则走蓝调配色,并额外挂一个可勾选按钮。 注意 IT_PENDING 分支里那两行绿底代码:m_BtnCheck.Create 用 macroMountName 拼出按钮名,def_BtnCheckEnabled / def_BtnCheckDisabled 是两种状态图;随后 SetStateButton(sz0, true) 直接把按钮初始置为启用。这意味着挂单在创建时就默认允许用户在面板上切换显示,和止损、结果线(IT_RESULT 用深蓝)的纯绘制逻辑不同。 宏 def_AdjustValue(A) 把偏移量折算成绝对价:当 A 为 0 时返回 0,否则用 price + A - m_Selection.pr 修正,避免切换品种时挂单价错位。macroForceNewType 则先 RemoveOrderPendent 清掉旧挂单再重画,防止重复堆叠。 开 MT5 把这段塞进你的 EA 面板类,改 clrCornflowerBlue 为别的色值,能直观区分不同经纪商挂单流。外汇与贵金属波动剧烈,面板逻辑只降低误操作概率,不消除爆仓风险。

MQL5 / C++
case IT_STOP    : macroCreateIndicator(it, clrFireBrick, clrMaroon, clrNONE); class="kw">break;
case IT_PENDING:
   macroCreateIndicator(it, clrCornflowerBlue, clrDarkGoldenrod, def_ColorVolumeEdit);
   m_BtnCheck.Create(ticket, sz0 = macroMountName(ticket, it, EV_CHECK), def_BtnCheckEnabled, def_BtnCheckDisabled);
   m_BtnCheck.SetStateButton(sz0, true);
   class="kw">break;
case IT_RESULT  : macroCreateIndicator(it, clrDarkBlue, clrDarkBlue, def_ColorVolumeResult); class="kw">break;
   }
   m_BtnClose.Create(ticket, macroMountName(ticket, it, EV_CLOSE), def_BtnClose);
}
class="macro">#undef macroCreateIndicator
class="macro">#define def_AdjustValue(A) (A == class="num">0 ? class="num">0 : price + A - m_Selection.pr)
class="macro">#define macroForceNewType  {                                                                       \
   RemoveOrderPendent(m_Selection.ticket);                                                                                      \

◍ 挂单落地的两段式清理与重建

在鼠标选价后,系统先清掉旧指标对象再下挂单,避免图表上残留虚拟标尺。RemoveIndicator(m_Selection.ticket) 负责注销被选中的临时指标,紧接着 CreateOrderPendent 用选定成交量、买卖方向、价格与经 def_AdjustValue 校准的止盈止损重建挂单。 SetPriceSelection 这个函数承接了选价回调。若 m_Selection.ticket 为 0 直接 return,说明没有有效选择;若等于 def_IndicatorTicket0,则走特殊分支:先建挂单、再删该指标、随后 return,不再执行后续通用逻辑。 实盘里外汇与贵金属波动剧烈、杠杆风险高,这类脚本只解决‘选价即下单’的交互,不保证成交价优于预期,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍,观察 ticket 为 0 时是否真的跳过下单。

MQL5 / C++
RemoveIndicator(m_Selection.ticket);
CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, def_AdjustValue(m_Selection.tp), def_AdjustValue(m_Selection.sl), m_Selection.bIsDayTrade);
class="kw">break;
}
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, def_AdjustValue(m_Selection.tp), def_AdjustValue(m_Selection.sl), m_Selection.bIsDayTrade);
      RemoveIndicator(def_IndicatorTicket0);
      class="kw">return;
   }

「挂单拖拽时的止损止盈重算」

当图表上被拖动的虚拟对象 ticket 等于 def_IndicatorFloat 时,说明用户正在移动一条用于批量改价的指示线,而非真实订单。此时进入 switch 按对象类型分流:止损线(IT_STOP)只改 sl,止盈线(IT_TAKE)只改 tp,而挂单线(IT_PENDING)则要把 sl、tp 和开仓价 pr 一并刷新。 对挂单分支,sl 与 tp 不是直接赋值为 price,而是先过一遍 def_AdjustValue() 做偏移校正,再写回 m_Selection,最后 pr = price 锁定挂单触发价。这一层处理能避免把指示线绝对坐标误当成挂单距离。 分流结束后无论哪种类型,都把 m_Selection.ticket 清 0、调 m_TradeLine.SpotLight() 重绘高亮,然后 return。开 MT5 把这段塞进你的拖拽回调里,拖一条挂单线就能看到 sl/tp 跟随重算,外汇与贵金属品种杠杆高,误改挂单价可能瞬间放大风险敞口。

MQL5 / C++
if (m_Selection.ticket == def_IndicatorFloat)
{
  class="kw">switch(m_Selection.it)
  {
    case IT_STOP   : m_Selection.sl = price; class="kw">break;
    case IT_TAKE   : m_Selection.tp = price; class="kw">break;
    case IT_PENDING:
      m_Selection.sl = def_AdjustValue(m_Selection.sl);
      m_Selection.tp = def_AdjustValue(m_Selection.tp);
      m_Selection.pr = price;
      class="kw">break;
  }
  m_Selection.ticket = class="num">0;
  m_TradeLine.SpotLight();
  class="kw">return;
}

挂单与持仓的拖拽改价分支

这段逻辑处理的是图表上拖拽止盈、止损或挂单线后的实际改写动作。先通过 GetInfosTradeServer 拿到票据的挂单状态,返回 0 就直接 return,避免对无效票据继续操作;随后 m_TradeLine.SpotLight() 负责高亮当前被拖动的线。 switch 按 m_Selection.it 区分三种交互类型:IT_TAKE 是改止盈,IT_STOP 是改止损,IT_PENDING 是改挂单本身。对于前两者,用 Pending 的正负判断票据是挂单还是已持仓——Pending<0 走 ModifyOrderPendent,否则走 ModifyPosition,参数顺序分别是(ticket, 开价/对手价, 新TP或SL, 另一头不变项)。 IT_PENDING 分支里先取 ORDER_TYPE,只处理 BUY_LIMIT 与 SELL_LIMIT 两类限价单。接着用 SymbolInfoDouble 取当前市场价:若 m_Selection.bIsBuy 为真取 SYMBOL_ASK,否则取 SYMBOL_BID,存入 last 变量作为后续重算挂单距离的基准。外汇与贵金属杠杆高,拖拽改价若跨点差过大可能直接被服务器拒单,建议在 MT5 策略测试器里用真实点差回放验证。

MQL5 / C++
if ((Pending = GetInfosTradeServer(m_Selection.ticket)) == class="num">0) class="kw">return;
   m_TradeLine.SpotLight();
   class="kw">switch (m_Selection.it)
   {
      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));

◍ 图表点击事件里的挂单回收逻辑

在 MT5 的 EA 面板交互里,CHARTEVENT_OBJECT_CLICK 是用户点掉指标或订单标签的入口。代码通过 GetIndicatorInfos 拿到 sparam 对应的 ticket 与类型,再按 EV_CLOSE 分支决定是删指标还是处理交易对象。 若 ticket 等于 def_IndicatorFloat,直接 RemoveIndicator 清掉浮动指标;否则走 GetInfosTradeServer 查服务端状态。cRet 非 0 时按 IT_PENDING / IT_RESULT 区分:cRet 小于 0 说明挂单已失效,调 RemoveOrderPendent 撤单,否则 ClosePosition 平掉持仓。 IT_TAKE 与 IT_STOP 分支不立即删除,而是把 ticket 和类型写进 m_Selection,再 SetPriceSelection(0) 让脚本进入价格重选状态,等下一轮条件判断去改 TP/SL 或强制新建类型。外汇与贵金属品种点差跳变频繁,这类服务端状态回查能避免对已成废单发修改指令导致 4756 报错。

MQL5 / C++
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=class="str">"cmt">// ... Internal code...
   case CHARTEVENT_OBJECT_CLICK:
      if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev)
      {
         case EV_CLOSE:
            if (ticket == def_IndicatorFloat) RemoveIndicator(def_IndicatorFloat, it);
            else 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);
                  class="kw">break;
               case IT_TAKE:
               case IT_STOP:
                  m_Selection.ticket = ticket;
                  m_Selection.it = it;
                  SetPriceSelection(class="num">0);
                  class="kw">break;
            }
      }
}

「挂单事件里的指标对象切换逻辑」

这段 switch 分支处理的是图表上指标浮层触发的三类事件:MOVE、CHECK 以及前面的 break 出口。当 ticket 不等于 def_IndicatorFloat 时,系统倾向于把它当作幽灵指标创建,而不是直接写入选中结构。 EV_MOVE 分支里,若 ticket 匹配 def_IndicatorFloat,就把 ticket 和迭代器 it 写入 m_Selection;否则调用 CreateGhostIndicator 生成临时指示层。EV_CHECK 分支更关键:非浮层 ticket 会先查 PendingAtFloat,命中就撤掉挂单,未命中则把对应按钮状态置 true。 当 ticket 正是 def_IndicatorFloat 时,代码把 def_IndicatorTicket0 与 IT_PENDING 塞进 m_Selection,并用 macroGetLinePrice 抓取挂单价、止损价、止盈价。方向判定靠一行:pr 小于 tp 或 sl 小于 pr 即判为买侧,外汇与贵金属品种在此处波动剧烈,误判可能直接吃滑点。 最后顺手把成交量按 EditInfo 文本乘最小交易量算出,调一次 SetPriceSelection 再 RemoveIndicator 清掉浮层。开 MT5 把这段贴进 EA 的事件回调,改 def_IndicatorFloat 的匹配条件就能验证切换是否如预期。

MQL5 / C++
            class="kw">break;
            case EV_MOVE:
                  if (ticket == def_IndicatorFloat)
                  {
                  m_Selection.ticket = ticket;
                  m_Selection.it = it;
                  }else  CreateGhostIndicator(ticket, it);
                  class="kw">break;
            case EV_CHECK:
                  if (ticket != def_IndicatorFloat)
                  {
                  if (PendingAtFloat(ticket)) RemoveOrderPendent(ticket);
                  else m_BtnCheck.SetStateButton(macroMountName(ticket, IT_PENDING, EV_CHECK), true);
                  } else
                  {
                  m_Selection.ticket = def_IndicatorTicket0;
                  m_Selection.it = IT_PENDING;
                  m_Selection.pr = macroGetLinePrice(def_IndicatorFloat, IT_PENDING);
                  m_Selection.sl = macroGetLinePrice(def_IndicatorFloat, IT_STOP);
                  m_Selection.tp = macroGetLinePrice(def_IndicatorFloat, IT_TAKE);
                  m_Selection.bIsBuy = (m_Selection.pr < m_Selection.tp) || (m_Selection.sl < m_Selection.pr);
                  m_Selection.bIsDayTrade = true;
                  m_Selection.vol = m_EditInfo1.GetTextValue(macroMountName(def_IndicatorFloat, IT_PENDING, EV_EDIT)) * Terminal.GetVolumeMinimal();
                  SetPriceSelection(m_Selection.pr);
                  RemoveIndicator(def_IndicatorFloat);
                  }
class=class="str">"cmt">// ... Rest of the code...
m_Selection.ticket = def_IndicatorTicket0;
m_Selection.it = IT_PENDING;
m_Selection.pr = macroGetLinePrice(def_IndicatorFloat, IT_PENDING);
m_Selection.sl = macroGetLinePrice(def_IndicatorFloat, IT_STOP);

记住这一条就够了

上面那段赋值把持仓方向判定收敛成一行逻辑:当挂单价低于止盈价,或止损价低于当前价,就倾向判为多单;同时把 m_Selection.bIsDayTrade 钉死为 true,意味着这套选择只跑日内。 具体成交量用 Edit 控件文本乘上 Terminal.GetVolumeMinimal() 得到,最小交易单位直接取自终端,避免手填手数越界报错。 最后 SetPriceSelection 落价、RemoveIndicator 清掉浮动指标——整条链路跑完,MT5 上你只需核对 IT_TAKE 映射的线价是否实时刷新,其余交给脚本。外汇与贵金属波动剧烈,日内策略同样带本金回撤风险,参数验证请在模拟盘先跑。

MQL5 / C++
m_Selection.tp = macroGetLinePrice(def_IndicatorFloat, IT_TAKE);
m_Selection.bIsBuy = (m_Selection.pr < m_Selection.tp) || (m_Selection.sl < m_Selection.pr);
m_Selection.bIsDayTrade = true;
m_Selection.vol = m_EditInfo1.GetTextValue(macroMountName(def_IndicatorFloat, IT_PENDING, EV_EDIT)) * Terminal.GetVolumeMinimal();
SetPriceSelection(m_Selection.pr);
RemoveIndicator(def_IndicatorFloat);

常见问题

在控件初始化时注册鼠标移动与点击回调,按挂单价格区间等比划分纵向像素,避免重叠;可先写死最小高度再动态调整。
当挂单处于可成交区间且未触发时显示移动按钮;盈利标签仅在挂单潜在利润超过设定阈值才高亮,否则置灰。
可以。小布能读取你的挂单与拖拽改价记录,自动重算止损止盈并标出偏离预设规则的异常单,省去肉眼核对。
先释放旧控件资源防内存泄漏,再按最新行情重建面板,可避免价格跳空时界面卡死或显示残留。
用独立状态标志位标记拖拽对象类型,在鼠标松开事件里走各自重算逻辑,别共用同一改价函数。