开发多币种 EA 交易(第 3 部分):架构修改·进阶篇
(2/3)· 当多个策略实例挤在同一账户,最小手数与反向仓位的坑怎么填
◍ 虚拟顾问的收发骨架怎么搭
在 MT5 里做虚拟持仓回测,核心是把「策略逻辑」和「下单通道」拆开。CVirtualAdvisor 这个类就是这么干的:protected 区挂了一个 CVirtualReceiver 指针 m_receiver,专门负责把虚拟仓位送到市场模拟层;public 区只暴露构造、析构和 Tick 重写,对外接口极薄。 构造时默认传 ulong p_magic = 1,意思是魔数写死为 1,直接调 CVirtualReceiver::Instance(p_magic) 拿静态单例。这样多顾问并行时,靠不同 magic 隔离接收器,不会串仓。 析构里只有一句 delete m_receiver,把接收器实例释放掉;Tick() 才是每根报价进来后的主循环——先 m_receiver.Tick() 处理虚拟仓位状态,再 CAdvisor::Tick() 跑策略本身,最后 m_receiver.Correct() 做市场成交量修正。三者顺序不能乱,否则仓位和信号会对不上。 外汇与贵金属杠杆高、滑点随机,这套虚拟层只解决回测隔离,不预示实盘收益,复制去 MT5 编译时先把 magic 改成你账号不冲突的值。
class="kw">protected: CVirtualReceiver *m_receiver; class=class="str">"cmt">// Receiver object that brings positions to the market class="kw">public: CVirtualAdvisor(class="type">ulong p_magic = class="num">1); class=class="str">"cmt">// Constructor ~CVirtualAdvisor(); class=class="str">"cmt">// Destructor class="kw">virtual class="type">void Tick() class="kw">override; class=class="str">"cmt">// OnTick event handler }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(class="type">ulong p_magic = class="num">1) : class=class="str">"cmt">// Initialize the receiver with a class="kw">static receiver m_receiver(CVirtualReceiver::Instance(p_magic)) {}; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualAdvisor::~CVirtualAdvisor() { class="kw">delete m_receiver; class=class="str">"cmt">// Remove the recipient } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTick event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualAdvisor::Tick(class="type">void) { class=class="str">"cmt">// Receiver handles class="kw">virtual positions m_receiver.Tick(); class=class="str">"cmt">// Start handling in strategies CAdvisor::Tick(); class=class="str">"cmt">// Adjusting market volumes m_receiver.Correct(); } class=class="str">"cmt">//+------------------------------------------------------------------+
「给虚拟仓位接上接收器和策略的指针」
虚拟仓位类现在需要持有指向 m_receiver 和 m_strategy 两个对象的指针,这两个字段不能在类内部凭空生成,只能在构造时由外部传入。构造函数因此改成了带 CVirtualReceiver* 和 CVirtualStrategy* 参数的形式,初始化列表里补了 m_receiver(p_receiver) 和 m_strategy(p_strategy) 两行,把外部对象的地址接进来。 为了方便后续排查和日志输出,类里加了 Id() 和 Symbol() 两个读取器,分别返回 m_id 和 m_symbol。其中 m_id 来自静态计数器 s_count 自增后的值,也就是每新建一个虚拟仓位对象,ID 就比之前多 1,这个细节在回测多仓位并发时很有用。 通知逻辑只挂在 Open() 和 Close() 上。开仓成功后会调用 m_receiver.OnOpen(GetPointer(this)) 把当前对象指针抛给接收器,同时 m_strategy.OnOpen() 触发策略侧动作;策略处理函数暂时不吃参数,所以实现里直接空参调用。外汇与贵金属市场高杠杆、滑点随机,这类虚拟撮合仅用于历史行情回放,实盘信号仍需独立验证。 代码继续留在 VirtualOrder.mqh 同名文件里,MT5 里 include 进来就能直接编译跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of class="kw">virtual orders and positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualOrder { class="kw">private: class=class="str">"cmt">//--- Static fields... class=class="str">"cmt">//--- Related recipient objects and strategies CVirtualReceiver *m_receiver; CVirtualStrategy *m_strategy; class=class="str">"cmt">//--- Order(position) properties ... class=class="str">"cmt">//--- Closed order(position) properties ... class=class="str">"cmt">//--- Private methods class="kw">public: CVirtualOrder( CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy ); class=class="str">"cmt">// Constructor class=class="str">"cmt">//--- Methods for checking the position(order) status ... class=class="str">"cmt">//--- Methods for receiving position(order) properties ... class="type">ulong Id() { class=class="str">"cmt">// ID class="kw">return m_id; } class="type">class="kw">string Symbol() { class=class="str">"cmt">// Symbol class="kw">return m_symbol; } class=class="str">"cmt">//--- Methods for handling positions(orders) ... }; CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) : class=class="str">"cmt">// Initialization list m_id(++s_count), class=class="str">"cmt">// New ID = object counter + class="num">1 m_receiver(p_receiver), m_strategy(p_strategy), ..., m_point(class="num">0) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Open a class="kw">virtual position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualOrder::Open(...) { class=class="str">"cmt">// If the position is already open, then do nothing ... if(s_symbolInfo.Name(symbol)) { class=class="str">"cmt">// Select the desired symbol class=class="str">"cmt">// Update information about current prices ... class=class="str">"cmt">// Initialize position properties ... class=class="str">"cmt">// Depending on the direction, set the opening price, as well as the SL and TP levels ... class=class="str">"cmt">// Notify the recipient and the strategy that the position(order) is open m_receiver.OnOpen(GetPointer(this)); m_strategy.OnOpen(); ... class="kw">return true; } class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Close a position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualOrder::Close() { if(IsOpen()) { class=class="str">"cmt">// If the position is open ... class=class="str">"cmt">// Define the closure reason to be displayed in the log ...
平仓后把控制权交还策略与接收端
在持仓或挂单被平掉之后,EA 不能闷头自己处理完就结束,得把事件显式通知出去。上面这段把收盘价按类型存好,随后立即调用接收端与策略的 OnClose 接口,让外部逻辑知道这一笔已经收尾。 m_receiver.OnClose(GetPointer(this)) 把当前 CTrade 派生对象的指针传给接收器,接收器据此做日志、统计或推送;m_strategy.OnClose() 则清空策略层对该笔持仓的状态标记。漏掉这两句,策略可能还以为仓在,下一次信号判断就会出错。 在 MT5 里实跑时,可在 OnClose 里打印 PositionGetInteger(POSITION_TICKET) 是否为 0,确认平仓事件确实已传导,而不是只停在本地变量层。外汇与贵金属杠杆高,事件漏通知会造成重复开仓,回测看不出、实盘才爆。
class=class="str">"cmt">// Save the close price depending on the type ... class=class="str">"cmt">// Notify the recipient and the strategy that the position(order) is open m_receiver.OnClose(GetPointer(this)); m_strategy.OnClose(); }
◍ 用单件模式锁住虚拟接收器实例
把 CVirtualReceiver 做成 Singleton,核心就三件事:构造函数私有化、加一个 static 指针 s_instance 存唯一对象、用 static 方法 Instance() 在空的时候 new 一个、非空直接返指针。这样整个 EA 里虚拟仓位转实盘的中枢只有一份,不会出现多个接收器互相覆盖仓位状态。 每个策略实例通过静态 Get() 向接收器要 N 个虚拟仓位,接收器在 m_orders 数组里挂好指针,同时也填进策略自己的虚拟仓位数组。注意 Get() 的入参 n 就是策略需要的虚拟仓位数量,调的时候数错了,后面 Correct() 会漏调。 接收器还持有一组交易品种接收器指针(CVirtualSymbolReceiver,尚未实现但已知职责是按品种直开直平)。Correct() 在接收器这层只是个壳:循环调每个品种接收器的 Correct(),真正干活在下层。 为了减少无谓交易触发,加 IsTradeAllowed() 查终端和账户状态,并用 m_isChanged 标记虚拟仓位有无变动。只有标记为真才进 Correct(),调成功后清标记。标记设置在 CVirtualOrder 的 OnOpen()/OnClose() 里——开平虚拟仓位即置位,同时首次开某品种虚拟仓时要现建品种接收器并通知,后续只通知已有接收器。 虚拟仓位除了信号开平,还能在 Tick() 里撞止损止盈自平。CVirtualReceiver::Tick() 负责每个分时遍历 m_orders 全调一遍。最后析构函数必须释放 m_orders 里所有 new 出来的对象,否则 MT5 跑久了内存泄漏会让 EA 变卡甚至崩。把上述代码存成 VirtualReceiver.mqh 即可在 MT5 里 include 验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for converting open volumes to market positions(receiver) | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualReceiver : class="kw">public CReceiver { class="kw">protected: class=class="str">"cmt">// Static pointer to a single class instance class="kw">static CVirtualReceiver *s_instance; ... CVirtualReceiver(class="type">ulong p_magic = class="num">0); class=class="str">"cmt">// Private constructor class="kw">public: class=class="str">"cmt">//--- Static methods class="kw">static CVirtualReceiver *Instance(class="type">ulong p_magic = class="num">0); class=class="str">"cmt">// Singleton - creating and getting a single instance ... }; class=class="str">"cmt">// Initializing a class="kw">static pointer to a single class instance CVirtualReceiver *CVirtualReceiver::s_instance = NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Singleton - creating and getting a single instance | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualReceiver* CVirtualReceiver::Instance(class="type">ulong p_magic = class="num">0) { if(!s_instance) { s_instance = new CVirtualReceiver(p_magic); } class="kw">return s_instance; } class CVirtualReceiver : class="kw">public CReceiver { class="kw">protected: ... CVirtualOrder *m_orders[]; class=class="str">"cmt">// Array of class="kw">virtual positions ... class="kw">public: class=class="str">"cmt">//--- Static methods ... class="kw">static class="type">void Get(CVirtualStrategy *strategy, CVirtualOrder *&orders[], class="type">int n); class=class="str">"cmt">// Allocate the necessary amount of class="kw">virtual positions to the strategy ... }; ... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Allocate the necessary amount of class="kw">virtual positions to strategy | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">static class="type">void CVirtualReceiver::Get(CVirtualStrategy *strategy, class=class="str">"cmt">// Strategy CVirtualOrder *&orders[], class=class="str">"cmt">// Array of strategy positions class="type">int n class=class="str">"cmt">// Required number ) {
「虚拟持仓的接收与调仓闸门」
在 MT5 的 EA 框架里,虚拟持仓系统靠一个 Receiver 单例来集中接管开平信号。下面这段初始化把订单数组扩容后,逐个 new 出 CVirtualOrder 对象并挂到 self.m_orders,相当于在内存里先铺好一批待撮合的仓位壳子。 CVirtualReceiver *self = Instance(); // Receiver singleton ArrayResize(orders, n); // Expand the array of virtual positions FOREACH(orders, orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects APPEND(self.m_orders, orders[i])) // Register the created virtual position ... Correct() 是调仓总闸:仅当 m_isChanged 为真且 IsTradeAllowed() 通过时才遍历各品种接收器做音量修正,跑完把 m_isChanged 翻成 !res。也就是说,若任一手符号级 Correct 返回 false,本次调整视为失败且不清除变更标记,下一 tick 还会再触发。 bool CVirtualReceiver::Correct() { bool res = true; if(m_isChanged && IsTradeAllowed()) { FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct()); m_isChanged = !res; } return res; } IsTradeAllowed() 同时查了 5 个环境位:MQL_TRADE_ALLOWED、TERMINAL_TRADE_ALLOWED、ACCOUNT_TRADE_EXPERT、ACCOUNT_TRADE_ALLOWED、TERMINAL_CONNECTED。少一个为真,虚拟单就不会下发——实盘外汇/贵金属波动大、断线常见,这个多重校验能避免重连瞬间乱补单。 bool CVirtualReceiver::IsTradeAllowed() { return (true && MQLInfoInteger(MQL_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && AccountInfoInteger(ACCOUNT_TRADE_EXPERT) && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_CONNECTED) ); } 开平事件由 OnOpen / OnClose 两个 public 方法承接,外部策略只需把 CVirtualOrder* 抛进来,Receiver 自己决定怎么落进符号级队列。想验证的话,在 MT5 里给 Correct() 加 Print(res) 跑一晚,大概率能看到断线期间 res 连续为 false 的日志。
CVirtualReceiver *self = Instance(); class=class="str">"cmt">// Receiver singleton ArrayResize(orders, n); class=class="str">"cmt">// Expand the array of class="kw">virtual positions FOREACH(orders, orders[i] = new CVirtualOrder(self, strategy); class=class="str">"cmt">// Fill the array with new objects APPEND(self.m_orders, orders[i])) class=class="str">"cmt">// Register the created class="kw">virtual position ... class CVirtualReceiver : class="kw">public CReceiver { class="kw">protected: ... CVirtualSymbolReceiver *m_symbolReceivers[]; class=class="str">"cmt">// Array of recipients for individual symbols class="kw">public: ... class=class="str">"cmt">//--- Public methods class="kw">virtual class="type">bool Correct() class="kw">override; class=class="str">"cmt">// Adjustment of open volumes }; class CVirtualReceiver : class="kw">public CReceiver { ... class="type">bool m_isChanged; class=class="str">"cmt">// Are there any changes in open positions? ... class="type">bool IsTradeAllowed(); class=class="str">"cmt">// Is trading available? class="kw">public: ... class="kw">virtual class="type">bool Correct() class="kw">override; class=class="str">"cmt">// Adjustment of open volumes }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Adjust open volumes | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualReceiver::Correct() { class="type">bool res = true; if(m_isChanged && IsTradeAllowed()) { class=class="str">"cmt">// If there are changes, then we call the adjustment of the recipients of individual symbols FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct()); m_isChanged = !res; } class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Is trading available? | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualReceiver::IsTradeAllowed() { class="kw">return (true && MQLInfoInteger(MQL_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && AccountInfoInteger(ACCOUNT_TRADE_EXPERT) && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_CONNECTED) ); } class CVirtualReceiver : class="kw">public CReceiver { ... class="kw">public: ... class=class="str">"cmt">//--- Public methods class="type">void OnOpen(CVirtualOrder *p_order); class=class="str">"cmt">// Handle class="kw">virtual position opening class="type">void OnClose(CVirtualOrder *p_order); class=class="str">"cmt">// Handle class="kw">virtual position closing ... };
虚拟持仓的开关与心跳怎么接
虚拟订单接收器把开仓、平仓、逐笔心跳三类事件拆开处理,核心是把订单按品种路由到对应的子接收器。开仓时若品种接收器不存在就现场 new 一个再追加进数组,存在则直接复用,避免重复实例化。 平仓逻辑更轻:只在品种接收器已存在时转发关闭通知,找不到就静默跳过,不补建接收器。两个分支都把 m_isChanged 置真,供上层判断是否需要重绘或推送到「小布盯盘」。 Tick() 用 FOREACH 把行情脉冲逐条灌给 m_orders 里的每个虚拟仓位;析构函数则反向清空,delete 所有残留虚拟单,防止 EA 重载时内存泄漏。外汇与贵金属杠杆高,虚拟回测和实盘滑点可能偏差明显,参数请在 MT5 策略测试器先跑一轮再上真仓。 下面这段可直接粘进你的 virtual_receiver.mqh 对照改:
class=class="str">"cmt">//| Handle opening a class="kw">virtual position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) { class="type">class="kw">string symbol = p_order.Symbol(); class=class="str">"cmt">// 取当前订单的交易品种 CVirtualSymbolReceiver *symbolReceiver; class="type">int i; FIND(m_symbolReceivers, symbol, i); class=class="str">"cmt">// 在品种接收器数组里找该品种 if(i == -class="num">1) { class=class="str">"cmt">// 没找到就新建一个该品种的接收器 symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol); class=class="str">"cmt">// 把它塞进品种接收器数组 APPEND(m_symbolReceivers, symbolReceiver); } else { class=class="str">"cmt">// 找到了就直接用现成的 symbolReceiver = m_symbolReceivers[i]; } symbolReceiver.Open(p_order); class=class="str">"cmt">// 通知品种接收器:新仓位来了 m_isChanged = true; class=class="str">"cmt">// 标记状态有变动 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle closing a class="kw">virtual position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualReceiver::OnClose(CVirtualOrder *p_order) { class="type">class="kw">string symbol = p_order.Symbol(); class=class="str">"cmt">// 取平仓单的品种 class="type">int i; FIND(m_symbolReceivers, symbol, i); class=class="str">"cmt">// 找品种接收器 if(i != -class="num">1) { m_symbolReceivers[i].Close(p_order); class=class="str">"cmt">// 通知对应接收器平仓 m_isChanged = true; class=class="str">"cmt">// 标记有变动 } } class CVirtualReceiver : class="kw">public CReceiver { ... class="kw">public: ... class=class="str">"cmt">//--- Public methods class="type">void Tick(); class=class="str">"cmt">// 处理虚拟订单数组的每次报价 ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle a tick for the array of class="kw">virtual orders(positions) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualReceiver::Tick() { FOREACH(m_orders, m_orders[i].Tick()); class=class="str">"cmt">// 每个虚拟单都吃一次Tick } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualReceiver::~CVirtualReceiver() { FOREACH(m_orders, class="kw">delete m_orders[i]); class=class="str">"cmt">// 清掉所有虚拟仓位 }