开发多币种 EA 交易(第 6 部分):自动选择实例组·进阶篇
⚙️

开发多币种 EA 交易(第 6 部分):自动选择实例组·进阶篇

(2/3)·手动筛2到5万组参数凑10到20个最佳组合,三级以上分组就累到不想动

偏理论进阶 第 2/3 篇
手工从数万组优化结果里挑十几组能协同发力的实例,做到三级分组基本就是体力极限。先别急着写复杂程序,用最简脚本跑一遍自动选组,和手挑基线比一比,才知道自动化值不值得继续投。

「EURGBP H1 上的多参数策略阵列」

下面这段初始化代码在 EURGBP 的 H1 周期上一次性挂了 12 个 CSimpleVolumesStrategy 实例,每个实例用不同参数组合跑量价逻辑。外汇与贵金属杠杆高,这种多实例并行只是参数暴露,不表示任何盈利担保,实盘前务必在 MT5 策略测试器逐组回测。 从参数排布看,第 3 个参数(如 38、98、44…)跨度从 12 到 230,第 4 个系数在 0.1 到 1.8 之间摆动,说明策略对成交量的敏感阈值被刻意打散。第 7 个参数(止损相关点数)最低 230、最高 1940,对应第 9 个参数(如 60000、7000)的资金或周期上限差异极大,短线止损与长线缓冲混在同一个 expert 里。 最后三行把数组包进 CVirtualStrategyGroup 再交给 expert.Add,并返回 INIT_SUCCEEDED。你直接把这段贴进 EA 的 OnInit 末端,改 scale_ 就能控制这组策略的整体仓位缩放,建议先设小值观察 MT5 日志里的下单请求是否按预期触发。

MQL5 / C++
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">38, class="num">1.4, class="num">0.1, class="num">0, class="num">19600, class="num">690, class="num">60000, class="num">3, class="num">1000),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">98, class="num">0.9, class="num">1, class="num">0, class="num">15600, class="num">1850, class="num">7000, class="num">3, class="num">1300),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">44, class="num">1.8, class="num">1.9, class="num">0, class="num">13000, class="num">675, class="num">45000, class="num">3, class="num">600),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">86, class="num">1, class="num">1.7, class="num">0, class="num">17600, class="num">1940, class="num">56000, class="num">3, class="num">1000),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">230, class="num">0.7, class="num">1.2, class="num">0, class="num">8800, class="num">1850, class="num">2000, class="num">3, class="num">1200),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">44, class="num">0.1, class="num">0.6, class="num">0, class="num">10800, class="num">230, class="num">8000, class="num">3, class="num">1200),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">108, class="num">0.6, class="num">0.9, class="num">0, class="num">12000, class="num">1080, class="num">46000, class="num">3, class="num">800),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">182, class="num">1.8, class="num">1.9, class="num">0, class="num">13000, class="num">675, class="num">33000, class="num">3, class="num">600),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">62, class="num">0.1, class="num">1.5, class="num">0, class="num">16800, class="num">255, class="num">2000, class="num">3, class="num">800),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">12, class="num">1.4, class="num">1.7, class="num">0, class="num">9600, class="num">440, class="num">59000, class="num">3, class="num">700),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">24, class="num">1.7, class="num">2, class="num">0, class="num">11600, class="num">1930, class="num">23000, class="num">3, class="num">700),
new CSimpleVolumesStrategy("EURGBP", PERIOD_H1,  class="num">30, class="num">1.1, class="num">0.1, class="num">0, class="num">18400, class="num">1295, class="num">27000, class="num">3, class="num">1500),
};
class=class="str">"cmt">// Add a group of selected strategies to the strategies
expert.Add(CVirtualStrategyGroup(strategies, scale_));
class="kw">return(INIT_SUCCEEDED);
}

◍ 用单字符串构造策略参数

