퐀⣘g·进阶篇
(2/3)·固定手数在余额增长后白白浪费风险预算,这篇把仓位大小变成可伸缩函数
用组对象把多策略归一化缩放
做组合回测时,往往不是单策略跑,而是一组策略或一组子组一起跑。这里引入 CVirtualStrategyGroup,构造时吃进策略指针数组(或组指针数组)再加一个缩放因子,内部统一调 Scale() 把归一化余额按倍数压一遍。 构造函数把传入数组直接 ArrayCopy 到对象属性里,缩放量不是简单乘 p_scale,而是 p_scale / ArraySize(...)。比如传 3 个策略、p_scale=1,每个策略实际按 0.333… 缩放,保证整组合计归一。 CVirtualStrategy 这边要补两个受保护属性:m_fittedBalance 存策略标准化余额,m_fixedLot 存固定手数;构造函数现在必须显式传 fittedBalance,而 fixedLot 默认锁死 0.01 最小手数。CSimpleVolumesStrategy 同步改掉,去掉原设虚拟仓位的参数。 EA 侧加一个重载 Add() 接收组对象,添加完立刻 delete 释放动态内存,组对象对 EA 而言是一次性的。外汇与贵金属组合测试波动剧烈,缩放因子设错可能放大爆仓概率,上 MT5 前先单组打印 m_fittedBalance 核对。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of trading strategies group(s) | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualStrategyGroup { class="kw">protected: class="type">void Scale(class="type">class="kw">double p_scale); class=class="str">"cmt">// Scale normalized balance class="kw">public: CVirtualStrategyGroup(CVirtualStrategy *&p_strategies[], class="type">class="kw">double p_scale = class="num">1); class=class="str">"cmt">// Constructor for a group of strategies CVirtualStrategyGroup(CVirtualStrategyGroup *&p_groups[], class="type">class="kw">double p_scale = class="num">1); class=class="str">"cmt">// Constructor for a group of strategy groups CVirtualStrategy *m_strategies[]; class=class="str">"cmt">// Array of strategies CVirtualStrategyGroup *m_groups[]; class=class="str">"cmt">// Array of strategy groups }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor for strategy groups | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualStrategyGroup::CVirtualStrategyGroup( CVirtualStrategy *&p_strategies[], class="type">class="kw">double p_scale ) { ArrayCopy(m_strategies, p_strategies); Scale(p_scale / ArraySize(m_strategies)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor for a group of strategy groups | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualStrategyGroup::CVirtualStrategyGroup( CVirtualStrategyGroup *&p_groups[], class="type">class="kw">double p_scale ) { ArrayCopy(m_groups, p_groups); Scale(p_scale / ArraySize(m_groups)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Scale normalized balance | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualStrategyGroup::Scale(class="type">class="kw">double p_scale) { FOREACH(m_groups, m_groups[i].Scale(p_scale)); FOREACH(m_strategies, m_strategies[i].Scale(p_scale)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of a trading strategy with class="kw">virtual positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualStrategy : class="kw">public CStrategy { class="kw">protected: ... class="type">class="kw">double m_fittedBalance; class=class="str">"cmt">// Strategy normalized balance class="type">class="kw">double m_fixedLot; class=class="str">"cmt">// Strategy fixed size ... class="kw">public: CVirtualStrategy(class="type">class="kw">double p_fittedBalance = class="num">0, class="type">class="kw">double p_fixedLot = class="num">0.01); class=class="str">"cmt">// Constructor ...
◍ 策略类构造与虚拟订单组的挂载方式
这段实现把成交量策略封装成可缩放的标准化对象:FittedBalance() 直接返回成员 m_fittedBalance,而 Scale() 用除法把归一化余额按 p_scale 压缩,方便多品种间做风险权重对比。 CSimpleVolumesStrategy 的构造函数接收一整套参数——信号周期、偏离值、挂单距离、止损止盈比例、订单过期与最大单数,最后那个带默认值的 p_fittedBalance = 0 会透传给基类 CVirtualStrategy,同时写死 0.01 作为基础手数系数。 CVirtualAdvisor 负责接管虚拟持仓,它的 Add() 方法先遍历策略组里的子组递归挂载并释放内存,再把具体策略指针交给上层 CAdvisor::Add。外汇与贵金属波动剧烈,这类虚拟订单框架只解决回测结构问题,实盘仍需警惕滑点与断连风险。 在 MT5 里把这段粘进 EA 源码,改 p_signalPeriod 从 20 调到 50,可能明显改变信号触发密度,值得你跑一轮历史数据验证。
class="type">class="kw">double FittedBalance() { class=class="str">"cmt">// Strategy normalized balance class="kw">return m_fittedBalance; } class="type">void Scale(class="type">class="kw">double p_scale) { class=class="str">"cmt">// Scale normalized balance m_fittedBalance /= p_scale; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CSimpleVolumesStrategy::CSimpleVolumesStrategy( class="type">class="kw">string p_symbol, ENUM_TIMEFRAMES p_timeframe, class="type">int p_signalPeriod, class="type">class="kw">double p_signalDeviation, class="type">class="kw">double p_signaAddlDeviation, class="type">int p_openDistance, class="type">class="kw">double p_stopLevel, class="type">class="kw">double p_takeLevel, class="type">int p_ordersExpiration, class="type">int p_maxCountOfOrders, class="type">class="kw">double p_fittedBalance = class="num">0) : class=class="str">"cmt">// Initialization list CVirtualStrategy(p_fittedBalance, class="num">0.01), m_symbol(p_symbol), m_timeframe(p_timeframe), m_signalPeriod(p_signalPeriod), m_signalDeviation(p_signalDeviation), m_signaAddlDeviation(p_signaAddlDeviation), m_openDistance(p_openDistance), m_stopLevel(p_stopLevel), m_takeLevel(p_takeLevel), m_ordersExpiration(p_ordersExpiration), m_maxCountOfOrders(p_maxCountOfOrders) { ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders) | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualAdvisor : class="kw">public CAdvisor { ... class="kw">public: ... class="kw">virtual class="type">void Add(CVirtualStrategyGroup &p_group); class=class="str">"cmt">// Method for adding a group of strategies ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Method for adding a group of strategies | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualAdvisor::Add(CVirtualStrategyGroup &p_group) { FOREACH(p_group.m_groups, { CVirtualAdvisor::Add(p_group.m_groups[i]); class="kw">delete p_group.m_groups[i]; }); FOREACH(p_group.m_strategies, CAdvisor::Add(p_group.m_strategies[i])); }
「把虚拟仓位换算成真实手数的静态类」
资金管理不该依赖多个实例各自算一遍,所以这里用纯静态类锁死对象唯一性:构造函数直接 delete,所有字段和方法都挂 static,任何地方都能用 CMoney::Volume() 调。 核心就三个量:s_depoPart 是账户余额里拿去交易的比例,s_fixedBalance 是可手动锁定的总资金基准(设 0 就走实时账户余额),fittedBalance 来自虚拟订单自身。默认 s_depoPart=1.0、s_fixedBalance=0,意味着不干预时按全余额跑。 Volume() 的逻辑很直白:虚拟单的 fittedBalance 为 0 时,真实手数原样返回;否则用 totalBalance(固定值或 AccountInfoDouble(ACCOUNT_BALANCE))乘 s_depoPart 再除 fittedBalance,得到缩放后的真实仓位。外汇和贵金属杠杆高,这套比例算错一层就可能把回撤放大数倍,上线前建议在 MT5 策略测试器里改 s_depoPart 从 0.1 到 1.0 各跑一遍。 代码存成 Money.mqh 丢进当前工程目录,EA 里 include 后直接 CMoney::DepoPart(0.2) 就能把参与资金压到两成。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Basic money management class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMoney { class="kw">static class="type">class="kw">double s_depoPart; class=class="str">"cmt">// Used part of the total balance class="kw">static class="type">class="kw">double s_fixedBalance; class=class="str">"cmt">// Total balance used class="kw">public: CMoney() = class="kw">delete; class=class="str">"cmt">// Disable the constructor class="kw">static class="type">class="kw">double Volume(CVirtualOrder *p_order); class=class="str">"cmt">// Determine the real size of the class="kw">virtual position class="kw">static class="type">void DepoPart(class="type">class="kw">double p_depoPart) { s_depoPart = p_depoPart; } class="kw">static class="type">void FixedBalance(class="type">class="kw">double p_fixedBalance) { s_fixedBalance = p_fixedBalance; } }; class="type">class="kw">double CMoney::s_depoPart = class="num">1.0; class="type">class="kw">double CMoney::s_fixedBalance = class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Determine the real size of the class="kw">virtual position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CMoney::Volume(CVirtualOrder *p_order) { class=class="str">"cmt">// Request the normalized strategy balance for the class="kw">virtual position class="type">class="kw">double fittedBalance = p_order.FittedBalance(); class=class="str">"cmt">// If it is class="num">0, then the real volume is equal to the class="kw">virtual one if(fittedBalance == class="num">0.0) { class="kw">return p_order.Volume(); } class=class="str">"cmt">// Otherwise, find the value of the total balance for trading class="type">class="kw">double totalBalance = s_fixedBalance > class="num">0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE); class=class="str">"cmt">// Return the calculated real volume based on the class="kw">virtual one class="kw">return p_order.Volume() * totalBalance * s_depoPart / fittedBalance ; } class=class="str">"cmt">//+------------------------------------------------------------------+
改 EA 做分组回测
把 SimpleVolumesExpertSingle.mq5 里 EA 初始化函数的策略构造函数参数列表删掉仓位大小参数 fixedLot_,单实例测试就不靠 EA 实时搜参,直接用之前跑出来的组合;EA 仍保留优化入口,必要时可重跑。 主文件 SimpleVolumesExpert.mq5 要动得更深。先建一个枚举 ENUM_VA_GROUP 描述策略实例的分组方式:前三个值各对应 EURGBP、EURUSD、GBPUSD 单一品种的三份副本;第四个 VAG_EURGBPUSD_9 是九个实例全上;第五个 VAG_EURGBPUSD_3_3_3 则是按三个品种各三组做归一化分组。 输入参数扩了三块:分组选择、资金管理(最大回撤预期 10%、固定余额 0 表示用全部、缩放倍数 1.0)、以及 magic 号 27183。OnInit 里按 10% 最大回撤做资金归一,建九份策略副本并按所选分组排好加进虚拟顾问。这套代码主要演示类能力,不是终版,外汇与贵金属品种波动大、回撤失控风险高,参数请先在策略测试器验证。 别把删参当终稿 Single 文件删 fixedLot_ 只是为隔离变量,主文件里资金管理类接管了仓位,真上 MT5 前要把 expectedDrawdown_ 和 scale_ 按自己账户容错重设。
class="type">int OnInit() { class=class="str">"cmt">// Create an EA handling class="kw">virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumesSingle"); 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); } enum ENUM_VA_GROUP { VAG_EURGBP, class=class="str">"cmt">// Only EURGBP(class="num">3 items) VAG_EURUSD, class=class="str">"cmt">// Only EURUSD(class="num">3 items) VAG_GBPUSD, class=class="str">"cmt">// Only GBPUSD(class="num">3 items) VAG_EURGBPUSD_9, class=class="str">"cmt">// EUR-GBP-USD(class="num">9 items) VAG_EURGBPUSD_3_3_3 class=class="str">"cmt">// EUR-GBP-USD(class="num">3+class="num">3+class="num">3 items) }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input group "::: Strategy groups" class="kw">input ENUM_VA_GROUP group_ = VAG_EURGBP; class=class="str">"cmt">// - Strategy group class="kw">input group "::: Money management" class="kw">input class="type">class="kw">double expectedDrawdown_ = class="num">10; class=class="str">"cmt">// - Maximum risk(%) class="kw">input class="type">class="kw">double fixedBalance_ = class="num">0; class=class="str">"cmt">// - Used deposit(class="num">0 - use all) in the account currency class="kw">input class="type">class="kw">double scale_ = class="num">1.0; class=class="str">"cmt">// - Group scaling multiplier class="kw">input group "::: Other parameters" class="kw">input class="type">ulong magic_ = class="num">27183; class=class="str">"cmt">// - Magic class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Set parameters in the money management class CMoney::DepoPart(expectedDrawdown_ / class="num">10.0); CMoney::FixedBalance(fixedBalance_); class=class="str">"cmt">// Create an EA handling class="kw">virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes_" + EnumToString(group_)); class=class="str">"cmt">// Create and fill the array of all strategy instances CVirtualStrategy *strategies[] = {
◍ 按符号分组的多周期成交量策略装配
上面这段初始化把 9 个 CSimpleVolumesStrategy 实例塞进数组,覆盖 EURGBP、EURUSD、GBPUSD 三个品种,全部跑在 H1 周期。每个实例的第 3 个参数(如 13、17、51、128)是成交量窗口长度,第 4、5 个浮点(如 0.3/1.0、1.7/0.5)大概率是阈值与仓位系数,后面几个整型则控制止损、止盈与魔术码一类属性。 紧接着按符号把指针拆成 strategiesEG / EU / GU 三组,每组 3 条策略。group_ 开关决定 EA 只加载哪一组,其余两组的指针在对应分支里直接 delete 掉,避免无谓的内存占用。 实盘里你可以照这个结构改 group_ 枚举,比如临时切到 VAG_GBPUSD 只跑 GU 那三条(窗口 80/128/13),看 H1 上英镑波动是否被成交量阈值过滤得更干净。外汇与贵金属杠杆高,这类多策略组合回测漂亮不代表实盘概率占优,上手前请在 MT5 策略测试器用 2020—2024 年数据先跑一遍。
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1, class="num">13, class="num">0.3, class="num">1.0, class="num">0, class="num">10500, class="num">465, class="num">1000, class="num">3, class="num">1600), new CSimpleVolumesStrategy("EURGBP", PERIOD_H1, class="num">17, class="num">1.7, class="num">0.5, class="num">0, class="num">16500, class="num">220, class="num">1000, class="num">3, class="num">900), new CSimpleVolumesStrategy("EURGBP", PERIOD_H1, class="num">51, class="num">0.5, class="num">1.1, class="num">0, class="num">19500, class="num">370, class="num">22000, class="num">3, class="num">1600), new CSimpleVolumesStrategy("EURUSD", PERIOD_H1, class="num">24, class="num">0.1, class="num">0.3, class="num">0, class="num">7500, class="num">2400, class="num">24000, class="num">3, class="num">2300), new CSimpleVolumesStrategy("EURUSD", PERIOD_H1, class="num">18, class="num">0.2, class="num">0.4, class="num">0, class="num">19500, class="num">1480, class="num">6000, class="num">3, class="num">2000), new CSimpleVolumesStrategy("EURUSD", PERIOD_H1, class="num">128, class="num">0.7, class="num">0.3, class="num">0, class="num">3000, class="num">170, class="num">42000, class="num">3, class="num">2200), new CSimpleVolumesStrategy("GBPUSD", PERIOD_H1, class="num">80, class="num">1.1, class="num">0.2, class="num">0, class="num">6000, class="num">1190, class="num">1000, class="num">3, class="num">2500), new CSimpleVolumesStrategy("GBPUSD", PERIOD_H1, class="num">128, class="num">2.0, class="num">0.9, class="num">0, class="num">2000, class="num">1170, class="num">1000, class="num">3, class="num">900), new CSimpleVolumesStrategy("GBPUSD", PERIOD_H1, class="num">13, class="num">1.5, class="num">0.8, class="num">0, class="num">2500, class="num">1375, class="num">1000, class="num">3, class="num">1400), }; class=class="str">"cmt">// Create arrays of pointers to strategies, one symbol at a time, from the available strategies CVirtualStrategy *strategiesEG[] = {strategies[class="num">0], strategies[class="num">1], strategies[class="num">2]}; CVirtualStrategy *strategiesEU[] = {strategies[class="num">3], strategies[class="num">4], strategies[class="num">5]}; CVirtualStrategy *strategiesGU[] = {strategies[class="num">6], strategies[class="num">7], strategies[class="num">8]}; class=class="str">"cmt">// Create and add selected groups of strategies to the EA class="kw">switch(group_) { case VAG_EURGBP: { expert.Add(CVirtualStrategyGroup(strategiesEG, scale_)); FOREACH(strategiesEU, class="kw">delete strategiesEU[i]); FOREACH(strategiesGU, class="kw">delete strategiesGU[i]); break; } case VAG_EURUSD: { expert.Add(CVirtualStrategyGroup(strategiesEU, scale_));