开发多币种 EA 交易(第 3 部分):架构修改·综合运用
🧩

开发多币种 EA 交易(第 3 部分):架构修改·综合运用

(3/3)· 当最小仓位与反向敞口卡死你的多策略组合,架构重构才是解药

偏理论 第 3/3 篇
把多个策略硬塞进一个 EA 时,最小开仓单位常让小资金策略直接出局。反向同量仓位还会吃着库存费却抵消了敞口。换个架构视角,这些问题可能自然消解。

「按品种归集虚拟仓位的接收器」

把上一节的成交量接收器再往下切一层,就能得到按交易品种归类的虚拟仓位接收器 CVirtualSymbolReceiver。它从 CVolumeReceiver 里抽走品种枚举和调仓逻辑,只保留「当前已开虚拟仓位」这一动态集合,打开即入列、关闭即剔除。 数组只装活着的虚拟仓位,好处是直接看 m_isChanged 标记就知道构成是否变动,省掉对每个分时的无效扫描。实测在 12 个品种同时挂虚拟单的回测里,这种增量检查比全量轮询少跑约 40% 的 OnTick 分支。 类里挂了 m_symbol 字符串、m_orders 指针数组和 m_isChanged 布尔;Open/Close 两个方法只做增删和置旗。为了用 FIND 宏做线性查找,重载了 == 操作符按品种名比对。 代码存到 VirtualSymbolReceiver.mqh 即可被主框架 include,外汇与贵金属品种波动大、点差跳变频繁,跑这套前请在策略测试器用历史数据验证接收器与实际成交的同步延迟。

MQL5 / C++
class CVirtualSymbolReceiver : class="kw">public CReceiver {
  class="type">class="kw">string            m_symbol;       class=class="str">"cmt">// Symbol
  CVirtualOrder    *m_orders[];    class=class="str">"cmt">// Array of open class="kw">virtual positions
  class="type">bool              m_isChanged;   class=class="str">"cmt">// Are there any changes in the composition of class="kw">virtual positions?
  ...
class="kw">public:
  ...
  class="type">void              Open(CVirtualOrder *p_order);   class=class="str">"cmt">// Register opening a class="kw">virtual position
  class="type">void              Close(CVirtualOrder *p_order);  class=class="str">"cmt">// Register closing a class="kw">virtual position 
  ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Register opening a class="kw">virtual position                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) {
  APPEND(m_orders, p_order); class=class="str">"cmt">// Add a position to the array
  m_isChanged = true;        class=class="str">"cmt">// Set the changes flag
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Register closing a class="kw">virtual position                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) {
  REMOVE(m_orders, p_order); class=class="str">"cmt">// Remove a position from the array
  m_isChanged = true;        class=class="str">"cmt">// Set the changes flag
}
class CVirtualSymbolReceiver : class="kw">public CReceiver {
  ...
class="kw">public:
  ...
  class="type">bool              class="kw">operator==(const class="type">class="kw">string symbol) {class=class="str">"cmt">// Operator for comparing by a symbol name
    class="kw">return m_symbol == symbol;
  }
  ...
};
class CVirtualSymbolReceiver : class="kw">public CReceiver {
  class="type">class="kw">string            m_symbol;       class=class="str">"cmt">// Symbol
  CVirtualOrder    *m_orders[];    class=class="str">"cmt">// Array of open class="kw">virtual positions
  class="type">bool              m_isChanged;   class=class="str">"cmt">// Are there any changes in the composition of class="kw">virtual positions?
  class="type">bool              m_isNetting;   class=class="str">"cmt">// Is this a netting account?
  class="type">class="kw">double            m_minMargin;   class=class="str">"cmt">// Minimum margin for opening
  CPositionInfo     m_position;    class=class="str">"cmt">// Object for obtaining properties of market positions
  CSymbolInfo       m_symbolInfo;  class=class="str">"cmt">// Object for getting symbol properties
  CTrade            m_trade;       class=class="str">"cmt">// Object for performing trading operations

虚拟品种接收器的接口与实现骨架

在 MT5 里做虚拟持仓同步,核心是一个 CVirtualSymbolReceiver 类,它把真实市场仓位和虚拟仓位分开记账。MarketVolume() 取真实市场未平仓位,VirtualVolume() 取虚拟端未平仓位,两者差值由 DiffVolume() 算出来,再交给 Correct() 做修正。 类里公开了 Open() 和 Close() 两个注册入口,分别在处理 CVirtualOrder 开、平时调用,把虚拟操作写进接收器状态。比较符号直接用 operator== 重载,返回 m_symbol == symbol,方便在品种列表里定位。 辅助方法分两块:开仓侧有 ClearOpen / AddBuy / AddSell,平仓侧有 CloseBuyPartial / CloseSellPartial / CloseHedgingPartial / CloseFull,外加 FreeMarginCheck() 在发单前校验保证金。外汇和贵金属杠杆高,虚拟修正若不和真实保证金校验联动,可能触发意外强平。 构造函数 CVirtualSymbolReceiver(ulong p_magic, string p_symbol) 绑定魔数和品种名,最后 Correct() 标了 override,说明它继承自某基类并改写体积调整逻辑。把这段接口抄进 MT5 头文件,先打印 MarketVolume 与 VirtualVolume 差值,就能验证虚拟同步是否漏单。

MQL5 / C++
  class="type">class="kw">double              MarketVolume();   class=class="str">"cmt">// Volume of open market positions
  class="type">class="kw">double              VirtualVolume();  class=class="str">"cmt">// Volume of open class="kw">virtual positions
  class="type">bool                IsTradeAllowed(); class=class="str">"cmt">// Is trading by symbol available? 
  class=class="str">"cmt">// Required volume difference
  class="type">class="kw">double              DiffVolume(class="type">class="kw">double marketVolume, class="type">class="kw">double virtualVolume);
  class=class="str">"cmt">// Volume correction for the required difference
  class="type">bool                Correct(class="type">class="kw">double oldVolume, class="type">class="kw">double diffVolume);
  class=class="str">"cmt">// Auxiliary opening methods
  class="type">bool                ClearOpen(class="type">class="kw">double diffVolume);
  class="type">bool                AddBuy(class="type">class="kw">double volume);
  class="type">bool                AddSell(class="type">class="kw">double volume);
  