参数组合当前以 CSV 形式落地,读取进 MT5 时拿到的是 string 类型。比起逐个传参,让构造函数只吃一个逗号分隔的字符串、内部自行拆解,更利于后续接 Input_Struct 之类的库。 下面这个第二个构造函数假定 11 个字段按固定次序排在同一行里:品种、周期、信号周期、信号偏差、附加偏差、挂单距离、止损比例、止盈比例、订单过期秒数、最大订单数、拟合余额。StringSplit 以逗号切分后,若 total 不等于 11 就直接跳过初始化,避免脏数据灌进类属性。 外汇与贵金属波动剧烈、杠杆风险高,这类参数化策略在实盘前务必用策略测试器跑多周期回测,验证字段映射无误再上真仓。 代码里 iVolumes 取的是 VOLUME_TICK 即时成交量,ArrayResize 按 m_signalPeriod 定长、ArraySetAsSeries 设成时间序列倒序,这几步漏掉会导致后续信号数组越界或取值错位。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading strategy using tick volumes                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : class="kw">public CVirtualStrategy {
  ...
class="kw">public:
  CSimpleVolumesStrategy(const class="type">class="kw">string &p_params);
  ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(const class="type">class="kw">string &p_params) {
  class="type">class="kw">string param[];
  class="type">int total = StringSplit(p_params, &class="macro">#x27;,&class="macro">#x27;, param);
  if(total == class="num">11) {
    m_symbol = param[class="num">0];
    m_timeframe = (ENUM_TIMEFRAMES) StringToInteger(param[class="num">1]);
    m_signalPeriod = (class="type">int) StringToInteger(param[class="num">2]);
    m_signalDeviation = StringToDouble(param[class="num">3]);
    m_signaAddlDeviation = StringToDouble(param[class="num">4]);
    m_openDistance = (class="type">int) StringToInteger(param[class="num">5]);
    m_stopLevel = StringToDouble(param[class="num">6]);
    m_takeLevel = StringToDouble(param[class="num">7]);
    m_ordersExpiration = (class="type">int) StringToInteger(param[class="num">8]);
    m_maxCountOfOrders = (class="type">int) StringToInteger(param[class="num">9]);
    m_fittedBalance = StringToDouble(param[class="num">10]);
    CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);
class=class="str">"cmt">// Load the indicator to get tick volumes
    m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);
class=class="str">"cmt">// Set the size of the tick volume receiving array and the required addressing
    ArrayResize(m_volumes, m_signalPeriod);
    ArraySetAsSeries(m_volumes, true);
  }
}

把 CSV 策略集塞进优化代理

在 SimpleVolumesExpert.mq5 基础上扩展 OptGroupExpert.mq5,核心目标是让手动筛选出的多个策略实例能直接进测试器做组合优化。同时运行的策略数上限锁死在 8 个,count_ 参数设小于 8 时,枚举只认前 count_ 个索引输入,多余 iN_ 不参与通道。 本地图表能读终端数据目录下的 Params_SV_EURGBP_H1.csv,但测试器用的是自己的沙盒目录,文件根本扫不到;更麻烦的是跑云网络或局域网代理集群优化时,数据文件不会自动分发。tester_file 预处理器指令能在编译期绑定一个常量文件名随代理下发,却不支持运行时用输入参数换文件名——这是硬限制。 变通做法是拿 __FILE__ 拼出固定名 PARAMS_FILE(例如 EA 源码路径加 .params.csv 后缀),用 #property tester_file 把它编进包里。LoadParams() 先查 FILE_COMMON 共享夹、再查本地数据夹,命中就用 FileCopy 覆写到 PARAMS_FILE,远程代理收到主实例发来的固定名文件后,在 OnTesterInit() 里调同一函数即可落盘。 OnTesterInit() 顺手把参数集索引范围设好,免得每次在优化窗口手填;count_ 不足 8 时顺手禁掉多余索引枚举。优化标准用 OnTester() 算「回撤锁 10% 初始固定余额下的最大利润」,相当于自动给每次跑出来的组合打一个风险缩放后的收益分。 OnInit() 先载参数集、抓 i0_~i7_ 做重复校验,有重号直接不启动该次 pass;通过后按索引从 params[] 抽字符串集灌进 EA。编译器强制要求配了 OnTesterInit() 的 EA 必须有 OnTesterDeinit() 空处理函数,少一个就报编不过。外汇与贵金属杠杆品种波动剧烈,这种组合优化仅降低过拟合概率,实盘仍属高风险。

