在 MQL5 中构建自优化智能交易系统(第八部分):多策略分析(基础篇)
📘

在 MQL5 中构建自优化智能交易系统(第八部分):多策略分析(基础篇)

第 1/3 篇

「为什么把价格预测换成指标预测」

做算法交易的人大多踩过同一个坑:直接拿模型去猜下一根 K 线的收盘价,胜率常年趴窝。本系列前面的实测表明,同一个统计工具,只要把任务从「预测价格」改成「预测技术指标的后续值」,模型表现会稳定占优。 原因不复杂——价格是混沌系统的表层输出,噪声占比极高;而均线、RSI 这类指标本身就是对价格的平滑与压缩,序列自相关性更强,统计模型更容易抓到结构。换问题框架不等于换引擎,工具没变,只是靶子更好打。 这一篇要验证一个具体设想:把三种独立策略(均线交叉延续、RSI 动量、威廉 %R 趋势突破)塞进一个应用,让它定期只挑当前最赚钱的那一种来跑,而不是三策略并行或固定加权。外汇与贵金属市场高杠杆、滑点随机,这类切换逻辑能否真跑出比「全跟」更好的曲线,需要用 MT5 的前向测试来证伪。 基准必须先立。我们拿三策略原始组合当标杆绩效,统计模型只有稳定超越它才有存在价值。MT5 自带的快速 / 慢速遗传优化器配合前向测试,能滚出新的参数集反复验,比单纯回测更能暴露过拟合。

◍ 用类封装把均线交叉策略拆干净

