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

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

(2/3)· 当平台自带票据系统又慢又易错,如何用 EA 自身在图表上重建止盈止损价位

实战向进阶 第 2/3 篇
很多交易者发现 MT5 原生票据系统恢复被删的 TP/SL 时慢且易错,却一直没在 EA 里补齐这套能力。其实不依赖外部资源,仅靠订单系统 EA 就能在图表上重建破位级别,本篇拆开具体做法。

把移动按钮从指标里剥出来

EA 的 Update 函数只管止盈止损指标的宏替换,但移动按钮必须独立创建,否则图表对象会和订单状态脱节。作者把按钮生成单独抽成 inline 函数,编译器会像宏一样内联展开,避免系统外观不一致。 为了统一配色,代码里用两个宏固定了破位线的颜色:take 用 clrDarkGreen、stop 用 clrMaroon。这样无论只建一个移动按钮还是完整指标,颜色都跟全局一致,不会某张单子按钮变色。 Update 里的宏替换分三步跑:第一,若破位线价格=0(订单删了止盈或止损),先删指标线,若移动对象不存在就建一个,图表上只剩可拖的按钮;第二,若服务器订单有破位级别但该线不在图表,删移动对象、建完整指标放到对应价位;第三,把指标钉到正确点位——如果只剩移动对象,就直接贴到所属订单/持仓的价位,一眼能看出缺了哪边。 外汇和贵金属杠杆高,这类半自动挂单辅助逻辑只降低操作失误概率,不保证成交价优于预期,实盘前务必在 MT5 策略测试器用历史数据跑一遍。

MQL5 / C++
class=class="str">"cmt">// ... class code ...
class="macro">#define def_ColorLineTake       clrDarkGreen
class="macro">#define def_ColorLineStop       clrMaroon
class=class="str">"cmt">// ... class code ....
class="kw">inline class="type">void CreateBtnMoveIndicator(class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">class="kw">color C = clrNONE)
                    {
                        class="type">class="kw">string sz0 = macroMountName(ticket, it, EV_MOVE);
                        ObjectDelete(Terminal.Get_ID(), macroMountName(ticket, it, EV_MOVE));
                        m_BtnMove.Create(ticket, sz0, "Wingdings", "u", class="num">17, (C == clrNONE ? (it == IT_TAKE ? def_ColorLineTake : def_ColorLineStop) : C));
                        m_BtnMove.Size(sz0, class="num">21, class="num">23);
                    }
class=class="str">"cmt">// ... the rest of the class code ...
class="macro">#define macroUpdate(A, B){                                                                                                \
                    if (B == class="num">0) {  if (macroGetPrice(ticket, A, EV_LINE) > class="num">0) RemoveIndicator(ticket, A);                      \
                    if (macroGetPrice(ticket, A, EV_MOVE) == class="num">0) CreateBtnMoveIndicator(ticket, A);                            \
                    } else if (b0 = (macroGetPrice(ticket, A, EV_LINE) == class="num">0 ? true : b0)) CreateIndicator(ticket, A);  \

◍ 挂单无价时的幽灵轴重建

在 UpdateIndicators 里,若传入的 ticket 不是 def_IndicatorGhost,会先通过 macroGetPrice 取 IT_RESULT 的 EV_LINE 价格。当该价格返回 0,且同 ticket 的 IT_PENDING 的 EV_MOVE 也为 0,说明挂单尚未给出有效价位,此时必须现场补建指标轴。 补建逻辑很直接:CreateIndicator(ticket, IT_PENDING) 先造壳,再用 PositionAxlePrice(ticket, IT_PENDING, m_Selection.pr) 把轴钉到当前鼠标选中的价格 m_Selection.pr 上,随后 ChartRedraw() 强制重绘。这样即便挂单没触发,图表上也不会缺那根参考轴。 价格回退那行值得盯:pr = (pr > 0 ? pr : macroGetPrice(ticket, IT_PENDING, EV_MOVE))。意思是 RESULT 线无效就退而求其次用 PENDING 的 MOVE 价;外汇与贵金属波动快,这种回退能避免轴瞬间消失,但高频重绘会增加 CPU 占用,实盘前建议在 MT5 用 EURUSD 的 M1 跑一遍看帧率。

MQL5 / C++
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 = class="kw">false;

   if (ticket == def_IndicatorGhost) pr = m_Selection.pr; else
   {
      pr = macroGetPrice(ticket, IT_RESULT, EV_LINE);
      if ((pr == class="num">0) && (macroGetPrice(ticket, IT_PENDING, EV_MOVE) == class="num">0))
      {
         CreateIndicator(ticket, IT_PENDING);
         PositionAxlePrice(ticket, IT_PENDING, m_Selection.pr);
         ChartRedraw();
      }
      pr = (pr > class="num">0 ? pr : macroGetPrice(ticket, IT_PENDING, EV_MOVE));

「挂单与止损止盈的界面同步写法」

这段片段展示了在 MQL5 中更新挂单体积以及同步止盈止损到图表信息面板的一种紧凑写法。核心是用宏把重复的 SetTextValue 调用收敛掉,只在需要刷新时调用 ChartRedraw。 代码中 SetTextValue(ticket, IT_PENDING, vol) 负责把当前挂单成交量写到指定 ticket 对应的面板字段;被标绿的 macroUpdate(IT_TAKE, tp) 与 macroUpdate(IT_STOP, sl) 则分别把止盈、止损数值推到界面。若 b0 为真,才触发 ChartRedraw(),避免无意义的重绘开销。 在 MT5 里验证时,可把这段直接嵌进你的挂单管理函数,观察终端右下角自定义面板是否随 tp/sl 修改实时变化;外汇与贵金属杠杆高,参数误写可能导致异常挂单,建议先用策略测试器跑一遍。

MQL5 / C++
SetTextValue(ticket, IT_PENDING, vol);
            }
            macroUpdate(IT_TAKE, tp);
            macroUpdate(IT_STOP, sl);
            if (b0) ChartRedraw();
            }