MQL5 / C++
input group "::: Selection for the group"
sinput class="type">class="kw">string fileName_ = "Params_SV_EURGBP_H1.csv";  class=class="str">"cmt">// File with strategy parameters(*.csv)
sinput class="type">int   count_ = class="num">8;    class=class="str">"cmt">// Number of strategies in the group(class="num">1 .. class="num">8)
input class="type">int    i0_ = class="num">0;       class=class="str">"cmt">// Strategy index #class="num">1
input class="type">int    i1_ = class="num">1;       class=class="str">"cmt">// Strategy index #class="num">2
input class="type">int    i2_ = class="num">2;       class=class="str">"cmt">// Strategy index #class="num">3
input class="type">int    i3_ = class="num">3;       class=class="str">"cmt">// Strategy index #class="num">4
input class="type">int    i4_ = class="num">4;       class=class="str">"cmt">// Strategy index #class="num">5
input class="type">int    i5_ = class="num">5;       class=class="str">"cmt">// Strategy index #class="num">6
input class="type">int    i6_ = class="num">6;       class=class="str">"cmt">// Strategy index #class="num">7
input class="type">int    i7_ = class="num">7;       class=class="str">"cmt">// Strategy index #class="num">8
class="macro">#define PARAMS_FILE __FILE__".params.csv"
class="macro">#class="kw">property tester_file PARAMS_FILE
class="type">class="kw">string params[];            class=class="str">"cmt">// Array of strategy parameter sets as strings
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load strategy parameter sets from a CSV file                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int LoadParams(const class="type">class="kw">string fileName, class="type">class="kw">string &p_params[]) {
   class="type">bool res = false;
   class=class="str">"cmt">// Check if the file exists in the shared folder and in the data folder
   if(FileIsExist(fileName, FILE_COMMON)) {
      class=class="str">"cmt">// If it is in the shared folder, then copy it to the data folder with a fixed name
      res = FileCopy(fileName, FILE_COMMON, PARAMS_FILE, FILE_REWRITE);
   } else if(FileIsExist(fileName)) {
      class=class="str">"cmt">// If it is in the data folder, then copy it here, but with a fixed name
      res = FileCopy(fileName, class="num">0, PARAMS_FILE, FILE_REWRITE);
   }
   class=class="str">"cmt">// If there is a file with a fixed name, that is good as well

「从 CSV 灌入参数集并初始化优化」

EA 在优化前先把外部 CSV 里的策略参数集读进内存:先判文件是否存在,存在才以只读文本方式打开,跳过首行表头后逐行抽取第 10~21 列的子串,追加进 params 数组,最终返回数组长度作为 totalParams。 若 totalParams 为 0,OnTesterInit 直接返回 INIT_FAILED 并在日志报错,提示检查文件是否在 data 或 common data 目录——这一步卡死能保证后续不会空跑。 scale_ 被强制设为不可变、范围 1~1、步长 2,相当于单值锁定;而 i0_~i7_ 这 8 个索引参数里,前 count_ 个开启枚举(范围 0 到 totalParams-1、步长 1),多余的则禁用,避免无效组合拖慢 MT5 优化器。 实盘前建议在 MT5 策略测试器里先故意删掉 CSV 跑一次,确认能稳定触发 INIT_FAILED 而非静默吞错;外汇与贵金属品种波动剧烈,参数集回测表现仅代表历史概率,不代表未来收益。

MQL5 / C++
if(FileIsExist(PARAMS_FILE)) {
    res = true;
}
class=class="str">"cmt">// If the file is found, then
if(res) {
    class=class="str">"cmt">// Open it
    class="type">int f = FileOpen(PARAMS_FILE, FILE_READ | FILE_TXT | FILE_ANSI);
    class=class="str">"cmt">// If opened successfully
    if(f != INVALID_HANDLE) {
        FileReadString(f); class=class="str">"cmt">// Ignore data column headers
        class=class="str">"cmt">// For all further file strings
        while(!FileIsEnding(f)) {
            class=class="str">"cmt">// Read the class="type">class="kw">string and extract the part containing the strategy inputs
            class="type">class="kw">string s = CSVStringGet(FileReadString(f), class="num">10, class="num">21);
            class=class="str">"cmt">// Add this part to the array of strategy parameter sets
            APPEND(p_params, s);
        }
        FileClose(f);
        class="kw">return ArraySize(p_params);
    }
}
class="kw">return class="num">0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialization before optimization                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnTesterInit(class="type">void) {
    class=class="str">"cmt">// Load strategy parameter sets
    class="type">int totalParams = LoadParams(fileName_, params);
    class=class="str">"cmt">// If nothing is loaded, report an error
    if(totalParams == class="num">0) {
        PrintFormat(__FUNCTION__" | ERROR: Can&class="macro">#x27;t load data from file %s.\n"
                              "Check that it exists in data folder or in common data folder.", fileName_);
        class="kw">return(INIT_FAILED);
    }
    class=class="str">"cmt">// Set scale_ to class="num">1
    ParameterSetRange("scale_", false, class="num">1, class="num">1, class="num">1, class="num">2);
    class=class="str">"cmt">// Set the ranges of change for the parameters of the set index iteration
    for(class="type">int i = class="num">0; i < class="num">8; i++) {
        if(i < count_) {
            ParameterSetRange("i" + (class="type">class="kw">string) i + "_", true, class="num">0, class="num">0, class="num">1, totalParams - class="num">1);
        } else {
            class=class="str">"cmt">// Disable the enumeration for extra indices
            ParameterSetRange("i" + (class="type">class="kw">string) i + "_", false, class="num">0, class="num">0, class="num">1, totalParams - class="num">1);
        }
    }
    class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Test results                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 回测里把回撤折算成仓位系数

在 MT5 策略测试器的 OnTester 里,可以用 STAT_EQUITY_DD 拿到最大权益回撤绝对值,再用 STAT_PROFIT 取净利润。把固定基准余额 fixedBalance_ 的 10% 除以回撤,得到 coeff,净利润乘上它就等于「若把回撤控制在 10% 基准内可放大的拟合利润」。这套算法只改评价函数,不改策略信号,外汇与贵金属品种回测仍属高风险,放大系数高只代表历史样本倾向,不等于实盘概率。 OnInit 这一段负责把外部参数集索引装配进虚拟策略组。先用 LoadParams 读文件,一条都没读出来就返回 INIT_PARAMETERS_INCORRECT;再把 i0_ 到 i7_ 拼成逗号串、Split 成数组、Resize 到 count_,用哈希集去重。count_ 不在 1~8 或有重复索引直接报错,保证最多 8 个实例且互不重复。 之后 CMoney::DepoPart(expectedDrawdown_/10.0) 把仓位风险线设为预期回撤的十分之一,FixedBalance 接死基准余额;new CVirtualAdvisor 建虚拟持仓壳,FOREACH 把选中索引对应的 CSimpleVolumesStrategy 实例塞进数组,最后 Add 进 EA。开 MT5 把这段贴进 EA 模板,改 fileName_ 和 i0_~i7_ 就能跑多实例参数组优化。

MQL5 / C++
class="type">class="kw">double OnTester(class="type">void) {
  class=class="str">"cmt">// Maximum absolute drawdown
  class="type">class="kw">double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);
  class=class="str">"cmt">// Profit
  class="type">class="kw">double profit = TesterStatistics(STAT_PROFIT);
  class=class="str">"cmt">// The ratio of possible increase in position sizes for the drawdown of class="num">10% of fixedBalance_
  class="type">class="kw">double coeff = fixedBalance_ * class="num">0.1 / balanceDrawdown;
  class=class="str">"cmt">// Recalculate the profit
  class="type">class="kw">double fittedProfit = profit * coeff;
  class="kw">return fittedProfit;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
  class=class="str">"cmt">// Load strategy parameter sets
  class="type">int totalParams = LoadParams(fileName_, params);
  class=class="str">"cmt">// If nothing is loaded, report an error 
  if(totalParams == class="num">0) {
    PrintFormat(__FUNCTION__" | ERROR: Can&class="macro">#x27;t load data from file %s.\n"
                "Check that it exists in data folder or in common data folder.", fileName_);
    class="kw">return(INIT_PARAMETERS_INCORRECT);
  }
  class=class="str">"cmt">// Form the class="type">class="kw">string from the parameter set indices separated by commas
  class="type">class="kw">string strIndexes = (class="type">class="kw">string) i0_ + "," + (class="type">class="kw">string) i1_ + "," +
                      (class="type">class="kw">string) i2_ + "," + (class="type">class="kw">string) i3_ + "," +
                      (class="type">class="kw">string) i4_ + "," + (class="type">class="kw">string) i5_ + "," +
                      (class="type">class="kw">string) i6_ + "," + (class="type">class="kw">string) i7_;
  class=class="str">"cmt">// Turn the class="type">class="kw">string into the array
  class="type">class="kw">string indexes[];
  StringSplit(strIndexes, &class="macro">#x27;,&class="macro">#x27;, indexes);
  class=class="str">"cmt">// Leave only the specified number of instances in it
  ArrayResize(indexes, count_);
  class=class="str">"cmt">// Multiplicity for parameter set indices
  CHashSet<class="type">class="kw">string> setIndexes;
  class=class="str">"cmt">// Add all indices to the multiplicity
  FOREACH(indexes, setIndexes.Add(indexes[i]));
  class=class="str">"cmt">// Report an error if
  if(count_ < class="num">1 || count_ > class="num">8           class=class="str">"cmt">// number of instances not in the range class="num">1 .. class="num">8
     || setIndexes.Count() != count_     class=class="str">"cmt">// not all indexes are unique
     ) {
    class="kw">return INIT_PARAMETERS_INCORRECT;
  }
  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_OptGroup");
  class=class="str">"cmt">// Create and fill the array of all strategy instances
  CVirtualStrategy *strategies[];
  FOREACH(indexes, APPEND(strategies, new CSimpleVolumesStrategy(params[StringToInteger(indexes[i])])));
  class=class="str">"cmt">// Create and add selected groups of strategies to the EA
  expert.Add(CVirtualStrategyGroup(strategies, scale_));
  class="kw">return(INIT_SUCCEEDED);
}
把重复劳动交给小布
这些多品种多周期优化结果的归一化与分组诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到候选实例组的回撤分布,你只管拍板。

常见问题

不同品种与周期的价格行为差异大,统一优化容易掩盖单实例的适配性;分开优化后再按归一化回撤挑组,才可能在协同时保持稳健,概率上更优。
目前小布内置的是诊断视图,展示各品种页的候选参数组与回撤区间,自动合并高阶层级仍需在MQL5侧写脚本;但重复的比对劳动可交给小布看盘口。
它是不参与优化的派生值,用来在统一尺度上比较不同实例的归一化表现,是后续按最大回撤率合并组的基础字段。
当最简脚本选出的组在同等或更低回撤下权益曲线不弱于手挑组,就说明值得投入更完整的实现;外汇贵金属高风险,结论仅代表概率倾向。