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

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

(3/3)·删掉的TP/SL想在原图复原却总踩坑?本篇只靠EA自身逻辑交付一套可拖拽的恢复方案

含代码示例偏理论 第 3/3 篇
手动在图表补回被删除的止盈止损,常被当成机械重复劳动,实则暗藏顺序错误风险。MT5原生票据系统不够快,也容易在操作中出错。用EA内部逻辑接管这套恢复动作,才可能把慢且易错变成可控流程。

◍ 用鼠标事件挂待选单的逻辑入口

在 MT5 自定义指标或 EA 里,想用鼠标在图上直接摆挂单,核心入口是图表事件回调函数 DispatchMessage。它接收 id、lparam、dparam、sparam,其中 id 决定事件类型,鼠标移动对应 CHARTEVENT_MOUSE_MOVE。 进入该分支后,先通过 Mouse.GetPositionDP 拿到光标处的日期时间和价格,再用 Mouse.GetButtonStatus 取按键掩码。掩码按位与:0x01 是左键点击,0x04 是 Shift 按下,0x08 是 Ctrl 按下。 代码里用 bKeyBuy 和 bKeySell 区分方向——Shift 和 Ctrl 不能同时算,所以判断 if (bKeyBuy != bKeySell) 才继续。若当前不在挂载中(!bMounting),会算基础仓位 finance、把 tp/sl 按成交量缩放并吸附到可交易价位,再把选择对象标记成挂单(IT_PENDING)并记下价格。 外汇和贵金属杠杆高,这种鼠标下单只是交互辅助,真下单前务必核对保证金与滑点风险。

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="type">class="kw">ulong   ticket;
   class="type">class="kw">double  price;
   class="type">bool    bKeyBuy,
class=class="str">"cmt">// ... Internal code ....

   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 button 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)
               {
                  m_Selection.bIsDayTrade = Chart.GetBaseFinance(m_Selection.vol, valueTp, valueSl);
                  valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_Selection.vol);
                  valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_Selection.vol);
                  m_Selection.it = IT_PENDING;
                  m_Selection.pr = price;
               }
               m_Selection.tp = (valueTp == class="num">0 ? class="num">0 : m_Selection.pr + (bKeyBuy ? valueTp : (-valueTp)));
               m_Selection.sl = (valueSl == class="num">0 ? class="num">0 : m_Selection.pr + (bKeyBuy ? (-valueSl) : valueSl));
               m_Selection.bIsBuy = bKeyBuy;

挂单指标挂载与价格联动的分支逻辑

这段交互逻辑处理的是图表上挂单指标(ticket=def_IndicatorTicket0)的挂载、卸载与价格跟随。核心靠 bMounting 这个布尔状态机切换:未挂载时点击买入键先置按钮状态、把指标加入图表并把 bMounting 翻成 true,已挂载时再触发则 RemoveIndicator 并清零 memLocal。 当既没挂载、又处于双键同态(bKeyBuy==bKeySell)且当前选择 ticket 大于幽灵值 def_IndicatorGhost 时,走纯选择移动分支:有鼠标事件就 SetPriceSelection 记鼠标价,否则 MoveSelection 跟随实时 price。这种分支划分能避免重复挂指标造成的重绘抖动。 UpdateIndicators 里用 macroGetPrice 取 IT_RESULT 的 EV_LINE 价格,若返回 0 则 bPen 置真并尝试 macroGetPrice 取 IT_PENDING 的 EV_MOVE;两者皆 0 才 CreateIndicator 补建。实盘跑 MT5 时,把 def_IndicatorGhost 设成 0、def_IndicatorTicket0 设成自定义大数(如 100001),能在日志里直接看出分支命中次数。外汇与贵金属杠杆高,这类自动挂单辅助逻辑仅降低操作延迟,不预示胜率,请先在策略测试器验证。

