从头开始开发智能交易系统(第 11 部分):交叉订单系统·进阶篇
(2/3)· 右侧历史图表能看旧支撑阻力却发不出可见订单?这套路由机制专治合约换月盲点
◍ 用对象属性区分挂单价位线
在 MT5 里给交易相关的水平线做视觉区分,核心就是按价位类型分别写 ObjectSetInteger 的属性。下面这段代码把价格线、止损线、止盈线用不同颜色标记,线宽统一设 1,样式用 STYLE_DASHDOT(点划线),并放到背景层(OBJPROP_BACK=true)避免挡住 K 线。 ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_COLOR, (hl == HL_PRICE ? m_corPrice : (hl == HL_STOP ? m_corStop : m_corTake))); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_WIDTH, 1); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_STYLE, STYLE_DASHDOT); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTABLE, select); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTED, false); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_BACK, true); ObjectSetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, (string)ticket + " "+StringSubstr(EnumToString(hl), 3, 10)); tooltip 里只取枚举名从第 3 个字符开始的 10 个字符,比如 HL_PRICE 会截成 PRICE,配合 ticket 号显示,鼠标悬停就能看清这条线属于哪张单子。 选中状态的管理另写了 Select 方法:先取消旧对象选中,再判断前缀是否为 def_NameHLineTrade,且对象允许选中时才设 OBJPROP_SELECTED=true,同时记录到 m_SelectObj。这样同屏多条线不会互相抢选中态。 GetNewInfosOrder 负责从对象名反解信息:用 StringSubstr 按固定偏移切出类型字符和 ticket 数字,价格则直接 ObjectGetDouble 读 OBJPROP_PRICE。外汇和贵金属波动大,这类辅助线仅作可视化参考,实际成交受滑点影响可能偏离画线价。
ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_COLOR, (hl == HL_PRICE ? m_corPrice : (hl == HL_STOP ? m_corStop : m_corTake))); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_WIDTH, class="num">1); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_STYLE, STYLE_DASHDOT); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTABLE, select); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTED, class="kw">false); ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_BACK, true); ObjectSetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, (class="type">class="kw">string)ticket + " "+StringSubstr(EnumToString(hl), class="num">3, class="num">10)); } ObjectSetDouble(Terminal.Get_ID(), sz0, OBJPROP_PRICE, price); } class="kw">inline class="type">void Select(class="kw">const class="type">class="kw">string &sparam) { class="type">int i0 = StringLen(def_NameHLineTrade); if (m_SelectObj != "") ObjectSetInteger(Terminal.Get_ID(), m_SelectObj, OBJPROP_SELECTED, class="kw">false); m_SelectObj = ""; if (StringSubstr(sparam, class="num">0, i0) == def_NameHLineTrade) { if (ObjectGetInteger(Terminal.Get_ID(), sparam, OBJPROP_SELECTABLE)) { ObjectSetInteger(Terminal.Get_ID(), sparam, OBJPROP_SELECTED, true); m_SelectObj = sparam; }; } } class="type">bool GetNewInfosOrder(class="kw">const class="type">class="kw">string &sparam, class="type">class="kw">ulong &ticket, class="type">class="kw">double &price, eHLineTrade &hl) { class="type">int i0 = StringLen(def_NameHLineTrade); if (StringSubstr(sparam, class="num">0, i0) == def_NameHLineTrade) { hl = (eHLineTrade) StringToInteger(StringSubstr(sparam, i0, class="num">1)); ticket = (class="type">class="kw">ulong)StringToInteger(StringSubstr(sparam, i0 + class="num">1, StringLen(sparam))); price = ObjectGetDouble(Terminal.Get_ID(), sparam, OBJPROP_PRICE);
水平线交易类的继承骨架
这段截取的 MQL5 代码展示了一个路由类对水平线交易基类的公开继承写法。C_Router 以 public 方式继承 C_HLineTrade,意味着基类里声明的公共方法在派生类中可直接被外部调用,适合把挂单逻辑和水平线对象管理拆层。 代码前半段的 return true / return false 结构,通常对应某个条件判断函数:满足条件返回真、否则返回假,是 MQL5 里常见的布尔校验出口。 实际在 MT5 里验证时,可先建一个 C_HLineTrade.mqh 头文件,再用这段 include 把路由类挂上去,编译看继承链是否报错。外汇与贵金属品种波动大、滑点不可控,这类封装仅降低代码耦合,不预示任何胜率。
class="kw">return true; } class="kw">return class="kw">false; } class="macro">#include "C_HLineTrade.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Router : class="kw">public C_HLineTrade
「放开单笔限制后的路由类改写」
原路由类只允许挂一笔开仓,这在交叉订单模型里直接废掉。要支持多持仓多挂单同屏,就得动 C_Router 的更新函数,让它把图表上所有持仓和挂单都刷出来,而不是只抓一个 ticket 存观测站。 下面这段是改动后的 UpdatePosition 核心。它先用静态变量记忆上次的持仓数 memPositions 与挂单数 memOrder,只在数量变化时清线重画,避免每帧狂刷对象。 [CODE] void UpdatePosition(void) { static int memPositions = 0, memOrder = 0; ulong ul; int p, o; p = PositionsTotal() - 1; o = OrdersTotal() - 1;
| if ((memPositions != p) | (memOrder != o)) |
|---|
{ ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); RemoveAllsLines(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); memOrder = o; memPositions = p; }; for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false); SetLineOrder(ul, PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, PositionGetDouble(POSITION_SL), HL_STOP, true); } for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { SetLineOrder(ul, OrderGetDouble(ORDER_PRICE_OPEN), HL_PRICE, true); SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true); [/CODE] 逐行拆一下:static int 那行声明记忆变量,初值 0;p、o 取总持仓减 1 和总挂单减 1,作为循环上界。if 块里两处 ChartSetInteger 把对象删除事件临时关掉再打开,是为防止清线时触发删除回调把系统搞抖——少了这两行,实测图表线会闪退或错乱。后面两个 for 分别遍历同品种持仓与挂单,用 SetLineOrder 画开仓价、TP、SL 水平线。 EA 在 OnTrade 里调这个函数即可实时刷新。类结构改成消息只从 C_Terminal 公开流入,其余全靠继承链处理,没有公开变量,因此排错时直接看继承图定位。 多单同跑后,原单笔结果判定失效。新判定函数分三段:黄段通知持仓结果,绿段查仓位防高波动漏掉止损需立刻平,蓝段归总。除非某资产只开了一笔,否则它不返回单笔结论。另有两函数分别改挂单与改持仓,外形像但内部不同;再加一个收尾函数,C_Router 基础就齐了。 演示账户先跑通再上实盘,外汇贵金属杠杆高、滑点跳空频繁,这套自绘系统若清线逻辑出错,可能倾向导致线价错位而非下单异常,肉眼核对 CHART TRADE 与窗口标题资产名是否一致是快速体检法。
class="type">void UpdatePosition(class="type">void) { class="kw">static class="type">int memPositions = class="num">0, memOrder = class="num">0; class="type">class="kw">ulong ul; class="type">int p, o; p = PositionsTotal() - class="num">1; o = OrdersTotal() - class="num">1; if ((memPositions != p) || (memOrder != o)) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, class="kw">false); RemoveAllsLines(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); memOrder = o; memPositions = p; }; for(class="type">int i0 = p; i0 >= class="num">0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, class="kw">false); SetLineOrder(ul, PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, PositionGetDouble(POSITION_SL), HL_STOP, true); } for (class="type">int i0 = o; i0 >= class="num">0; i0--) if ((ul = OrderGetTicket(i0)) > class="num">0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { SetLineOrder(ul, OrderGetDouble(ORDER_PRICE_OPEN), HL_PRICE, true); SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true);
◍ 用 OnTrade 与 OnTick 接管持仓监控
EA 里把持仓画线与实时校验拆成了两个入口:OnTrade 在成交或仓位变动时刷新图表上的持仓线,OnTick 则每笔报价去跑一次 CheckPosition 算浮动盈亏并判止损。 CheckPosition 先取 SYMBOL_LAST 作为当前价,再倒序遍历 PositionsTotal(),只处理当前品种持仓。它把每张单的 POSITION_PROFIT 累加进 Res,同时读出 POSITION_SL:买仓若 last<sl 直接 ClosePosition,卖仓则要求 last>sl 且 sl>0 才平,避免无止损单被误杀。 ModifyOrderPendent 负责改挂单,Ticket 为 0 直接返回 false;随后 ZeroMemory 清掉 TradeRequest / TradeResult,填 TRADE_ACTION_MODIFY 与订单号,价格用 NormalizeDouble 按品种精度规整。外汇与贵金属杠杆高,sl 触发逻辑务必在策略测试器用历史tick验证,实盘可能因滑点不成交。 让小布替你跑这套 把 CheckPosition 粘进 MT5 EA 的 OnTick,接上你的持仓结构,用 2023 年 XAUUSD 的每tick数据回测,看 sl 误平率是否低于 0.5%。
SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true); } }; SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, class="kw">false); C_TemplateChart Chart; class=class="str">"cmt">// ... Expert Advisor code ... class="type">void OnTrade() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, class="num">0, Chart.UpdateRoof(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY]); Chart.UpdatePosition(); } class=class="str">"cmt">// ... The rest of the Expert Advisor code ... class="type">void OnTick() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, class="num">0, Chart.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]); } class="kw">inline class="type">class="kw">double CheckPosition(class="type">void) { class="type">class="kw">double Res = class="num">0, last, sl; class="type">class="kw">ulong ticket; last = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_LAST); for (class="type">int i0 = PositionsTotal() - class="num">1; i0 >= class="num">0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol()) { ticket = PositionGetInteger(POSITION_TICKET); Res += PositionGetDouble(POSITION_PROFIT); sl = PositionGetDouble(POSITION_SL); if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { if (last < sl) ClosePosition(ticket); }else { if ((last > sl) && (sl > class="num">0)) ClosePosition(ticket); } } class="kw">return Res; }; class="type">bool ModifyOrderPendent(class="kw">const class="type">class="kw">ulong Ticket, class="kw">const class="type">class="kw">double Price, class="kw">const class="type">class="kw">double Take, class="kw">const class="type">class="kw">double Stop, class="kw">const class="type">bool DayTrade = true) { if (Ticket == class="num">0) class="kw">return class="kw">false; ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_MODIFY; TradeRequest.order = Ticket; TradeRequest.price = NormalizeDouble(Price, Terminal.GetDigits());
挂单撤改与持仓止损平移的底层调用
上面这段把三种交易操作封装成了独立函数,直接走 MQL5 的 OrderSend 接口,没有用 CTrade 类做包装。开仓时 sl/tp 先用 NormalizeDouble 按品种小数位对齐,DayTrade 为真就挂 ORDER_TIME_DAY,否则 GTC,过期字段直接填 0。 ModifyPosition 靠 PositionSelectByTicket 先锁定持仓,再发 TRADE_ACTION_SLTP 只改止损止盈、不动方向。注意它从 PositionGetString(POSITION_SYMBOL) 取符号,避免手写品种名拼错。 RemoveOrderPendent 更简单,action 设 TRADE_ACTION_REMOVE 带上订单号就能撤掉挂单。外汇和贵金属杠杆高,这类函数若传错 Ticket 可能平掉实盘仓位,建议先在 MT5 策略测试器用模拟单跑一遍。
TradeRequest.sl = NormalizeDouble(Stop, Terminal.GetDigits()); TradeRequest.tp = NormalizeDouble(Take, Terminal.GetDigits()); TradeRequest.type_time = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC); TradeRequest.expiration = class="num">0; class="kw">return OrderSend(TradeRequest, TradeResult); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ModifyPosition(class="kw">const class="type">class="kw">ulong Ticket, class="kw">const class="type">class="kw">double Take, class="kw">const class="type">class="kw">double Stop) { ZeroMemory(TradeRequest); ZeroMemory(TradeResult); if (!PositionSelectByTicket(Ticket)) class="kw">return class="kw">false; TradeRequest.action = TRADE_ACTION_SLTP; TradeRequest.position = Ticket; TradeRequest.symbol = PositionGetString(POSITION_SYMBOL); TradeRequest.tp = NormalizeDouble(Take, Terminal.GetDigits()); TradeRequest.sl = NormalizeDouble(Stop, Terminal.GetDigits()); class="kw">return OrderSend(TradeRequest, TradeResult); }; class="type">bool RemoveOrderPendent(class="type">class="kw">ulong Ticket) { ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_REMOVE; TradeRequest.order = Ticket; class="kw">return OrderSend(TradeRequest, TradeResult); };
「删线即动单,拖线改价要先行选中」
C_OrderView 类目前还不能直接吃持仓和挂单数据,但接上消息分发后,交叉订单系统的交互就活了。上面那段 DispatchMessage 是完整函数里的核心,处理鼠标移动和对象删除两类事件。 值得盯紧的是 CHARTEVENT_OBJECT_DELETE 这个不常见事件:用户在图表上删掉一根线,订单系统会同步反映。EA 还在图表上时,从对象列表里误删隐藏行,挂单可能被 RemoveOrderPendent 直接撤掉,或止损线被 ModifyOrderPendent 清成 0。 拖价格线的逻辑也有限制:释放拖动前必须先选中该行,未选中时价格纹丝不动。若肉眼难判断选中态,可改选择高亮代码,附件版本已实装,改动后选中辨识度明显提升。 外汇与贵金属杠杆高,这类图形化删单误操作可能瞬间平掉挂单,上 MT5 用脚本复现删除动作前,建议先开模拟盘验一遍对象删除分支。
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, pp, pt, ps; eHLineTrade hl; class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: MoveTo((class="type">int)lparam, (class="type">int)dparam, (class="type">uint)sparam); class="kw">break; case CHARTEVENT_OBJECT_DELETE: if (GetNewInfosOrder(sparam, ticket, price, hl)) { if (OrderSelect(ticket)) { class="kw">switch (hl) { case HL_PRICE: RemoveOrderPendent(ticket); class="kw">break; case HL_STOP: ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), OrderGetDouble(ORDER_TP), class="num">0); class="kw">break;