一、为什么回测与优化如此重要
回测是在历史数据上测试你的EA,以评估其在实盘交易前的表现。优化是为你的策略寻找最佳参数值的过程。没有正确的回测和优化,你基本上是在用真金白银赌博。这些过程将你的EA从一个未经测试的假设转变为一个经过验证的交易系统。
二、回测完整速查表
| 组件 | 说明 | 关键设置 |
|------|------|----------|
| 策略测试器 | MT4内置回测平台 | 品种、模型、日期范围 |
| 优化模式 | 参数搜索方法 | 慢速完整、快速遗传 |
| 前向测试 | 样本外验证 | 独立的日期范围 |
| 报告指标 | 绩效评估 | 盈利因子、夏普比率、回撤 |
三、MT4策略测试器 - 入门指南
```mql4
// 回测前,确保EA有正确的输入参数
// 针对回测优化的EA结构示例
input double InpLotSize = 0.1; // 固定手数
input int InpMagic = 12345; // EA标识符
input bool InpUseFixLot = true; // 使用固定手数vs风险手数
input double InpRiskPercent = 2.0; // 每笔交易风险(%)
input int InpStopLoss = 50; // 止损点数
input int InpTakeProfit = 100; // 止盈点数
input int InpTrailingStart = 30; // 移动止损启动点数
input int InpTrailingStep = 10; // 移动止损步长
input int InpMaxSpread = 30; // 最大允许点差
input int InpSlippage = 30; // 滑点点数
input string InpSymbol = "EURUSD"; // 交易品种
// 添加OnTester()函数用于优化自定义指标
double OnTester() {
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
double sharpeRatio = TesterStatistics(STAT_SHARPE_RATIO);
double maxDrawdown = TesterStatistics(STAT_EQUITY_DD_PERCENT);
// 自定义优化标准 - 最大化此值
double customScore = profitFactor * (1 - maxDrawdown/100);
Print("盈利因子:", profitFactor);
Print("夏普比率:", sharpeRatio);
Print("最大回撤:", maxDrawdown, "%");
Print("自定义评分:", customScore);
return customScore;
}
```
如何在MT4中运行回测
分步操作指南:
1. 打开MT4,按Ctrl+R打开策略测试器
2. 从下拉列表中选择你的EA
3. 选择交易品种(如EURUSD)
4. 设置模型:选择“每个即时价格”以获得最准确结果
5. 设置日期范围(如2024.01.01 - 2024.06.01)
6. 可选的:开启可视化模式观察测试过程
7. 关闭优化模式进行单次测试
8. 点击“开始”
四、回测模型 - 精度与速度的权衡
| 模型 | 精度 | 速度 | 适用场景 |
|------|------|------|----------|
| 每个即时价格 | 最高(99%) | 慢 | 最终验证 |
| 1分钟OHLC | 中等(85%) | 中等 | 快速检查 |
| 仅使用开盘价 | 最低(60%) | 快 | 初步测试 |
```mql4
// 确保准确回测,将以下代码添加到EA中
int OnInit() {
// 检查是否在回测环境中
if(IsTesting()) {
Print("在策略测试器模式下运行");
Print("模型:推荐使用【每个即时价格】模型以确保精度");
}
return(INIT_SUCCEEDED);
}
```
五、单次回测 - 评估一组参数
```mql4
// 单次回测流程
// 1. 关闭优化
// 2. 输入你的参数值
// 3. 运行测试并分析报告
// 回测报告需要分析的关键指标:
/*
1. 盈利因子 = 毛利 / 毛损
- 大于1.5:良好
- 大于2.0:优秀
2. 夏普比率
- 大于1.0:可接受
- 大于2.0:非常好
3. 最大回撤
- 低于20%:可接受
- 低于10%:优秀
4. 总交易次数
- 至少200次才能获得可靠的统计
5. 盈利交易占比
- 趋势策略大于40%
- 均值回归策略大于55%
6. 平均每笔交易
- 必须为正
7. 连续亏损次数
- 大多数策略不应超过10次
*/
```
六、优化 - 寻找最佳参数
```mql4
// 第1步:定义带优化范围的输入参数
input int InpFastMAPeriod = 10; // 快线MA周期(5到30,步长5)
input int InpSlowMAPeriod = 30; // 慢线MA周期(20到100,步长10)
input int InpRSIPeriod = 14; // RSI周期(7到21,步长2)
input int InpStopLoss = 50; // 止损(30到150,步长10)
input int InpTakeProfit = 100; // 止盈(50到300,步长25)
// 第2步:添加OnTester函数用于优化目标
double OnTester() {
// 获取回测统计
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
double sharpeRatio = TesterStatistics(STAT_SHARPE_RATIO);
double maxDrawdown = TesterStatistics(STAT_EQUITY_DD_PERCENT);
double totalTrades = TesterStatistics(STAT_TRADES);
double percentProfit = TesterStatistics(STAT_PROFIT_TRADES) / totalTrades * 100;
double avgTrade = TesterStatistics(STAT_GROSS_PROFIT) / totalTrades;
// 惩罚交易次数过少的策略
double tradePenalty = 1.0;
if(totalTrades < 100) tradePenalty = totalTrades / 100;
// 惩罚高回撤
double drawdownPenalty = 1.0;
if(maxDrawdown > 30) drawdownPenalty = 0.5;
else if(maxDrawdown > 20) drawdownPenalty = 0.8;
// 综合评分
double customScore = profitFactor * sharpeRatio * tradePenalty * drawdownPenalty;
// 将结果保存到文件供分析
int handle = FileOpen("OptimizationResults.csv", FILE_WRITE|FILE_CSV|FILE_READ, ",");
if(handle != INVALID_HANDLE) {
FileWrite(handle,
IntegerToString(InpFastMAPeriod),
IntegerToString(InpSlowMAPeriod),
IntegerToString(InpRSIPeriod),
IntegerToString(InpStopLoss),
IntegerToString(InpTakeProfit),
DoubleToString(profitFactor, 2),
DoubleToString(sharpeRatio, 2),
DoubleToString(maxDrawdown, 2),
DoubleToString(customScore, 2)
);
FileClose(handle);
}
return customScore;
}
// 如何运行优化:
// 1. 打开策略测试器(Ctrl+R)
// 2. 选择你的EA
// 3. 勾选“优化”复选框
// 4. 在“输入”选项卡中设置输入范围
// 5. 点击“开始”
// 6. 结果出现在“优化结果”选项卡中
```
七、避免过度拟合(曲线拟合)
```mql4
// 过度拟合是指EA完美适配历史数据
// 它会在实盘交易中失败。以下是如何避免:
// 方法1:使用样本外测试
// 将数据分为样本内(用于优化)和样本外(用于验证)
/*
推荐的分割:
以2年数据为例:
*/
// 方法2:在多个品种上验证
bool ValidateOnMultipleSymbols() {
string symbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD"};
int passedCount = 0;
for(int i = 0; i < 4; i++) {
double profitFactor = TestOnSymbol(symbols[i]);
if(profitFactor > 1.2) passedCount++;
}
return (passedCount >= 3); // 必须在4个品种中的3个上有效
}
// 方法3:前进分析
/*
前进分析流程:
1. 在时段1上优化(如1-3月)
2. 在时段2上测试(如4月)
3. 在时段2上优化(2-4月)
4. 在时段3上测试(5月)
5. 重复并汇总结果
*/
// 方法4:参数敏感性分析
void AnalyzeParameterSensitivity() {
// 一个稳健的策略应该有参数的高原区域
// 而不是单一的尖峰
double baseProfitFactor = 1.5;
int fastMATested[] = {8, 9, 10, 11, 12};
int slowMATested[] = {25, 30, 35, 40};
Print("参数敏感性分析:");
Print("快线MA\t慢线MA\t盈利因子\t是否稳定?");
for(int f = 0; f < 5; f++) {
for(int s = 0; s < 4; s++) {
// 使用这些参数运行回测
double pf = RunBacktestWithParams(fastMATested[f], slowMATested[s]);
bool stable = (pf > baseProfitFactor * 0.8);
Print(fastMATested[f], "\t", slowMATested[s], "\t", pf, "\t", stable ? "是" : "否");
}
}
}
double RunBacktestWithParams(int fastMA, int slowMA) {
// 模拟回测 - 在实际代码中,你会运行实际回测
// 返回盈利因子
return 1.5;
}
```
八、解读回测结果
```mql4
// 完整的回测结果分析器
void AnalyzeBacktestResults() {
// 获取上次回测的所有统计
double grossProfit = TesterStatistics(STAT_GROSS_PROFIT);
double grossLoss = TesterStatistics(STAT_GROSS_LOSS);
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
double sharpeRatio = TesterStatistics(STAT_SHARPE_RATIO);
double maxDrawdown = TesterStatistics(STAT_EQUITY_DD_PERCENT);
double totalTrades = TesterStatistics(STAT_TRADES);
double profitableTrades = TesterStatistics(STAT_PROFIT_TRADES);
double losingTrades = TesterStatistics(STAT_LOSS_TRADES);
double averageProfit = TesterStatistics(STAT_AVERAGE_PROFIT_TRADE);
double averageLoss = TesterStatistics(STAT_AVERAGE_LOSS_TRADE);
double maxConsecutiveWins = TesterStatistics(STAT_CONPROFIT_MAX);
double maxConsecutiveLosses = TesterStatistics(STAT_CONLOSS_MAX);
Print("========== 回测分析 ==========");
Print("毛利:", grossProfit);
Print("毛损:", grossLoss);
Print("盈利因子:", profitFactor, " ", GetProfitFactorRating(profitFactor));
Print("夏普比率:", sharpeRatio, " ", GetSharpeRating(sharpeRatio));
Print("最大回撤:", maxDrawdown, "% ", GetDrawdownRating(maxDrawdown));
Print("总交易次数:", totalTrades);
Print("盈利交易:", profitableTrades, " (",
DoubleToString(profitableTrades/totalTrades*100, 1), "%)");
Print("亏损交易:", losingTrades, " (",
DoubleToString(losingTrades/totalTrades*100, 1), "%)");
Print("平均盈利:", averageProfit);
Print("平均亏损:", averageLoss);
Print("最大连续盈利次数:", maxConsecutiveWins);
Print("最大连续亏损次数:", maxConsecutiveLosses);
// 破产风险计算
double winRate = profitableTrades / totalTrades;
double avgWinLossRatio = -averageProfit / averageLoss;
double riskOfRuin = CalculateRiskOfRuin(winRate, avgWinLossRatio);
Print("破产风险(10次交易):", riskOfRuin * 100, "%");
Print("========================================");
}
string GetProfitFactorRating(double pf) {
if(pf >= 2.0) return "(优秀)";
if(pf >= 1.5) return "(良好)";
if(pf >= 1.2) return "(可接受)";
return "(差 - 应拒绝)";
}
string GetSharpeRatioRating(double sr) {
if(sr >= 2.0) return "(非常好)";
if(sr >= 1.0) return "(良好)";
if(sr >= 0.5) return "(可接受)";
return "(差)";
}
string GetDrawdownRating(double dd) {
if(dd <= 10) return "(优秀)";
if(dd <= 20) return "(良好)";
if(dd <= 30) return "(可接受)";
return "(过高)";
}
double CalculateRiskOfRuin(double winRate, double winLossRatio) {
// 简化版的破产风险计算
double p = winRate; // 胜率
double q = 1 - p; // 败率
double b = winLossRatio; // 盈亏比
// 有意义计算的前提条件
if(p * b <= q) return 1.0; // 负期望值
// 连续亏损10次的概率
return MathPow(q, 10);
}
```
九、前向测试 - 最终验证
```mql4
// 前向测试流程
/*
1. 优化后,不要直接使用优化后的参数
2. 在全新的数据上运行前向测试
3. 比较前向测试结果与回测结果
4. 如果结果相似,EA是稳健的
5. 如果结果明显更差,EA过度拟合
前向测试检查清单:
*/
// 蒙特卡洛模拟 - 稳健性测试
void MonteCarloSimulation(int iterations) {
Print("开始蒙特卡洛模拟,共", iterations, "次迭代");
double originalProfitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
double results[];
ArrayResize(results, iterations);
for(int i = 0; i < iterations; i++) {
// 模拟随机订单顺序打乱
// 测试策略是否依赖订单顺序
double shuffledPF = RunShuffledTrades();
results[i] = shuffledPF;
}
// 计算分布
double sum = 0;
for(int i = 0; i < iterations; i++) sum += results[i];
double mean = sum / iterations;
double variance = 0;
for(int i = 0; i < iterations; i++) variance += MathPow(results[i] - mean, 2);
double stdDev = MathSqrt(variance / iterations);
Print("蒙特卡洛结果:");
Print("原始盈利因子:", originalProfitFactor);
Print("打乱后平均盈利因子:", mean);
Print("标准差:", stdDev);
// 如果平均值显著偏低,策略依赖交易顺序(对实盘交易不利)
if(mean < originalProfitFactor * 0.7) {
Print("警告:策略高度依赖交易顺序,实际表现可能不如预期。");
}
}
double RunShuffledTrades() {
// 占位符 - 实际实现中会真正打乱交易顺序
return MathRand() / 32768.0 * 2.0;
}
```
十、常见回测错误及避免方法
| 错误类型 | 问题描述 | 解决方案 |
|----------|----------|----------|
| 未来函数偏差 | 使用了未来数据 | 只使用当前K线收盘价,使用偏移shift=1 |
| 幸存者偏差 | 只使用现有品种 | 包含已退市的品种 |
| 过度优化 | 曲线拟合 | 使用样本外测试 |
| 数据不足 | 样本量太小 | 最少200次交易 |
| 忽略滑点 | 成交价格不真实 | 在EA中添加滑点 |
| 忽略手续费 | 结果不准确 | 在计算中包含手续费 |
| 劣质Tick数据 | 建模不准确 | 使用高质量的Tick数据 |
```mql4
// 示例:在EA中添加真实的滑点和手续费
double GetRealisticFillPrice(int orderType, int slippagePoints) {
RefreshRates();
double idealPrice = (orderType == OP_BUY) ? Ask : Bid;
// 在指定范围内添加随机滑点
int actualSlippage = MathRand() % (slippagePoints + 1);
double fillPrice = (orderType == OP_BUY)
? idealPrice + actualSlippage * Point
: idealPrice - actualSlippage * Point;
return NormalizeDouble(fillPrice, Digits);
}
double CalculateWithCommission(double profit) {
double commissionPerLot = 5.0; // 每手5美元
double lots = 0.1;
double totalCommission = commissionPerLot * lots * 2; // 来回
return profit - totalCommission;
}
```
十一、回测与优化最佳实践清单
参考来源:
9. 下一步
第17篇将讲解实战项目 - 构建多策略组合EA – 组合多种策略、多货币对监控、半自动交易面板的完整指南。