MQL5 / C++
m_BtnInfoType.SetStateButton(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_TYPE), bKeyBuy);
if (!bMounting)
{
   IndicatorAdd(m_Selection.ticket = def_IndicatorTicket0);
   bMounting = true;
}
MoveSelection(price);
if ((bEClick) && (memLocal == class="num">0)) SetPriceSelection(memLocal = price);
}else if (bMounting)
{
   RemoveIndicator(def_IndicatorTicket0);
   memLocal = class="num">0;
   bMounting = class="kw">false;
}else if ((!bMounting) && (bKeyBuy == bKeySell) && (m_Selection.ticket > def_IndicatorGhost))
{
   if (bEClick) SetPriceSelection(m_Selection.MousePrice); else MoveSelection(price);
}
class="kw">break;
class=class="str">"cmt">// ... Rest of the code...
class="type">void UpdateIndicators(class="type">class="kw">ulong ticket, class="type">class="kw">double tp, class="type">class="kw">double sl, class="type">class="kw">double vol, class="type">bool isBuy)
{
   class="type">class="kw">double pr;
   class="type">bool b0, bPen = true;
   if (ticket == def_IndicatorGhost) pr = m_Selection.pr; else
   {
      bPen = (pr = macroGetPrice(ticket, IT_RESULT, EV_LINE)) == class="num">0;
      if (bPen && (macroGetPrice(ticket, IT_PENDING, EV_MOVE) == class="num">0))
      {
         CreateIndicator(ticket, IT_PENDING);

「挂单与止损止盈线的动态刷新逻辑」

这段内联函数负责在拖拽或修改挂单后,同步刷新图表上的止盈(IT_TAKE)、止损(IT_STOP)以及挂单线本身。核心是先判断价格是否为0:为0代表删除该线并保留移动按钮,非0则创建或更新指标线。 UpdateIndicatorsLimits 里先用 Terminal.GetPointPerTick() 取每跳点值 d1,作为价格边界修正的最小单位。对于 IT_TAKE,若是买单且传入 price 低于参考价 pr,会被强制抬到 pr+d1,避免止盈落在入场下方这种无效配置;卖单则对称处理。 IT_STOP 多一个 isPen 开关:当允许穿透(isPen=true)且止损方向错误时,同样用 d1 修正到 pr 另一侧;不允穿透就原样保留 price。最后无论哪种情况,都会调用 PositionAxlePrice 把轴线价设成 price 或 pr,并用 SetTextValue 写出盈亏距离(买单 price-pr,卖单 pr-price)。 在 MT5 里跑这段代码时,注意 d1 来自品种报价精度,黄金 XAUUSD 的 point per tick 通常是 0.01,若手动改 d1 为固定值会导致止损边界偏移一个 tick 以上。外汇与贵金属杠杆高,这类自动修正只解决逻辑非法,不预示价格走向,实际成交仍可能因滑点偏离。

MQL5 / C++
PositionAxlePrice(ticket, IT_PENDING, m_Selection.pr);
            ChartRedraw();
            }
            pr = (pr > class="num">0 ? pr : macroGetPrice(ticket, IT_PENDING, EV_MOVE));
            SetTextValue(ticket, IT_PENDING, vol);
            }
            b0 = UpdateIndicatorsLimits(ticket, IT_TAKE, tp, vol, pr, isBuy, bPen);
            b0 = (UpdateIndicatorsLimits(ticket, IT_STOP, sl, vol, pr, isBuy, bPen) ? true : b0);
            if (b0) ChartRedraw();
}
class="kw">inline class="type">bool UpdateIndicatorsLimits(class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">class="kw">double price, class="type">class="kw">double vol, class="type">class="kw">double pr, class="type">bool isBuy, class="type">bool isPen)
{
            class="type">bool b0 = class="kw">false;
            class="type">class="kw">double d1 = Terminal.GetPointPerTick();
            
            if (price == class="num">0)
            {
                if (macroGetPrice(ticket, it, EV_LINE) > class="num">0) RemoveIndicator(ticket, it);
                if (macroGetPrice(ticket, it, EV_MOVE) == class="num">0) CreateBtnMoveIndicator(ticket, it);
            } else if (b0 = (macroGetPrice(ticket, it, EV_LINE) == class="num">0 ? true : b0)) CreateIndicator(ticket, it);
            class="kw">switch (it)
            {
                case IT_TAKE:
                    price = (price == class="num">0 ? class="num">0 : (((isBuy ? price - pr : pr - price) > class="num">0) ? price : (isBuy ? pr + d1 : pr - d1)));
                    class="kw">break;
                case IT_STOP:
                    price = (price == class="num">0 ? class="num">0 : (isPen ? (((isBuy ? price - pr : pr - price) < class="num">0) ? price : (isBuy ? pr - d1 : pr + d1)) : price));
                    class="kw">break;
            }
            if (m_Selection.it == it) m_Selection.MousePrice = price;
            PositionAxlePrice(ticket, it, (price == class="num">0 ? pr : price));
            SetTextValue(ticket, it, vol, (isBuy ? price - pr : pr - price));
            
            class="kw">return b0;
}

◍ 画得少,看得清

走到这一步,前面几篇攒下的图形化下单与键盘鼠标混合操作已经能跑通一套接近完整的订单流程,对常见市况也有了基本适应能力。 眼下这套系统还远没到封神的地步,作者放出的 EA_-_Em_diret4o_ao_Futuro_q_III_k.zip 约 11.7 MB,直接丢进 MT5 的 Experts 目录就能看当前阶段的实盘行为和界面反应。 外汇和贵金属杠杆高、滑点猛,拿这套半成品去真金白银前,建议先开策略测试器用历史数据跑几遍,把下单延迟和撤单逻辑摸熟再谈下一步。

把重复劳动交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到TP/SL恢复状态的提示,把重复劳动交给小布,你专注决策。

常见问题

原生单据系统依赖特定操作顺序,人工跟进时容易漏步或错位,而EA内部逻辑可固化流程,降低随机失误概率。
不同开发者思路差异很大,本文展示的是基于现有订单系统EA调整代码、用拖拽等方式实现,无需新增外部元素。
划掉的代码并非永久移除,会在后续合适阶段回归,当前阶段保留反而增加负担,故暂时剔除。
可以,小布盯盘的AIGC模块会标记品种页的TP/SL恢复状态,发现数值偏离时给出提示,减少人工核对成本。
不属于,外汇贵金属属高风险市场,恢复价位仅补全订单参数,实际走向仍受行情概率影响。