从头开始开发智能交易系统(第 28 部分):面向未来((III)·进阶篇
(2/3)· 当平台自带票据系统又慢又易错,如何用 EA 自身在图表上重建止盈止损价位
把移动按钮从指标里剥出来
EA 的 Update 函数只管止盈止损指标的宏替换,但移动按钮必须独立创建,否则图表对象会和订单状态脱节。作者把按钮生成单独抽成 inline 函数,编译器会像宏一样内联展开,避免系统外观不一致。 为了统一配色,代码里用两个宏固定了破位线的颜色:take 用 clrDarkGreen、stop 用 clrMaroon。这样无论只建一个移动按钮还是完整指标,颜色都跟全局一致,不会某张单子按钮变色。 Update 里的宏替换分三步跑:第一,若破位线价格=0(订单删了止盈或止损),先删指标线,若移动对象不存在就建一个,图表上只剩可拖的按钮;第二,若服务器订单有破位级别但该线不在图表,删移动对象、建完整指标放到对应价位;第三,把指标钉到正确点位——如果只剩移动对象,就直接贴到所属订单/持仓的价位,一眼能看出缺了哪边。 外汇和贵金属杠杆高,这类半自动挂单辅助逻辑只降低操作失误概率,不保证成交价优于预期,实盘前务必在 MT5 策略测试器用历史数据跑一遍。
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 跑一遍看帧率。
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 修改实时变化;外汇与贵金属杠杆高,参数误写可能导致异常挂单,建议先用策略测试器跑一遍。
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,外汇贵金属这类高杠杆品种下误触浮动线可能瞬间改掉模拟止损,先开策略测试器验证再上盘。
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) 细分处理,原文后续省略。
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 可能让挂单在剧烈波动中无声失效,实盘前务必在策略测试器用挂单+负止盈拖拽复现一遍。
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();