把多个策略硬塞进一个 EA 的弊端很明显:逻辑耦合后,任何一处改动都可能引发意料之外的副作用。更稳的做法是模仿人工搭配——一个类管开仓、另一个类管平仓,各自只解决一小块问题。在 MT5 里落地,就是给所有策略抽一个 Parent 基类,通用功能(参数更新、买卖信号检查)放基类,子类用 virtual 重写信号判定。 第一个落地类是 OpenCloseMACrossover:两条同周期 MA,分别挂开盘价和收盘价数据源。规则很直白——收盘 MA 在开盘 MA 之上,倾向多头;反之倾向空头。因为平均收盘价高于平均开盘价时,价格行为偏看涨,趋势延续概率更高。外汇与贵金属波动大,这种基础信号只作概率参考,不构成方向保证。 验证不能省。我们把类模块和硬编码版本在相同 D1 区间回测对比:类版本完成 127 笔交易,多空比与硬编码版一致,夏普比率高度接近,资金曲线几乎重合。这说明封装没引入偏差,后续可放心寻优。 寻优阶段把系统常量改成输入参数,用 MT5 策略测试器的「快速遗传算法」跑 Forward=1/2(前半训练、后半验证)。优化器只看得见训练段,散点图每个点是一次迭代;三维条形图显示更高时间周期夏普倾向更好。前向结果与回测相似,是稳定性尚可的信号,若两者最佳参数脱节则策略可能过拟合。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  Strategy.mqh |
class=class="str">"cmt">//|                                                       Gamuchirai Ndawana |
class=class="str">"cmt">//|                                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class Strategy
  {
class="kw">private:
                     class="type">int   buffer_size;
                     class="type">bool buy;

策略基类的信号接口与初始化缓冲

在 MT5 里搭一套可扩展的交易策略框架,第一步往往是写一个父类把信号接口先占住。下面这段把 buy / sell 两个布尔标志、默认指标缓冲大小,以及买卖信号虚函数都封进一个 Strategy 类,子类之后只需重写 BuySignal 与 SellSignal。 构造函数里把 buy、sell 初始化为 false,buffer_size 给到 10——意思是默认预留 10 根 K 线的指标缓冲。GetIndicatorBufferSize 直接返回这个整型值,方便后续数组申请时对齐长度。 BuySignal 的父类实现是个安全闸:若子类忘了重写,它只会 Print 一条警告并 return false,避免裸跑出无意义的真信号。外汇与贵金属波动剧烈、杠杆高风险,这种‘默认不交易’的fail-safe设计能少踩很多坑。 把这段代码贴进 MT5 的 Include 目录编译,自己建个子类重写那两个虚函数,就能在策略测试器里验证信号触发逻辑是否如预期。

MQL5 / C++
class="type">bool sell;
class="kw">public:
  class=class="str">"cmt">//--- Class constructors and destructors
  Strategy(class="type">void);
  ~Strategy(class="type">void);

  class=class="str">"cmt">//--- Check if we have any valid trading signals from our strategy
  class="kw">virtual class="type">bool BuySignal(class="type">void);
  class="kw">virtual class="type">bool SellSignal(class="type">void);

  class=class="str">"cmt">//--- Update the technical indicators in our strategy
  class="kw">virtual class="type">bool Update(class="type">void);

  class=class="str">"cmt">//--- Get the size of the technical indicator buffers
  class="type">int  GetIndicatorBufferSize(class="type">void);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The only way to create an object of the class                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
Strategy::Strategy(class="type">void)
  {
    class=class="str">"cmt">//--- Upon initialization, both flags should be false 
    buy = false;
    sell = false;
    buffer_size = class="num">10;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The size of our indicator buffer                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int Strategy::GetIndicatorBufferSize(class="type">void)
  {
    class="type">int res = buffer_size;
    class="kw">return(res);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if our strategy is giving us any buy signals               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Strategy::BuySignal(class="type">void)
  {
    class=class="str">"cmt">//--- The user is intended to overwrite the function in the child class
    class=class="str">"cmt">//--- Otherwise, failing to do so will always class="kw">return false as a safety feature
    Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
    class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if our strategy is giving us any sell signals              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Strategy::SellSignal(class="type">void)
  {

「父类留的空方法和子类怎么接」

策略基类里把 Update() 写成虚函数并留了默认实现:如果用户没在子类重写,它就直接 Print 一条警告,提示“此函数仅在父类实现,请修正子类”,然后 return(false)。这是一道安全闸,避免你忘了重写却误以为策略在跑。 看下面这段父类默认代码,逻辑很直白:先打警告带 __FUNCSIG__ 函数签名,再返回 false 阻断后续。析构函数当前是空的,暂时不用管资源释放。

MQL5 / C++
class=class="str">"cmt">//--- The user is intended to overwrite the function in the child class
class=class="str">"cmt">//--- Otherwise, failing to do so will always class="kw">return false as a safety feature
Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
class="kw">return(false);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our strategy parameters                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Strategy::Update(class="type">void)
  {
  class=class="str">"cmt">//--- The user is intended to overwrite the function in the child class
  Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The class destructor is currently empty                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
Strategy::~Strategy(class="type">void)
  {
  }
真正落地时,子类 OpenCloseMACrossover 公开声明了 virtual bool Update(void),必须在 .mqh 里给出自己的版本,否则 MT5 回测会一直刷那条 WARNING 且信号恒为 false。外汇与贵金属波动大、杠杆高,这类框架错误若没及时发现,可能让你在实盘误判信号缺失风险。 子类头文件里已经挂了两个 MA 指针数组 ma_array[2],构造函数接收 symbol、周期、shift 和 ma_mode——你开 MT5 把 include 路径配好,就能照着这个骨架把 Update 里填进双均线交叉判断。

MQL5 / C++
class=class="str">"cmt">//--- The user is intended to overwrite the function in the child class
class=class="str">"cmt">//--- Otherwise, failing to do so will always class="kw">return false as a safety feature
Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our strategy parameters                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Strategy::Update(class="type">void)
  {
  class=class="str">"cmt">//--- The user is intended to overwrite the function in the child class
  Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The class destructor is currently empty                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
Strategy::~Strategy(class="type">void)
  {
  }

常见问题

直接预测价格噪声大、过拟合严重;改预测均线或MACD等指标值,信号更平滑,回测衰减更慢。把价格预测换成指标预测能降低策略对随机波动的敏感。
把快慢线参数、缓冲、信号判断收进独立类,主程序只调接口。后续加新策略不用改老代码,排查信号错误也只需看对应类。
可以。小布能读取你的策略输出,标出不同策略在同一根K线反向发信号的情况,并给出仓位折中建议,省去你手动比对。
常见是子类没在Init里调用父类分配缓冲,或指标句柄未校验有效性就取数。先确认Handle不等于INVALID_HANDLE再绑定缓冲区。
若父类定义为纯虚或强制调用,不写子类无法实例化;若只是空实现供覆盖,空着就用父类默认逻辑,但信号可能一直为空。