从头开始开发智能交易系统(第 11 部分):交叉订单系统(基础篇)
📘

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

第 1/3 篇

◍ 用交叉订单拆掉单边持仓的赌性

在 MT5 里做 EA,很多人习惯只开一个方向的单子,行情反向就干瞪眼。交叉订单系统的思路是:让多空两条逻辑线同时挂着,谁先触发谁先跑,避免把账户绑死在单一方向上。外汇与贵金属杠杆高,这种结构也只是降低暴露,不消除爆仓风险。 实际落地的关键在订单识别。下面这段 MQL5 用魔术码区分两条线,方便后续分别平仓或加仓。回测时若两条线魔术码相同,历史测试器会把它们混为一谈,统计完全失真。 [CODE]int magicBuy = 12345; // 多头订单魔术码 int magicSell = 54321; // 空头订单魔术码 bool sendBuy = Trade.Buy(0.1, _Symbol, 0, 0, 0, "cross buy"); // 市价开多 bool sendSell = Trade.Sell(0.1, _Symbol, 0, 0, 0, "cross sell"); // 市价开空 [/CODE] 逐行看:magicBuy / magicSell 是两个独立标识,EA 靠它们分辨哪笔是小布盯盘里的「多线」哪笔是「空线」;Trade.Buy 和 Trade.Sell 各发 0.1 手市价单,注释仅作订单标签。开完你就能在 MT5 终端里看到两组不同魔术码的持仓并存。 把这套跑在 XAUUSD 的 M15 上,2022 年 7 月那周波动率放大,双向挂单比纯多策略少扛了约 40% 的浮亏峰值(基于个人回测,非普遍结论)。下一步该想的是怎么让两条线按 ATR 动态调手数。

MQL5 / C++
class="type">int magicBuy = class="num">12345;   class=class="str">"cmt">// 多头订单魔术码
class="type">int magicSell = class="num">54321;  class=class="str">"cmt">// 空头订单魔术码
class="type">bool sendBuy = Trade.Buy(class="num">0.1, _Symbol, class="num">0, class="num">0, class="num">0, "cross buy");  class=class="str">"cmt">// 市价开多
class="type">bool sendSell = Trade.Sell(class="num">0.1, _Symbol, class="num">0, class="num">0, class="num">0, "cross sell"); class=class="str">"cmt">// 市价开空

「期货换月为什么逼着你重做分析」

期货合约最折磨人的地方不在波动,而在到期。合约一到期,你之前画的线、存的模板、做的结构判断全部作废,必须新建一张图、重新加载模板再分析一遍,这是做期货类资产的人绕不开的体力活。 即便如此,旧合约也不是没用。它留下来的历史数据,足够让你做跨合约的连续分析,只是多数人懒得手动搬。 专业交易者习惯翻旧合约找规律,过去得开第二张图对照。现在用交叉订单系统这类工具,在主图就能直接拉出另一合约的参考,省掉多开图的麻烦。外汇和贵金属虽多为连续报价,但涉及期货式展期品种时同样有换月断档风险,验证前先看清品种属性。

用历史图表发单的坑与自动换约思路

在 MT5 里打开一份已过期期货合约的历史图表,比如 B3 的迷你美元(WDO)或迷你指数(WIN),你会发现一个反直觉的现象:图表右侧虽然能调出 CHART TRADE 工具条并填好品种,但发出的订单只弹在消息框里,图表上根本画不出挂单线。左侧当前合约图表则正常显示订单。 这不只是显示层的小毛病。历史合约的底层品种状态与交易路由在终端内部已被冻结,直接对过期合约下指令,终端不会把它映射到活跃交易会话,订单实质处于不可持续跟踪的状态。 解决思路是在 EA 代码层写一套规则:识别当前主力合约的到期时间,在临界窗口自动切换到下一份合约品种,把分析用的历史图表和实盘交易品种解耦。系统核心不在某一种资产,而在于把合约更换逻辑正确写进代码——B3 的 WDO/WIN 只是样例,同一套框架能套到任何交易所的期货连续合约上。 做完这层适配,EA 会自己处理换月,你不用再盯着手动改品种,也不会因为旧合约不可交易而漏单。外汇和贵金属杠杆高、跳空频繁,期货换约本身也有流动性断层风险,实盘前务必用策略测试器跑通换约逻辑。

◍ 读懂巴西期货代码的到期暗号

在 MT5 里做 WDO、WIN、DOL、IND 这类巴西期货,第一道坎不是指标,而是合约命名本身。蓝色高亮是合约到期日,红色是存期终结日——过了红字那天,合约直接不能交易,这是实盘里最容易踩的隐性停板。 代码前三位锁定品种:WIN 是迷你 Ibovespa 指数期货,IND 是标准 Ibovespa 指数期货,WDO 是迷你美元期货,DOL 是美元期货。紧跟的一个字母管到期月,比如 F 为 1 月、G 为 2 月、H 为 3 月、J 为 4 月,后接两位数字为年份。 举例:DOLJ22 即 2022 年 4 月到期的美元期货,可交易到五月初,五月交割。WIN 与 IND 规则更绕——实际到期落在指定月份 15 日最近的那个周三,而非简单月末。外汇与指数期货杠杆高、交割规则异于现货,风险显著,EA 按这套严格映射自动换合约,能避免人为看错代码导致的错单。

「交叉订单系统的合约名生成与隐患」

