从头开始开发智能交易系统(第 11 部分):交叉订单系统·进阶篇
🔁

从头开始开发智能交易系统(第 11 部分):交叉订单系统·进阶篇

(2/3)· 右侧历史图表能看旧支撑阻力却发不出可见订单?这套路由机制专治合约换月盲点

案例拆解 第 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。外汇和贵金属波动大,这类辅助线仅作可视化参考,实际成交受滑点影响可能偏离画线价。

MQL5 / C++
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 把路由类挂上去,编译看继承链是否报错。外汇与贵金属品种波动大、滑点不可控,这类封装仅降低代码耦合,不预示任何胜率。

MQL5 / C++
            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 与窗口标题资产名是否一致是快速体检法。

MQL5 / C++
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%。

MQL5 / C++
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 策略测试器用模拟单跑一遍。

MQL5 / C++
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 用脚本复现删除动作前,建议先开模拟盘验一遍对象删除分支。

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, 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;
把换月路由交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到当前主力与过期合约的切换状态,你只管确认信号,换月路由的脏活让它跑。

常见问题

订单实际发往了过期交易品种,终端只在消息框提示,图表不渲染。本质是路由没把历史图表的下单请求重定向到当前合约。
难点只在代码层的合约规格与到期日解析,逻辑通了就能套用任意期货。本文以 B3 为例,理解后改几个字段即可。
它负责判定当前图是历史还是实盘合约,并把订单请求导向正确的交易品种,避免手动选错。
可以,品种页会标出临近到期的合约和接替的主力,省去自己翻交易所日历。仍要结合你的策略判断,期货波动高风险。
模板独立于合约数据,路由只换下单目标,图形分析照常保留,这也是交叉订单系统的便利处。