퐀⣘g·进阶篇
⚖️

퐀⣘g·进阶篇

(2/3)·固定手数在余额增长后白白浪费风险预算,这篇把仓位大小变成可伸缩函数

案例拆解 第 2/3 篇
很多多币种EA跑出利润后依旧用启动时的固定手数,账户浮盈却没转化为更大的合理暴露。把仓位锁死在初始参数上,等于主动放弃复利结构里的风险预算。让头寸跟着总余额走,才是多实例协作该有的样子。

用组对象把多策略归一化缩放

做组合回测时,往往不是单策略跑,而是一组策略或一组子组一起跑。这里引入 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 核对。

MQL5 / C++
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,可能明显改变信号触发密度,值得你跑一轮历史数据验证。

MQL5 / C++
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) 就能把参与资金压到两成。

MQL5 / C++
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_ 按自己账户容错重设。

MQL5 / C++
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 年数据先跑一遍。

MQL5 / C++
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_));
交给小布盯盘看多实例暴露
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到各策略实例的归一化余额占用与回撤比,你只管调参数。

常见问题

没有硬数学约束,10%只是便于心算且心理可接受的规范参数,可换成1%、5%或50%,本质只是归一化标尺。
不是。固定策略大小是用于计算未平仓位规模的基准值,固定手数常指直接写死的下单量,前者更适配系列化加仓计算。
不会,接上篇的状态恢复机制已保证EA重启后继续处理已有仓位,仓位计算参数也存在实例配置里。
可以,在品种页的AIGC诊断里填入固定策略大小与测试区间,小布会按最大净值回撤反推拟合余额,省去手动乘10。
用当前总余额除以标准化策略余额再乘固定策略大小,即可在维持约10%相对回撤下得到新仓位规模。