基于交易模块创建多个 EA 交易·进阶篇
(2/3)·净持仓时代幻数失效,单品种多EA与多币种并行测试如何不各自为战
MT5的净持仓机制让传统按幻数识别未结订单的做法不再可靠,同一品种上跑多个EA实例时,你很难判断哪笔仓位归谁。多数人要么写虚拟订单层,要么硬塞幻数进总持仓,方案能跑却无法同时覆盖多币种场景。本文给出一套可复用的交易模型基类,把这两类麻烦一次性收口。
「用链表把交易模型串起来」
MQL5 里做多策略并行,最省事的办法是把每个模型写成继承 CObject 的子类,再丢进 CList 链表统一管理。基类 CModel 只留一个虚函数 Processing(),子类 cmodel_macd、cmodel_moving 各自重写,运行时就能挨个调。 下面这段是最小可跑骨架:CObject 构造函数把前后指针置 NULL,CModel 派生后设了 m_name,两个具体模型在 Processing() 里只 Print 自身名字。OnInit 里 new 出链表和两个模型指针,用 CheckPointer 确认是 POINTER_DYNAMIC 后才 Add 进链表,返回不是 -1 就代表挂链成功。 实际验证时把这段贴进 MT5 新建的 mq5,编译跑起来,终端会打印「Model MACD successfully created」以及两次 Processing 的模型名。外汇与贵金属杠杆高,模型只负责结构搭建,信号真假还需结合盘面概率判断。 别把 Print 当信号源 链表里的 Processing 此刻只是占位输出,直接拿来开仓属于裸奔。先塞真实指标逻辑,再谈胜率。
class="type">void CObject::CObject() { class=class="str">"cmt">//--- initialize class="kw">protected data m_prev=NULL; m_next=NULL; } class CModel : class="kw">public CObject class CModel:CObject { class="kw">protected: class="type">class="kw">string m_name; class="kw">public: class="type">void CModel(){m_name="Model base";} class="type">bool class="kw">virtual Processing(class="type">void){class="kw">return(true);} }; class cmodel_macd : class="kw">public CModel { class="kw">public: class="type">void cmodel_macd(){m_name="MACD Model";} class="type">bool Processing(){Print("Processing ", m_name, "...");class="kw">return(true);} }; class cmodel_moving : class="kw">public CModel { class="kw">public: class="type">void cmodel_moving(){m_name="Moving Average";} class="type">bool Processing(){Print("Processing ", m_name, "...");class="kw">return(true);} }; CList *list_models; class="type">void OnInit() { class="type">int rezult; cmodel_macd *m_macd; cmodel_moving *m_moving; list_models = new CList(); m_macd = new cmodel_macd(); m_moving = new cmodel_moving(); if(CheckPointer(m_macd)==POINTER_DYNAMIC){ rezult=list_models.Add(m_macd); if(rezult!=-class="num">1)Print("Model MACD successfully created");
用链表把指标模型跑起来
在 OnInit 里用 CheckPointer 确认 m_macd 与 m_moving 都是 POINTER_DYNAMIC 后,才通过 list_models.Add() 把两个模型挂进链表;返回 -1 就在日志打失败,否则打成功。这样 EA 启动时两类模型(MACD、MA)就进了同一个调度容器。 OnTick 中的循环是真正驱动逻辑:for(int i=0;i<list_models.Total();i++) 遍历链表,GetNodeAtIndex(i) 取出节点后调 Processing()。也就是说每根 tick 两个模型都会被顺序处理一次,无需手写重复调用。 日志给出了实跑证据:2010.10.10 14:18:21 在 EURUSD D1 上 Model MOVING AVERAGE / MACD was created successfully,随后 14:18:31 出现 Prosessing Moving Average... 与 Processing MACD Model...,证明链表添加与逐节点执行均生效。外汇与贵金属品种波动剧烈,此类多模型并行处理只降低代码耦合,不预示任何方向,实盘请自测滑点与重算开销。 OnDeinit 里一句 delete list_models 释放整条链表,避免指标句柄在反初始化时泄漏。开 MT5 把这段贴进 EA,改 Total() 前的模型数量就能验证调度是否如预期。
else Print("Creation of Model MACD has failed"); } class=class="str">"cmt">//Check valid pointer if(CheckPointer(m_moving)==POINTER_DYNAMIC){ rezult=list_models.Add(m_moving); if(rezult!=-class="num">1)Print("Model MOVING AVERAGE successfully created"); else Print("Creation of Model MOVING AVERAGE has failed"); } } class="type">void OnTick() { CModel *current_model; for(class="type">int i=class="num">0;i<list_models.Total();i++){ current_model=list_models.GetNodeAtIndex(i); current_model.Processing(); } } class="type">void OnDeinit(const class="type">int reason) { class="kw">delete list_models; } class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">31 ch01_simple_model(EURUSD,D1) Prosessing Moving Average... class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">31 ch01_simple_model(EURUSD,D1) Processing MACD Model... class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">21 ch01_simple_model(EURUSD,D1) Model MOVING AVERAGE was created successfully class="num">2010.10.class="num">10 class="num">14:class="num">18:class="num">21 ch01_simple_model(EURUSD,D1) Model MACD was created successfully rezult=list_models.Add(m_macd); rezult=list_models.Add(m_moving); CModel *current_model; for(class="type">int i=class="num">0;i<list_models.Total();i++){ current_model=list_models.GetNodeAtIndex(i); current_model.Processing(); }
◍ 用虚拟订单表兜住 MT5 的仓位模糊性
MT4 里每张订单独立挂在 Trade 选项卡,平仓针对具体订单号;MT5 改了规则,挂单成交后进入服务器历史,终端只暴露总持仓量。EA 想平掉自己那部分仓位时,没有“按原订单号反向”的概念,只能全平或部平,但部平哪一段说不清。 若靠翻历史订单去匹配 EA 自己发的单,再关联市况拦截回报,实现成本极高,且难判断某单之前是否已被拦过。假设当前仓位只归一个策略的这一次 EA,也仅适用于单策略单实例,多系统同跑就崩。 最直接的办法是在模型内存里自建一张虚拟订单表:模型发单时把 ticket 记进 CTableOrders 实例,用熟悉的链表挂到 ListTableOrders。知道 ticket 就能反查几乎所有信息,模型与订单号因此绑定。 MT5 的 SL/TP 是绑总仓位的——例如先开 1.0 手 EURUSD 买无止损,后开 0.1 手买设距现价 100 点止损止盈,价格触线时 1.1 手整仓被平。所以多系统 EA 不能依赖系统级 SL/TP,只能在虚拟订单内写自己的模拟止损止盈;价位一到,EA 自行拦截该单并从活动表移除。 CTableOrders 的 Add() 是先拿订单号去历史订单分配(用 CHistoryOrderInfo),失败再试当前挂单 OrderSelect()。文中所有交易模型只做市价执行(ORDER_TYPE_BUY / ORDER_TYPE_SELL),挂单策略靠这张表也能落地。外汇与贵金属杠杆高,自建虚拟表不消除爆仓风险,参数错了照样快速回撤。
class="macro">#class="kw">property copyright "Copyright class="num">2010, MetaQuotes Software Corp." class="macro">#include <Trade\OrderInfo.mqh> class="macro">#include <Trade\HistoryOrderInfo.mqh> class="macro">#include <Arrays\List.mqh> class CTableOrders : CObject { class="kw">private: class="type">ulong m_magic; class=class="str">"cmt">// EA幻数,标识发出订单的EA class="type">ulong m_ticket; class=class="str">"cmt">// 基础订单号 class="type">ulong m_ticket_sl; class=class="str">"cmt">// 绑定基础订单的模拟止损单号 class="type">ulong m_ticket_tp; class=class="str">"cmt">// 绑定基础订单的模拟止盈单号 ENUM_ORDER_TYPE m_type; class=class="str">"cmt">// 订单类型 class="type">class="kw">datetime m_time_setup; class=class="str">"cmt">// 订单建立时间 class="type">class="kw">double m_price; class=class="str">"cmt">// 订单价格 class="type">class="kw">double m_sl; class=class="str">"cmt">// 止损价(虚拟) class="type">class="kw">double m_tp; class=class="str">"cmt">// 止盈价(虚拟) class="type">class="kw">double m_volume_initial; class=class="str">"cmt">// 订单交易量 class="kw">public: CTableOrders(); class="type">bool Add(COrderInfo &order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit);
「订单容器类的存取与初始化写法」
把历史订单和当下挂单统一收进一个 CTableOrders 结构,核心就是两个重载的 Add 方法:一个吃 CHistoryOrderInfo,一个吃 COrderInfo,都靠外部传入 stop_loss / take_profit 来补全止损止盈字段。 类里暴露的只读接口很直白——StopLoss()、TakeProfit()、Magic()、Ticket()、Price() 等直接 return 私有成员,Type() 则把 m_type 强转回 ENUM_ORDER_TYPE 方便后续按类型分支。 默认构造函数把 m_magic、m_ticket、m_type、m_time_setup 清零,m_price 与 m_volume_initial 置 0.0,意味着新建对象不绑定任何订单,必须等 Add 成功后才算落地。 Add 内部先 HistoryOrderSelect / OrderSelect 按票号定位,失败就返回 false,调用方得自己处理这种未选中情况;外汇与贵金属品种波动剧烈,回测里若票号失效会导致容器漏单,实盘前建议在策略测试器用 EURUSD 的 2023 年数据跑一遍确认命中率。
class="type">bool Add(CHistoryOrderInfo &history_order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit); class="type">class="kw">double StopLoss(class="type">void){class="kw">return(m_sl);} class="type">class="kw">double TakeProfit(class="type">void){class="kw">return(m_tp);} class="type">ulong Magic(){class="kw">return(m_magic);} class="type">ulong Ticket(){class="kw">return(m_ticket);} class="type">int Type() const; class="type">class="kw">datetime TimeSetup(){class="kw">return(m_time_setup);} class="type">class="kw">double Price(){class="kw">return(m_price);} class="type">class="kw">double VolumeInitial(){class="kw">return(m_volume_initial);} }; CTableOrders::CTableOrders(class="type">void) { m_magic=class="num">0; m_ticket=class="num">0; m_type=class="num">0; m_time_setup=class="num">0; m_price=class="num">0.0; m_volume_initial=class="num">0.0; } class="type">bool CTableOrders::Add(CHistoryOrderInfo &history_order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit) { if(HistoryOrderSelect(history_order_info.Ticket())){ m_magic=history_order_info.Magic(); m_ticket=history_order_info.Ticket(); m_type=history_order_info.Type(); m_time_setup=history_order_info.TimeSetup(); m_volume_initial=history_order_info.VolumeInitial(); m_price=history_order_info.PriceOpen(); m_sl=stop_loss; m_tp=take_profit; class="kw">return(true); } else class="kw">return(false); } class="type">bool CTableOrders::Add(COrderInfo &order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit) { if(OrderSelect(order_info.Ticket())){ m_magic=order_info.Magic(); m_ticket=order_info.Ticket(); m_type=order_info.Type(); m_time_setup=order_info.TimeSetup(); m_volume_initial=order_info.VolumeInitial(); m_price=order_info.PriceOpen(); m_sl=stop_loss; m_tp=take_profit; class="kw">return(true); } else class="kw">return(false); } class="type">int CTableOrders::Type() const { class="kw">return((ENUM_ORDER_TYPE)m_type); }
CModel 基类把交易模型骨架定死了
写 EA 时如果多个模型要共用一套订单表和风控逻辑,先理解 CModel 这个基类能省掉大量重复代码。它把任何交易模型都需要的常量收拢到一起:幻数 m_magic、交易品种 m_symbol、周期 m_timeframe、模型名 m_model_name,外加一个 m_delta 用作资金管理的系数——比如账户风险 2% 就填 0.02,不用资本化公式时默认 0.0 即可。 订单数据走指针动态创建:CTableOrders *table 存订单表,CList *ListTableOrders 存每个订单的副本。辅助类从 CAccountInfo 到 CPositionInfo 一应俱全,派生模型不用再自己调 OrderSelect / OrderSend 那套底层函数。 m_timing 里塞的是 t_period 结构,解决多模型不同周期触发的问题。OnTimer 不够用,因为小时图模型要每根小时柱触发、日图模型要每根日柱触发。t_period 记上次柱时间,比较当前柱时间不等就返回 true 并更新,相等返回 false,新柱判定就这么简单。 SendOrder 是唯一的发单出口。它先用 CheckLot 校验可行交易量:假设双向上限 15 标准手,已有 3 手买仓,模型想加 18.6 手买,CheckLot 会修正成 12 手;若已有的是卖仓则直接给 15 手。超量返回 EMPTY_VALUE 必须往上传递。 预付款不够时 CheckMargin 会尝试缩量,连最小手都保不住就进追加状态返回 EMPTY_VALUE。遇到超大单量还会拆单:想买 11.3 手 EURUSD、上限 5.0 手,就拆成 5.0 + 5.0 + 1.3 三个独立订单进表,各自带止损获利,平仓时按顺序扫。ADD_ORDER 进表、DELETE_ORDER 发完删链接,靠订单号或填零区分。 基类还提供 Add / Delete / GetNumberOrders / CloseAllPosition / Processing 等函数,Init 在构造里自动跑。下面这段是类声明的核心部分,指针成员和辅助类一目了然。
class CModel : class="kw">public CObject { class="kw">protected: class="type">long m_magic; class="type">class="kw">string m_symbol; ENUM_TIMEFRAMES m_timeframe; class="type">class="kw">string m_model_name; class="type">class="kw">double m_delta; CTableOrders *table; CList *ListTableOrders; CAccountInfo m_account_info; CTrade m_trade; CSymbolInfo m_symbol_info; COrderInfo m_order_info; CHistoryOrderInfo m_history_order_info; CPositionInfo m_position_info; CDealInfo m_deal_info; t_period m_timing; class="kw">public: CModel() { Init(); } ~CModel() { Deinit(); }
◍ 订单模型与周期切换的底层接口
这套交易模型把订单对象的核心读写都收口在几个访问器里:Name() 双向重载可读写模型名,Timeframe() 只暴露周期枚举,Symbol() 同样双向重载。虚拟函数 Init、Deinit、Processing 留了扩展点,Deinit 里直接 delete ListTableOrders 释放挂单表,避免 EA 卸载时泄漏。 protected 区才是真正落单的逻辑:Add() 接收 COrderInfo 或 CHistoryOrderInfo 并带入 sl/tp,SendOrder() 汇总 symbol、订单类型、模式、票号、手数、价格、止损止盈和注释一次性发单。CloseAllPosition 与两种 Delete 重载(按枚举或 ticket)构成持仓清理面。 周期触发靠 t_period 结构体和 timing() 配合。t_period 用 m1 到 mn1 共 21 个 datetime 字段记住各周期上一根 K 线的开盘时间,current 存当前。timing() 每次只 CopyRates 取最新 1 根,若 rez==0 打印 Error timing 并返回 false;否则按 tf 比对 raters[0].time 与对应字段,相同说明本根已处理过返回 false,不同则更新字段并返回 true——这是典型的「每根 K 线只跑一次」闸门。 把这段直接塞进你的 EA 头文件,在 OnTick 里对 PERIOD_M1 调 timing,就能验证黄金 1 分钟图平均每日触发约 1440 次、其中因重复被拦掉的占绝大多数。外汇与贵金属波动剧烈,这类高频触发逻辑需自行压测滑点风险。
class="type">class="kw">string Name(){class="kw">return(m_model_name);} class="type">void Name(class="type">class="kw">string name){m_model_name=name;} ENUM_TIMEFRAMES Timeframe(class="type">void){class="kw">return(m_timeframe);} class="type">class="kw">string Symbol(class="type">void){class="kw">return(m_symbol);} class="type">void Symbol(class="type">class="kw">string set_symbol){m_symbol=set_symbol;} class="type">bool class="kw">virtual Init(); class="type">void class="kw">virtual Deinit(){class="kw">delete ListTableOrders;} class="type">bool class="kw">virtual Processing(){class="kw">return (true);} class="type">class="kw">double GetMyPosition(); class="type">bool Delete(ENUM_TYPE_DELETED_ORDER); class="type">bool Delete(class="type">ulong Ticket); class="type">void CloseAllPosition(); class=class="str">"cmt">//class="type">bool class="kw">virtual Trade(); class="kw">protected: class="type">bool Add(COrderInfo &order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit); class="type">bool Add(CHistoryOrderInfo &history_order_info, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit); class="type">void GetNumberOrders(n_orders &orders); class="type">bool SendOrder(class="type">class="kw">string symbol, ENUM_ORDER_TYPE op_type, ENUM_ORDER_MODE op_mode, class="type">ulong ticket, class="type">class="kw">double lot, class="type">class="kw">double price, class="type">class="kw">double stop_loss, class="type">class="kw">double take_profit, class="type">class="kw">string comment); }; class="kw">struct t_period { class="type">class="kw">datetime m1; class="type">class="kw">datetime m2; class="type">class="kw">datetime m3; class="type">class="kw">datetime m4; class="type">class="kw">datetime m5; class="type">class="kw">datetime m6; class="type">class="kw">datetime m10; class="type">class="kw">datetime m12; class="type">class="kw">datetime m15; class="type">class="kw">datetime m20; class="type">class="kw">datetime m30; class="type">class="kw">datetime h1; class="type">class="kw">datetime h2; class="type">class="kw">datetime h3; class="type">class="kw">datetime h4; class="type">class="kw">datetime h6; class="type">class="kw">datetime h8; class="type">class="kw">datetime h12; class="type">class="kw">datetime d1; class="type">class="kw">datetime w1; class="type">class="kw">datetime mn1; class="type">class="kw">datetime current; }; class="type">bool timing(class="type">class="kw">string symbol, ENUM_TIMEFRAMES tf, t_period &timeframes) { class="type">int rez; class="type">MqlRates raters[class="num">1]; rez=CopyRates(symbol, tf, class="num">0, class="num">1, raters); if(rez==class="num">0) { Print("Error timing"); class="kw">return(false); } class="kw">switch(tf){ case PERIOD_M1: if(raters[class="num">0].time==timeframes.m1)class="kw">return(false); else{timeframes.m1=raters[class="num">0].time; class="kw">return(true);} }