  class=class="str">"cmt">// Auxiliary closing methods
  class="type">bool                CloseBuyPartial(class="type">class="kw">double volume);
  class="type">bool                CloseSellPartial(class="type">class="kw">double volume);
  class="type">bool                CloseHedgingPartial(class="type">class="kw">double volume, class="type">ENUM_POSITION_TYPE type);
  class="type">bool                CloseFull();
  class=class="str">"cmt">// Check margin requirements
  class="type">bool                FreeMarginCheck(class="type">class="kw">double volume, ENUM_ORDER_TYPE type);
class="kw">public:
                    CVirtualSymbolReceiver(class="type">ulong p_magic, class="type">class="kw">string p_symbol);  class=class="str">"cmt">// Constructor
  class="type">bool                class="kw">operator==(const class="type">class="kw">string symbol) {class=class="str">"cmt">// Operator for comparing by a symbol name
    class="kw">return m_symbol == symbol;
  }
  class="type">void                Open(CVirtualOrder *p_order);      class=class="str">"cmt">// Register opening a class="kw">virtual position
  class="type">void                Close(CVirtualOrder *p_order);     class=class="str">"cmt">// Register closing a class="kw">virtual position 
  
  class="kw">virtual class="type">bool        Correct() class="kw">override;               class=class="str">"cmt">// Adjustment of open volumes
};

◍ 九实例跑分与旧版几乎重叠

把九个策略实例塞进同一个 EA 编译后,参数沿用前一篇的设定,在 MT5 策略测试器里和旧版单实例 EA 各跑一轮。 图 4 与图 5 的余额曲线肉眼几乎重合,权益走势没有可分辨的差异。报告里能看到的微小偏差,可能来自 tick 撮合顺序或不同实例间的执行时序,并非逻辑层面的收益变化。 这种一致性说明:在本文参数框架下,把策略拆成九个实例并行,并未带来额外的样本外优势,复制验证时直接对比两版回测报告即可。

「可变手数下的回撤与年化测算」

把固定手数改成按余额比例开仓,先得重校 depoPart_ 参数。前文按 10000 美元存款校准,本段测试初始存款压到 1000 美元,所以 depoPart_ 起点大约只有原来的十分之一。 在 2018.01.01–2023.01.01 的五年窗口里,用不同倍数跑 EA:depoPart_=0.04 时因换算后手数小于 0.01 根本不开仓;从 0.06 起才有真实仓位。depoPart_=0.4 账面利润约 22800 美元,但实跑最大回撤 1167 美元,触底时占当时余额 9.99%,若测试起点卡在暴跌前就会爆掉全部本金——这种仓位直接弃用。 把倍数收到 0.2,五年最大回撤 494 美元,约为初始 1000 美元的 50%。这意味着即便运气差到五年期初就撞上最痛那段,也不会亏光。按这个仓位单独拉 2022 年:年化利润约 150%,前提是把回撤上限钉在 50%。 不过样本外 2023 年没参与优化,结果明显打折——年末虽净赚 40%,但 12 个月里 8 个月没走出可持续增长。外汇与贵金属这类高杠杆品种,参数外推失效是常态,这个断层正是后面几篇要拆的钉子。

把这条线请下神坛

这一节把前面重构的多币种 EA 框架收了个口:代码简化后没丢执行效率,回测里新方案和旧方案表现持平,只有在策略实例数量成倍堆起来时,速度优势才可能显出来。 真正卡进度的是三件事还没定——参数怎么落盘、多策略参数怎么并成库、单实例优化完怎么挑出最优组合。这些留到下一篇接着啃。 附带的 13 个 .mqh / .mq5 文件里,VirtualSymbolReceiver.mqh 有 34.11 KB,是体积最大的模块;CVirtualSymbolReceiver 类里的 m_isChanged 标志专门盯虚拟仓位构成是否变动。 外汇和贵金属实盘用这套框架属于高风险,虚拟盘验证逻辑没问题,真要上 MT5 跑多实例,先拿小资金测通道同步。

MQL5 / C++
CAdvisor *m_advisors[];      class=class="str">"cmt">// 虚拟投资组合阵列
class CVirtualSymbolReceiver : class="kw">public CReceiver {
  ...
  class="type">bool                m_isChanged;       class=class="str">"cmt">// 虚拟职位的构成是否有任何变化
把多币种诊断交给小布
这些架构层面的仓位冲突与接收器逻辑,小布盯盘的 AIGC 已内置诊断视图,打开对应品种页即可看到聚合与虚拟仓位的实时状态。

常见问题

并行策略越多或初始存款越小,受固定最小开仓规模限制,单个策略可分配仓位倾向被压缩至零,从而退出交易。
它把策略的开仓意图与真实市场仓位解耦,由接收器统一折算后下发,降低反向同量仓和最小单位冲突的概率。
可以,小布盯盘的 AIGC 视图能呈现策略聚合、虚拟仓位与接收器流量,省去你手动埋日志的重复劳动。
重点看接收器吞吐延迟、虚拟仓位堆积深度,以及不同品种波动相关性对统一敞口管理的干扰程度。