class="macro">#undef macroUpdate

浮动指标不在服务器上的坑怎么填

浮动指标有个硬伤:它画在图表上,却不存在于交易服务器。EA 里那些基于真实订单、持仓的统计指标,拿不到浮动指标的数据源,两者运行机制根本不对齐。 要抹平这个差异,得进 DispatchMessage 函数改事件处理逻辑。核心思路是:当点击的对象属于浮动指标(ticket == def_IndicatorFloat)时,用同一套算法去重算 TP、SL 和刷新显示,让浮动数据和服务器数据走一样的调整通道。 改动后,浮动指标的设置问题基本消失——你能用调真实持仓的方式去调浮动指标的止盈止损。但还没完,另一个老毛病得挪到下一节单独拆。 开 MT5 把下面这段塞进你的 DispatchMessage,重点看 macroGetDataIndicatorFloat 之后的三行:TP/SL 按买卖方向用 MathAbs 重算偏移,ticket 置 0 再走 UpdateIndicators,外汇贵金属这类高杠杆品种下误触浮动线可能瞬间改掉模拟止损,先开策略测试器验证再上盘。

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=class="str">"cmt">// ... Internal code...
					
		class="kw">switch (id)
		{
class=class="str">"cmt">// ... Internal code...
				case CHARTEVENT_OBJECT_CLICK:
					if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev)
					{
							case EV_TYPE:
								if (ticket == def_IndicatorFloat)
								{
									macroGetDataIndicatorFloat;
									m_Selection.tp = (m_Selection.tp == class="num">0 ? class="num">0 : m_Selection.pr + (MathAbs(m_Selection.tp - m_Selection.pr) * (m_Selection.bIsBuy ? class="num">1 : -class="num">1)));
									m_Selection.sl = (m_Selection.sl == class="num">0 ? class="num">0 : m_Selection.pr + (MathAbs(m_Selection.sl - m_Selection.pr) * (m_Selection.bIsBuy ? -class="num">1 : class="num">1)));
									m_Selection.ticket = class="num">0;
									UpdateIndicators(def_IndicatorFloat, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy);
								} else m_BtnInfoType.SetStateButton(sparam, !m_BtnInfoType.GetStateButton(sparam));
								class="kw">break;
						case EV_DS:
							if (ticket != def_IndicatorFloat) m_BtnInfo_DS.SetStateButton(sparam, !m_BtnInfo_DS.GetStateButton(sparam));

◍ 平仓事件里的指标回收逻辑

在 MT5 自定义交易面板的事件分发里,EV_CLOSE 分支负责处理持仓或挂单被平掉后的善后。当 ticket 等于 def_IndicatorFloat 时,说明是浮动指标线对应的那笔订单结束,需要先抓取该笔的最后指标数据,再把它从画面上移除。 macroGetDataIndicatorFloat 负责把平仓瞬间的浮动值读出来;RemoveIndicator(def_IndicatorFloat, it) 紧接着删掉对应类型的图形对象,避免残留线条干扰后续判断。 如果订单类型不是 IT_PENDING,代码会调用 UpdateIndicators 重建剩余指标的止盈止损与手数——止盈或止损若被本次平仓消耗,对应参数传 0,否则沿用 m_Selection 里的 tp、sl 与 vol。外汇与贵金属杠杆高,这类自动回收若漏写,可能让旧线误导新单,建议直接开 MT5 把这段塞进自己的面板 EA 里断点跟一遍。 其余分支用 GetInfosTradeServer 拿服务器返回码,非 0 才进 switch(it) 细分处理,原文后续省略。

MQL5 / C++
            class="kw">break;
            case EV_CLOSE:
               if (ticket == def_IndicatorFloat)
               {
                  macroGetDataIndicatorFloat;
                  RemoveIndicator(def_IndicatorFloat, it);
                  if (it != IT_PENDING) UpdateIndicators(def_IndicatorFloat, (it == IT_TAKE ? class="num">0 : m_Selection.tp), (it == IT_STOP ? class="num">0 : m_Selection.sl), m_Selection.vol, m_Selection.bIsBuy);
               }else if ((cRet = GetInfosTradeServer(ticket)) != class="num">0) class="kw">switch (it)
class=class="str">"cmt">// ... The rest of the code...

「负止盈与挂单破位价的边界处理」

MT5 的 EA 里止盈字段长期允许填负值,但服务器收到复数价格会直接报错拒单。旧逻辑更隐蔽:EA 本地显示已发送,实际后台返回错误却被忽略,挂单场景尤其麻烦——破位价若误改为正数,订单系统会误以为成功,持仓和挂单的行为差异又没修干净。 解决思路是在结构体里加一个 MousePrice 变量,专门存图表上鼠标拖出的局部价格,避开调用 Windows API 操纵鼠标(不愿开 dll 权限)。这个值在移动函数、指标刷新、挂单破位计算三处复用,比宏替换更可控。 关键约束:止盈等于或小于 0 时强制锁死不允许更小;挂单的止损绝不允许大于 0,满足条件时 EA 取最小允许值。持仓则不同,正止损表示触发后计入账户,EA 应尊重交易者输入。这样无论订单还是持仓,取值都不会为负,服务器拒单概率显著下降。 外汇与贵金属杠杆高、滑点大,这类边界 bug 可能让挂单在剧烈波动中无声失效,实盘前务必在策略测试器用挂单+负止盈拖拽复现一遍。

MQL5 / C++
class="kw">struct st00
{
    eIndicatorTrade it;
    class="type">bool            bIsBuy,
                    bIsDayTrade;
    class="type">class="kw">ulong           ticket;
    class="type">class="kw">double          vol,
                    pr,
                    tp,
                    sl,
                    MousePrice;
}m_Selection;
class="type">void MoveSelection(class="type">class="kw">double price)
{
    if (m_Selection.ticket == class="num">0) class="kw">return;
    class="kw">switch (m_Selection.it)
    {
        case IT_TAKE:
            UpdateIndicators(m_Selection.ticket, price, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy);
            class="kw">break;
        case IT_STOP:
            UpdateIndicators(m_Selection.ticket, m_Selection.tp, price, m_Selection.vol, m_Selection.bIsBuy);
            class="kw">break;
        case IT_PENDING:
            PositionAxlePrice(m_Selection.ticket, IT_PENDING, price);
            UpdateIndicators(m_Selection.ticket, (m_Selection.tp == class="num">0 ? class="num">0 : price + m_Selection.tp - m_Selection.pr), (m_Selection.sl == class="num">0 ? class="num">0 : price + m_Selection.sl - m_Selection.pr), m_Selection.vol, m_Selection.bIsBuy);
            m_Selection.MousePrice = price;
            class="kw">break;
    }
    if (Mouse.IsVisible())
    {
        m_TradeLine.SpotLight(macroMountName(m_Selection.ticket, m_Selection.it, EV_LINE));
        Mouse.Hide();
把重复劳动交给小布
这些诊断和小布盯盘里的 AIGC 已内置,打开对应品种页即可看到 EA 图表上 TP/SL 破位恢复的异常提示,你专注决策就好。

常见问题

平台原生流程需要遵循特定场景步骤,手动跟随时容易漏掉顺序或误改参数,且响应速度受终端负载影响,可能比专注该任务的 EA 更慢。
不必。本文演示仅用订单系统 EA 自身逻辑即可生成和修正 SL/TP 破位,无需调用任何外部资源或额外指标文件。
负止盈会扭曲盈亏计算与图表显示逻辑,增加浮动指标的不便,并可能在回测和实盘切换时引发校验错误,概率上更建议用正向结构替代。
可以。小布盯盘的 AIGC 模块已内置常见 EA 图表操作诊断,能提示破位级别缺失或恢复失败,省去人工逐图核对。
因指标锚定点未随破位重建同步刷新,需调整更新函数,具体方案见本篇 2.0.3 节对浮动指标不便的解决思路。