MetaTrader 5的策略测试器功能强大,但其内置的遗传算法(GA)本质上是一种启发式搜索——算是一种高级猜测。多年来,我一直接受GA给出的"最佳"参数,结果却看着EA在实盘测试中持续亏损。问题不在EA的逻辑,而在优化框架本身:我们优化的是过去的表现,而非未来的稳健性。
本文不是教你怎么用优化器,而是探讨如何替换优化引擎的核心逻辑,引入更复杂的方法:贝叶斯优化。我们会深入代码、数学原理,以及推进分析中那些残酷的现实。
暴力搜索与遗传算法的问题
标准的MT5优化器,无论是穷举还是GA,都遵循"适应度"原则。它运行一次模拟,得出一个结果(如盈利因子或夏普比率),然后试图找到能最大化该结果的输入参数组合。
过度拟合的陷阱: GA的目标是在大海捞针。问题在于,这片"海"充满了噪声。通过调整参数来拟合过去5年数据中的每一个微小波动,你创建的模型会成为"历史学大师",但对未来却一窍不通。
局部极小值问题: GA可能陷入局部最优解。它们会收敛到一个"不错"的解,但未必是最好的,更不用说是最稳健的那个。
参考来源: Robert Pardo 在 The Evaluation and Optimization of Trading Strategies (Wiley, 2008) 中指出,优化的目标不是找到单一的最佳参数组合,而是找到一个稳定的参数区域。他强调策略必须在一系列参数值上表现出稳健性,而非仅仅依赖一个尖锐的峰值。而GA的设计初衷恰恰是寻找那个尖锐的峰值。
贝叶斯优化:一种概率方法
与寻找单一最佳点不同,贝叶斯优化会为目标函数(即EA的表现)建立一个概率模型。它使用高斯过程(GP)来映射参数与表现之间的关系。GP能提供均值预测(预期表现)和方差(对该预测的不确定性)。
随后,算法利用采集函数(如期望改进或置信上界)来决定下一步在哪里进行测试。它平衡了探索(测试不确定性高的区域)和利用(测试预测均值高的区域)。这比GA高效得多,因为它能从每次测试中学习。
在MQL5中实现贝叶斯优化器
我们无法重写策略测试器的核心,但可以构建一个在其中运行的框架。思路是创建一个"包装器"EA,通过
OnTester 和 OnTesterInit 控制优化过程。概念性实现如下:
``
cpp
//+------------------------------------------------------------------+
//| BayesianOpt.mq5 |
//| 贝叶斯优化框架 |
//+------------------------------------------------------------------+
#property copyright "FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
// 待优化的外部参数
sinput double risk_per_trade = 1.0; // 0.5 - 2.0 Step 0.1
sinput int atr_period = 14; // 10 - 30 Step 1
sinput int ma_fast = 10; // 5 - 30 Step 1
sinput int ma_slow = 30; // 20 - 100 Step 1
// 贝叶斯优化相关全局变量
struct GaussianProcess {
// 高斯过程的占位符。实际实现涉及矩阵运算(核函数、乔列斯基分解)
// 在无外部库支持的MQL5中实现较为复杂。
// 我们在此仅为演示目的模拟其逻辑。
};
//+------------------------------------------------------------------+
//| 专家顾问触发函数 |
//+------------------------------------------------------------------+
void OnTick() {
// EA核心逻辑在此...
// 为聚焦优化过程,此处不展开交易逻辑细节。
}
//+------------------------------------------------------------------+
//| 测试器初始化函数 |
//+------------------------------------------------------------------+
void OnTesterInit() {
// 在优化开始时重置GP模型
// 实际实现中,需要定义初始随机点来启动GP。
Print("正在初始化贝叶斯优化器...");
}
//+------------------------------------------------------------------+
//| 测试器单次测试函数 |
//+------------------------------------------------------------------+
double OnTester() {
// 该函数在每组参数测试时被调用。
// 需要返回适应度值(如夏普比率)。
double sharpe_ratio = CalculateSharpeRatio();
// 这里是关键所在。
// 标准GA中,仅需返回值。
// 对于贝叶斯方法,我们需要记录参数和结果,
// 在每次测试后更新我们的GP模型。
// 但由于测试框架是独立处理每次测试的,我们无法在单次测试运行期间
// 更新GP。
// 在MQL5中实现贝叶斯优化的真正方法是
// 将结果保存到文件/CSV中,并运行外部优化器(如Python脚本),
// 通过终端调用EA。这就是"黑盒"优化方法。
return sharpe_ratio;
}
//+------------------------------------------------------------------+
//| 测试器结束函数 |
//+------------------------------------------------------------------+
void OnTesterDeinit() {
// 将GP模型和结果保存到文件,供外部处理。
// 关键在于:导出数据,让外部Python脚本
// 运行贝叶斯优化,决定下一组要测试的参数。
ExportResultsToCSV();
}
//+------------------------------------------------------------------+
`
上述代码揭示了一个关键细节:MQL5内置的优化本质上是顺序的。它独立测试每组参数。对于真正的贝叶斯优化,你需要一个反馈循环。标准的策略测试器在单次运行中并不支持这一点。
实际变通方案:
<strong>首次运行:</strong> 在较宽参数范围内以优化模式运行EA。将所有 OnTester 结果(参数+适应度)导出到文件。
<strong>外部处理:</strong> 使用Python的 scikit-optimize 或 GPyOpt 读取该文件,构建GP,并生成下一组建议参数。
<strong>迭代运行:</strong> 手动(或通过脚本)再次运行优化器,但将参数范围限定在贝叶斯模型"建议"的下一组值上。重复此过程。
这是一种经典的"黑盒"优化方法。虽不如引擎内解决方案优雅,但非常有效。
独家视角:"优化悖论"
这是我多年来一直在思考的一个观点。在追求优化的过程中,我们常把"适应度函数"奉为圭臬。但适应度函数本身就是过度拟合的主要来源。例如,使用简单的夏普比率可能会因少数离群交易而产生严重偏差。
我的提议:动态适应度函数。
我建议使用一个会"进化"的适应度函数,而不是静态指标。在优化的早期阶段,适应度应更侧重于"稳健性"(例如,较高的平均盈亏比,结合较低的最大回撤)。在后期,当我们缩小到稳定区域后,再将权重转向纯粹的盈利能力。
这模拟了人类交易员分析策略的方式:首先,你想看看它是否"有效";后期,你才尝试榨取最后一分利润。据我所知,这在EA开发文献中很少被讨论。这是一种对优化过程本身的"元优化"。
推进分析的雷区
即使有了贝叶斯优化,如果不进行推进分析(WFA),你的策略依然注定失败。WFA是验证策略稳健性的黄金标准。
过程:
<strong>样本内(IS)期间:</strong> 在历史数据的一个周期上优化策略(如2020年1月-2021年12月)。
<strong>样本外(OOS)期间:</strong> 取"最优"参数,在随后周期上运行策略(如2022年1月-2022年12月)。
<strong>滚动推进:</strong> 将IS期间向前滚动(2021年1月-2022年12月),重新优化,然后在下一个OOS(2023年1月-2023年12月)上进行测试。
这模拟了策略在实时环境中的表现。
隐藏细节: 大多数人运行WFA,如果OOS结果为正,就欢欣鼓舞。但OOS结果的方差至关重要。如果OOS表现剧烈波动(例如,一年+20%,下一年-15%),则该策略不稳定。目标是在所有OOS期间实现一致的、即使较低的表现。这比平均OOS利润更重要。
参考来源: 此方法与CFA Institute评估投资策略的标准一致。他们强调,稳健的策略必须在不同的市场制度下表现出稳健性,而非仅在一个历史时期有效。
关于"未来函数"的注意
在进行WFA时,必须严防"未来函数"(或前视偏差)。常见来源包括:
iClose(NULL, PERIOD_CURRENT, 1):安全。这是昨天的收盘价。
iClose(NULL, PERIOD_CURRENT, 0)`:这是当前未完成的K线。用它作为开平仓信号或移动止损,是典型的前视偏差。你使用了在交易发生时根本不存在的未来数据。结语:面向实效的路径
你可以花数月时间在Python脚本中构建完美的贝叶斯优化器。这是一个漂亮的数学解决方案。但对于时间有限的零售交易者,我提供一个务实的折衷方案:使用MT5的GA,但以推进分析的思维模式进行。
这种方法在数学上不如贝叶斯优化纯粹,但实用得多,而且根据我的经验,它之所以能带来更好的实盘结果,是因为它迫使你与数据本身角力,而不仅仅是与数学公式角力。
参考来源: 这种务实的方法得到了 Marcos López de Prado 在 Advances in Financial Machine Learning (Wiley, 2018) 中的支持,他倡导采用严谨的数据科学方法进行金融策略开发,强调了通过交叉验证和清洗测试集来防范过度拟合的风险。
归根结底,代码只是画布。艺术在于你如何设计验证框架。贝叶斯优化是一把出色的画笔,但你仍然需要知道如何画出一幅在实盘市场的阳光下不会崩坏的画作。
本文首发于FXEAR.com,原创内容,未经授权禁止转载。