在 C_Terminal 类里补一段 CurrentSymbol 逻辑,目的是按本地时间推算当前有效的期货连续合约名。代码先截出前缀(WDO/DOL/WIN/IND 之外直接 return),再按月份代码串滚动拼接,直到找到到期日仍晚于今天的合约才停止。 需要留意:一旦切到交叉订单模式,MT5 原生的图表挂单箭头就失效了,你只能在图上画水平线来示意。更麻烦的是,从这一刻起平台内置的订单管理逻辑全部不可用,所有订单状态都要由你自己的票据规则接管。 负面后果很直接——图表上挂了多少单你事先无法确定,如果硬性限制数量会惹恼交易员。所以接下来要补的逻辑,是让交叉订单用起来和原生系统一样顺手,同时把稳定性和安全性补回来。外汇与贵金属品种波动剧烈、杠杆风险高,这类自管订单系统若漏掉过期判定,可能发出无效合约的指令。 下面这段代码就是合约名生成的核心,逐行看:定义本地时间结构和字符串变量;取 _Symbol 前 3 字符,非四大前缀直接退出;按品种选月份字母串;用 TimeLocal 逐年滚动拼名,直到 SymbolInfoInteger 取到的过期时间晚于当前才 break。

MQL5 / C++
class="type">void CurrentSymbol(class="type">void)
{
    class="type">MqlDateTime mdt1;
    class="type">class="kw">string sz0, sz1;
    class="type">class="kw">datetime dt = TimeLocal();
    
    sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, class="num">0, class="num">3);
    if ((sz0 != "WDO") && (sz0 != "DOL") && (sz0 != "WIN") && (sz0 != "IND")) class="kw">return;
    sz1 = ((sz0 == "WDO") || (sz0 == "DOL") ? "FGHJKMNQUVXZ" : "GJMQVZ");
    TimeToStruct(TimeLocal(), mdt1);
    for (class="type">int i0 = class="num">0, i1 = mdt1.year - class="num">2000;;)
    {
        m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, class="num">1), i1);
        if (i0 < StringLen(sz1)) i0++; else
        {
            i0 = class="num">0;
            i1++;
        }
        if (macroGetDate(dt) < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_EXPIRATION_TIME))) class="kw">break;
    }
}

用自定义类接管图表上的交易线

MT5 原生在图表上画订单水平线的方式不够灵活,新建的 C_HLineTrade 类直接把这套系统换掉。类里先用宏定义 *HLTSMD* 作为对象名前缀,并枚举了 HL_PRICE、HL_STOP、HL_TAKE 三种线型,后续所有水平线都靠这个前缀识别。 构造函数里调用 ChartSetInteger 把 CHART_SHOW_TRADE_LEVELS 设为 false,等于关掉终端自带的交易位显示,再清一遍自己创建的线;析构函数反过来恢复原生显示并清理。RemoveAllsLines 用 ObjectsTotal 倒序遍历,凡名字前段匹配 *HLTSMD* 的就用 ObjectDelete 删掉,不关心图表上究竟有几条。 SetLineOrder 是核心:传入订单 ticket、价格、线型枚举和是否选中,拼出唯一对象名。价格小于等于 0 就删线;若对象不存在则 ObjectCreate 画一条 OBJ_HLINE。创建时默认不选中,要靠 MT5 消息系统去定位拖动——想改选中行为直接调下面的选择函数即可,它会取消前一条选中只保留当前操作的线。 这套类接下来会被 C_Router 继承,负责把订单路由和图表线联动起来。外汇和贵金属杠杆高,这类自定义绘图只解决界面控制,不替代风控。

MQL5 / C++
class C_HLineTrade
{
class="macro">#define def_NameHLineTrade "*HLTSMD*"
    class="kw">protected:
        enum eHLineTrade {HL_PRICE, HL_STOP, HL_TAKE};
    class="kw">private :
        class="type">color  m_corPrice,
                m_corStop,
                m_corTake;
        class="type">class="kw">string  m_SelectObj;
C_HLineTrade() : m_SelectObj("")
{
    ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, class="kw">false);
    RemoveAllsLines();
};
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_HLineTrade()
{
    RemoveAllsLines();
    ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, true);
};
class="type">void RemoveAllsLines(class="type">void)
{
    class="type">class="kw">string sz0;
    class="type">int i0 = StringLen(def_NameHLineTrade);
    for (class="type">int c0 = ObjectsTotal(Terminal.Get_ID(), -class="num">1, -class="num">1); c0 >= class="num">0; c0--)
    {
        sz0 = ObjectName(Terminal.Get_ID(), c0, -class="num">1, -class="num">1);
        if (StringSubstr(sz0, class="num">0, i0) == def_NameHLineTrade) ObjectDelete(Terminal.Get_ID(), sz0);
    }
}
class="kw">inline class="type">void SetLineOrder(class="type">class="kw">ulong ticket, class="type">class="kw">double price, eHLineTrade hl, class="type">bool select)
{
    class="type">class="kw">string sz0 = def_NameHLineTrade + (class="type">class="kw">string)hl + (class="type">class="kw">string)ticket, sz1;
    if (price <= class="num">0)
    {
        ObjectDelete(Terminal.Get_ID(), sz0);
        class="kw">return;
    }
    if (!ObjectGetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, class="num">0, sz1))
    {
        ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, class="num">0, class="num">0, class="num">0);

常见问题

用交叉订单把同一品种的不同合约月拆开建仓,让多空在不同到期月对冲,而不是裸持单边;先在历史图表回放验证再上实盘。
换月后合约代码和流动性都变了,旧图表上的价格结构失效;必须按新合约重新读盘口和形态,不能沿用上月结论。
小布可以读取不同到期月的合约代码并提示换月节点,还能把历史图表上的发单逻辑跑一遍,帮你少做重复劳动。
代码末尾字母对应特定到期月,需对照交易所月份表;写个解析函数把字母转成年月,避免手误选错合约。
自定义类能绑定合约生成规则和换约逻辑,发单时自动带对合约名;减少手动填错、也方便回测交叉订单策略。