开发多币种 EA 交易(第 2 部分):过渡到交易策略的虚拟仓位·综合运用
(3/3)·当策略数冲到上百,真实开仓会逼停大半实例,这篇给收口方案
◍ 把简单策略改成虚拟仓位驱动
基类 CStrategy 里已经留了 m_orders[] 数组专门装虚拟仓位对象,CSimpleVolumesStrategy 直接继承就能用,不用再另起炉灶。策略的 m_maxCountOfOrders 参数定死了同屏最大开仓数,构造函数里把虚拟仓位数组尺寸设成跟它相等,避免越界。 真正的改动落在 OpenBuyOrder() 和 OpenSellOrder():原来下真实市价单的地方,换成在 m_orders[] 里找第一个 IsOpen() 为假的槽位,调它的 Open() 写虚拟买卖。挂单类的 BuyStop / SellLimit 暂时没有虚拟等价物,原文是直接用块注释 /* */ 把 trade.xxx 那几行灭掉,不删代码只停用。 改完存回 SimpleVolumesStrategy.mqh 即可,MT5 里把 EA 切到这个头文件编译,能在回测里看到虚拟仓位按 m_maxCountOfOrders 上限堆叠,而不会真去 broker 那侧占用保证金。外汇与贵金属品种波动大,虚拟逻辑验证通过后仍需小仓位实盘核对滑点差异。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Open BUY order | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSimpleVolumesStrategy::OpenBuyOrder() { class=class="str">"cmt">// ... if(m_openDistance > class="num">0) { class=class="str">"cmt">/* class=class="str">"cmt">// Set BUY STOP pending order res = trade.BuyStop(lot, ...); */ } else if(m_openDistance < class="num">0) { class=class="str">"cmt">/* class=class="str">"cmt">// Set BUY LIMIT pending order res = trade.BuyLimit(lot, ...); */ } else { class=class="str">"cmt">// Open a class="kw">virtual BUY position for(class="type">int i = class="num">0; i < m_maxCountOfOrders; i++) { class=class="str">"cmt">// Iterate through all class="kw">virtual positions if(!m_orders[i].IsOpen()) { class=class="str">"cmt">// If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; class=class="str">"cmt">// and exit } } } ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Open SELL order | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSimpleVolumesStrategy::OpenSellOrder() { class=class="str">"cmt">// ... if(m_openDistance > class="num">0) { class=class="str">"cmt">/* class=class="str">"cmt">// Set SELL STOP pending order res = trade.SellStop(lot, ...); */ } else if(m_openDistance < class="num">0) { class=class="str">"cmt">/* class=class="str">"cmt">// Set SELL LIMIT pending order res = trade.SellLimit(lot, ...); */ } else { class=class="str">"cmt">// Open a class="kw">virtual SELL position for(class="type">int i = class="num">0; i < m_maxCountOfOrders; i++) { class=class="str">"cmt">// Iterate through all class="kw">virtual positions if(!m_orders[i].IsOpen()) { class=class="str">"cmt">// If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_SELL, m_fixedLot, NormalizeDouble(sl, digits),
止盈价的小数位归一化细节
在 EA 下单逻辑里,对止盈价做 NormalizeDouble(tp, digits) 是关键一步。MT5 的报价小数位由品种决定,黄金通常 digits=3 或 2,欧美多为 5 或 4;若直接把计算出的 tp 传给 OrderSend,可能因精度不匹配被服务器拒单。 上面这段代码把 tp 用 NormalizeDouble 收敛到 digits 位,随后 break 退出循环。这个写法意味着每根 K 线或每次 tick 只处理一个挂单分支,避免重复发送。 实操上,开 MT5 按 F4 把这段嵌进你的下单函数,打印 NormalizeDouble 前后数值,能直观看到黄金 XAUUSD 在 digits=3 时 1900.1234 会变成 1900.123。外汇与贵金属杠杆高,点位误差虽小但可能直接废单,建议每次改品种后重测 digits 变量。
NormalizeDouble(tp, digits)); break; class=class="str">"cmt">// and exit } } } } } }
「派生类接管实际建仓逻辑」
基类只负责把未平仓交易量抽象成对象,真正在市场上开平仓位要交给派生类 CVolumeReceiver 来做。这个类继承 CReceiver,核心在 Correct() 方法的重载链:先按交易品种汇总各策略的总持仓,生成 newVolumes 数组,再逐品种比对旧持仓与所需变化量,决定是加仓、部分平仓还是对冲平仓。 账户类型直接决定执行路径。m_isNetting 标记当前是净仓账户还是对冲账户,对冲账户下 CloseHedgingPartial 会按 ENUM_POSITION_TYPE 区分买仓卖仓分别处理;净仓账户则走 ClearOpen 与 AddBuy/AddSell 的单向调整。 构造函数里 p_minMargin 默认给 100,对应 FreeMarginCheck 中开仓前的最低保证金门槛。把这个类存成 VolumeReceiver.mqh,include 进 Receiver.mqh 后,你可以在 MT5 里改 p_minMargin 的值观察小额账户在波动行情中被拒单的频率变化——外汇与贵金属杠杆高,保证金不足会直接打断仓位同步,属常见高风险坑。
class="macro">#include "Receiver.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for converting open volumes into market positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVolumeReceiver : class="kw">public CReceiver { class="kw">protected: class="type">bool m_isNetting; class=class="str">"cmt">// Is this a netting account? class="type">class="kw">string m_symbols[]; class=class="str">"cmt">// Array of used symbols class="type">class="kw">double m_minMargin; class=class="str">"cmt">// Minimum margin for opening CPositionInfo m_position; CSymbolInfo m_symbolInfo; CTrade m_trade; class=class="str">"cmt">// Filling the array of open market volumes by symbols class="type">void FillSymbolVolumes(class="type">class="kw">double &oldVolumes[]); class=class="str">"cmt">// Correction of open volumes using the array of volumes class="kw">virtual class="type">bool Correct(class="type">class="kw">double &symbolVolumes[]); class=class="str">"cmt">// Volume correction for this symbol class="type">bool CorrectPosition(class="type">class="kw">string symbol, class="type">class="kw">double oldVolume, class="type">class="kw">double diffVolume); class=class="str">"cmt">// Auxiliary methods class="type">bool ClearOpen(class="type">class="kw">string symbol, class="type">class="kw">double diffVolume); class="type">bool AddBuy(class="type">class="kw">string symbol, class="type">class="kw">double volume); class="type">bool AddSell(class="type">class="kw">string symbol, class="type">class="kw">double volume); class="type">bool CloseBuyPartial(class="type">class="kw">string symbol, class="type">class="kw">double volume); class="type">bool CloseSellPartial(class="type">class="kw">string symbol, class="type">class="kw">double volume); class="type">bool CloseHedgingPartial(class="type">class="kw">string symbol, class="type">class="kw">double volume, class="type">ENUM_POSITION_TYPE type); class="type">bool CloseFull(class="type">class="kw">string symbol = ""); class="type">bool FreeMarginCheck(class="type">class="kw">string symbol, class="type">class="kw">double volume, ENUM_ORDER_TYPE type); class="kw">public: CVolumeReceiver(class="type">ulong p_magic, class="type">class="kw">double p_minMargin = class="num">100); class=class="str">"cmt">// Constructor class="kw">virtual class="type">void Add(CStrategy *strategy) class="kw">override; class=class="str">"cmt">// Add strategy class="kw">virtual class="type">bool Correct() class="kw">override; class=class="str">"cmt">// Adjustment of open volumes }; class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 多策略持仓量的聚合与差额调仓
在同时跑多个策略的 EA 里,同一个交易品种可能被好几套逻辑同时占用仓位。CVolumeReceiver::Correct() 先按品种把 m_strategies 里各策略的 Volume() 累加进 newVolumes 数组,得到该符号当前应持有的总敞口。 真正动手前,Correct(double &newVolumes[]) 会拿新敞口和 oldVolumes 逐品种做差:diffVolume = newVolume - oldVolume。只有 MathAbs(diffVolume) > 0.001 时才认为需要调整——这个 0.001 的手数阈值直接决定了会不会因微小浮动频繁发单。 调仓方向由 oldVolume 正负与 diffVolume 符号共同决定。原有多头(oldVolume>0)时,diffVolume 为正就加买、为负且小于原仓就平部分、大于等于原仓就全平。外汇与贵金属杠杆高,这类自动加减仓逻辑若阈值设错,可能在波动期连续触发,放大回撤概率。 下面这段是原文核心聚合与差额判定,可直接贴进 MT5 看类结构是否对齐:
class="type">bool CVolumeReceiver::Correct() { class="type">int symbolsTotal = ArraySize(m_symbols); class="type">class="kw">double newVolumes[]; ArrayResize(newVolumes, symbolsTotal); ArrayInitialize(newVolumes, class="num">0); for(class="type">int j = class="num">0; j < symbolsTotal; j++) { for(class="type">int i = class="num">0; i < ArraySize(m_strategies); i++) { if(m_strategies[i].Symbol() == m_symbols[j]) { newVolumes[j] += m_strategies[i].Volume(); } } } class="kw">return Correct(newVolumes); } class="type">bool CVolumeReceiver::Correct(class="type">class="kw">double &newVolumes[]) { class="type">bool res = true; for(class="type">int j = class="num">0; j < ArraySize(m_symbols); j++) { class="type">class="kw">double oldVolume = oldVolumes[j]; class="type">class="kw">double newVolume = newVolumes[j]; class="type">class="kw">double diffVolume = newVolume - oldVolume; if(MathAbs(diffVolume) > class="num">0.001) { res = res && CorrectPosition(m_symbols[j], oldVolume, diffVolume); } } class="kw">return res; } class="type">bool CVolumeReceiver::CorrectPosition(class="type">class="kw">string symbol, class="type">class="kw">double oldVolume, class="type">class="kw">double diffVolume) { class="type">bool res = false; class="type">class="kw">double volume = MathAbs(diffVolume); if(oldVolume > class="num">0) { if(diffVolume > class="num">0) { res = AddBuy(symbol, volume); } else if(diffVolume < class="num">0) { if(volume < oldVolume) { res = CloseBuyPartial(symbol, volume); } else { res = CloseFull(symbol);
持仓方向与新增量的对冲判定
这段逻辑处理的是「已有卖单」状态下,新成交回报带来的仓位变化。当 oldVolume 为负,说明本地记录里有一笔卖仓;此时若 diffVolume 也小于 0,代表远端又开出了新的卖单,直接调用 AddSell 把 volume 累进监控池。 如果 diffVolume 大于 0,则是出现了买方向的成交。这里分两层:当 volume 绝对值小于原卖仓(即 -oldVolume)时,只做部分平仓 CloseSellPartial;否则先 CloseFull 清掉老卖单,若买方量还溢出,就再用 AddBuy 把多出的 volume+oldVolume 开成新买仓。 最后 else 分支覆盖无旧仓情形,直接 ClearOpen 按 diffVolume 同步。外汇与贵金属杠杆高,这类仓位镜像同步若差一个符号,实盘可能瞬间双向敞口,建议把这段代码贴进 MT5 的 EA 回测器,用 0.01 手 EURUSD 跑一次 tick 级成交验证。
if(res && volume > oldVolume) { res = AddSell(symbol, volume - oldVolume); } } } } else if(oldVolume < class="num">0) { class=class="str">"cmt">// Have SELL position if(diffVolume < class="num">0) { class=class="str">"cmt">// New SELL position res = AddSell(symbol, volume); } else if(diffVolume > class="num">0) { class=class="str">"cmt">// New BUY position if(volume < -oldVolume) { res = CloseSellPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > -oldVolume) { res = AddBuy(symbol, volume + oldVolume); } } } } else { class=class="str">"cmt">// No old position res = ClearOpen(symbol, diffVolume); } class="kw">return res;
「用虚拟仓位跑单一策略的 EA 骨架」
把多策略框架砍到只跑一个实例,核心是把 CVolumeReceiver 挂进 EA 构造,再把具体策略类换成 CSimpleVolumesStrategy。这样同一套调度逻辑不用重写,只改注入对象就能切换策略。 代码里三行 include 是必须的前置:Advisor 管总调度,SimpleVolumesStrategy 提供成交量信号逻辑,VolumeReceiver 负责虚拟仓位的成交回报。缺任意一个,OnInit 里的 new 会直接编译报错。 OnInit 中 expert 指针先被赋予带 CVolumeReceiver(magic_) 的 CAdvisor,随后 Add 进一个 CSimpleVolumesStrategy 实例,参数从 symbol_ 到 maxCountOfOrders_ 全部透传,等于把策略绑定到具体品种与周期。OnTick 只调 expert.Tick() 推进,OnDeinit 里 delete expert 防内存泄漏。 在 MT5 里把这段存成 SimpleVolumesExpertSingle.mq5,magic_ 与 fixedLot_ 等输入参数沿用前篇头文件定义,编译后加载到 EURUSD 15 分钟图,可能看到虚拟仓位按 signalPeriod_=20、signalDeviation_=2.0 的设定产生挂单。外汇与贵金属杠杆高,虚拟仓位仅用于逻辑验证,实盘前需自担回测偏差风险。
class="macro">#include "Advisor.mqh" class="macro">#include "SimpleVolumesStrategy.mqh" class="macro">#include "VolumeReceiver.mqh" class=class="str">"cmt">// Input parameters... CAdvisor *expert; class=class="str">"cmt">// Pointer to the EA object class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { expert = new CAdvisor(new CVolumeReceiver(magic_)); expert.Add(new CSimpleVolumesStrategy( symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); class=class="str">"cmt">// Add one strategy instance class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { expert.Tick(); } class="type">void OnDeinit(const class="type">int reason) { class="kw">delete expert; }
◍ 直接开仓与虚拟仓位的实盘对照
把同一套策略、同一组参数分别跑两遍:一遍直接下真单,一遍走虚拟仓位逻辑,间隔压到极短,再导出两份报告比对成交明细。测试品种固定为 EURGBP、手数 0.01,为压缩表格宽度已隐去这些恒定列。 两种模式下建仓价与时间点完全一致。例如 2018.03.02 15:46:47 的 SELL 与 2018.03.06 13:56:04 的 BUY,虚拟仓位版在开 BUY 的同时平掉了旧 SELL(2018.03.06 13:56:04 buy out);而直接开仓版会让反方向头寸共存、持续吃掉库存费。 对 SimpleVolumesMarketExpert.mq5 做同样改造:引入 CVolumeReceiver,在 EA 构造时注入该类并替换原策略实例,另存为 SimpleVolumesExpert.mq5 回测。九策略并发下,带虚拟仓位的版本相对原版利润略增、回撤收窄,夏普比率与利润因子同步抬升——外汇与贵金属杠杆高,这类改善不预示稳健盈利,仅说明摩擦成本被压低。
虚拟仓位只是起点
切换到虚拟仓位后,多策略同跑不再互相挤占下单通道,协同效率明显提升;同时因为不每个实例都开实盘仓,最低保证金占用比单独跑实例更低,净额账户也能接住了。 目前只落地了市价建仓类策略,挂单逻辑、资金管理和跨品种混合策略都还没接进这套结构,当前仓位大小还是固定手数加人工挑。 上面那段把全部输入塞进一个字符串再传给策略构造函数,是这套封装的入口写法: string inInputsAll = NULL; expert.Add(new CSimpleVolumesStrategy(inInputsAll)); 真要上实盘,外汇和贵金属杠杆高、回撤快,先在 MT5 策略测试器拿历史数据跑一遍 VirtualOrder 相关 mqh,确认虚拟转实盘的信号触发和你预期一致再谈。
class="type">class="kw">string inInputsAll = NULL; expert.Add(new CSimpleVolumesStrategy(inInputsAll));