One of the dirtiest secrets in retail algo trading is the chasm between a beautifully optimized MT4 backtest and the grim reality of a forward test. The problem is almost always overfitting, and the usual suspect is the genetic algorithm (GA) optimizer itself. GAs are brilliant for finding a local maximum in a multidimensional space, but they are too good at it. They will happily find that perfect, fragile combination of parameters that worked flawlessly in 2018 and then blew up your account in 2020.
This isn't a condemnation of automated optimization, but a plea for a more nuanced approach. I want to walk through a practical workflow that puts robustness first. It involves ditching the "Optimization" tab's default "Balance" or "Sharpe Ratio" criteria in favor of a custom metric, and implementing a manual, iterative process that I've found to be far more reliable.
The Problem with the GA's "Best" Result
The standard MT4 optimizer, when using a Genetic Algorithm, is essentially a sophisticated curve-fitting tool. It evaluates a subset of parameter combinations, "breeds" the best ones, and mutates them to find an optimal point. The problem is that the fitness function (maximizing balance, profit factor, etc.) is ahistorical. It doesn't care if the strategy survived a specific volatile period; it only cares about the final number.
Let's look at a common scenario. You're optimizing a simple moving average crossover EA. Your parameters are
FastMA and SlowMA. The GA might converge on a solution where the spread between them is just 2 bars (e.g., Fast=14, Slow=16). This will generate a ton of signals, many of which will be whipsaws, but in the specific historical data used for optimization, it caught the trend perfectly. This is the classic "over-optimized" trap. The strategy is brittle.My approach is to optimize for stability rather than performance. I want a set of parameters that sits on a "plateau" of profitability, not a sharp peak. This means we need to analyze the parameter space, not just a single point.
A Manual, Multi-Stage Optimization Process
Here's a process I've refined over a few years. It's a bit more work, but the forward-test results consistently outperform the GA's single best run.
Stage 1: Coarse Grid Search with a Custom Metric
Forget the built-in optimizer for this stage. We'll use a script to run a grid search and store the results in a CSV file. This lets us define our own fitness function. My favorite is a Profit Factor / Max Drawdown Ratio.
Code: MQL4 Grid Search Skeleton
``
mql4
//+------------------------------------------------------------------+
//| GridSearchSkel.mq4 |
//| Copyright 2023, FXEAR.com |
//| https://www.fxear.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict
//+------------------------------------------------------------------+
//| Custom Fitness Function - Profit Factor over Max Drawdown |
//+------------------------------------------------------------------+
double CustomFitness(double profitFactor, double maxDrawdownPercent) {
if (maxDrawdownPercent == 0.0) return profitFactor;
// Penalize high drawdown heavily
return profitFactor / (1.0 + maxDrawdownPercent 10.0);
}
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart() {
int handle = FileOpen("GridSearch_Results.csv", FILE_WRITE|FILE_CSV|FILE_READ, ",");
if (handle < 0) {
Print("Failed to create file. Error: ", GetLastError());
return;
}
// Write CSV Header
FileWrite(handle, "FastMA", "SlowMA", "ProfitFactor", "MaxDD%", "Fitness");
// Define Parameter Ranges
int fastStart = 5, fastEnd = 25, fastStep = 2;
int slowStart = 20, slowEnd = 100, slowStep = 5;
// This is a skeleton. In reality, you would call a backtest function here.
// The following loop simulates the grid search.
for (int fast = fastStart; fast <= fastEnd; fast += fastStep) {
for (int slow = slowStart; slow <= slowEnd; slow += slowStep) {
if (slow <= fast) continue; // Skip invalid combos
// --- Placeholder for backtest execution ---
// double profitFactor = RunBacktest(fast, slow);
// double maxDD = GetMaxDrawdown();
// --- End Placeholder ---
// Simulated data for demonstration
double profitFactor = (rand() % 200) / 100.0;
double maxDD = (rand() % 30) / 100.0;
double fitness = CustomFitness(profitFactor, maxDD);
FileWrite(handle, fast, slow, DoubleToString(profitFactor, 2),
DoubleToString(maxDD 100, 2), DoubleToString(fitness, 3));
}
}
FileClose(handle);
Print("Grid search complete. Results written to 'GridSearch_Results.csv'");
}
//+------------------------------------------------------------------+
`
Commentary on the Code:
#property strict: A must-have. It forces proper variable typing and reduces errors.
CustomFitness: This is where the magic happens. By dividing profit factor by a scaled drawdown, we heavily favor strategies that are smooth. A strategy with PF=2.0 and DD=5% gets a score of 2.0 / (1.5) = 1.33. A strategy with PF=3.0 but DD=20% gets 3.0 / (3.0) = 1.0. The smoother strategy wins. This is a direct counter to the default optimizer's logic.
The FileWrite Loop: This is far more powerful than the built-in optimizer's output because you can then analyze this CSV in Python or Excel to look for "plateaus" of high fitness, not just the single highest point.
Stage 2: Analyzing the "Plateau"
After running the grid, I plot the results in a heatmap. I'm not looking for the single highest peak. Instead, I look for the largest contiguous area of high fitness scores. This "plateau" represents parameter sets that are robust to minor changes. A strategy that works well with Fast=14, Slow=45 and also with Fast=16, Slow=50 is more reliable than one that only works at 14, 45.
A word on the "Walk-Forward" concept: This plateau is essentially a manual, in-sample sanity check. The real test is out-of-sample.
Stage 3: The Walk-Forward Analysis (The Real Test)
This is the most critical part of the process and is often misunderstood. People run a single optimization and then a single forward test. That's not rigorous. A proper walk-forward analysis involves a rolling window.
<strong>In-Sample (IS):</strong> Optimize on data from 2018-2019.
<strong>Out-of-Sample (OOS):</strong> Test the optimized parameters on data from 2020-Q1.
<strong>Roll Forward:</strong> Now, re-optimize on data from 2018-2020 (including the OOS period) and test on 2020-Q2.
Continue this process.
The idea is that if your optimization process is sound, the OOS performance should be, on average, about 60-70% of the IS performance. If it's massively underperforming, your strategy is overfitted.
Addressing the "Future Function" Gotcha
This is the single most overlooked detail in EA development. A "future function" is any indicator or calculation that uses data from a bar that hasn't closed yet at the point in time the EA is executing. The most common culprit is iClose(NULL, 0, 0) inside the OnTick() function. At the opening tick of a new bar, iClose(NULL, 0, 0) is the closing price of the previous bar, which is fine. However, when using high timeframe indicators (e.g., H4 on M15 charts), the 0 index refers to the current, still-forming higher timeframe bar. This can cause your EA to "see" the future.
A specific, tricky example: You have an EA that trades based on the H4 close. You set up a check: if (iClose(NULL, PERIOD_H4, 0) > iMA(NULL, PERIOD_H4, 20, 0, MODE_SMA, PRICE_CLOSE, 0)). In this line, iClose(NULL, PERIOD_H4, 0) is the current, unclosed H4 bar's current price. It's a moving target and is a clear future function. The correct way is to always check the closed bar: iClose(NULL, PERIOD_H4, 1). This is a mistake many advanced programmers make when trying to speed up execution or simplify code.
My exclusive view on this: I believe the industry over-focuses on the optimizer as the sole source of overfitting. In my experience, 80% of backtest-to-real-life failure is due to these hidden future functions, not the optimization algorithm itself. The optimizer is just a magnifying glass; it's highlighting the flaws you already have in your code. Fix the logic first. Run an audit of every iClose and iOpen call, and verify on which bar you're operating.
A Note on Cross-Platform Migration (MQL4 to MQL5)
The concepts of robust optimization are platform-agnostic. However, migrating your custom fitness logic is where the difficulty lies. In MQL5, the OnTester() function gives you direct access to the optimization pass, allowing you to return a custom double value. This is far superior to MQL4's TesterStatistics().
My "plateau" analysis is much easier to implement in MQL5's OnTesterInit() and OnTesterPass() functions, where you can build the heatmap directly within the tester framework. The common mistake when migrating is trying to replicate the MQL4 GlobalVariable trick in MQL5. Don't. Embrace the OnTester event structure; it's cleaner and faster.
The Real Measure of Success
I stopped caring about a high "Optimization Score" a long time ago. The real metric is the ratio of your Walk-Forward Analysis OOS return to your IS return. If this ratio is consistently above 0.6 over multiple rolling windows, you have a robust system. Everything else is just noise.
Reference:
Pardo, Robert. The Evaluation and Optimization of Trading Strategies. John Wiley & Sons, 2008.
MQL5 Reference on OnTester` and Custom Optimization Criteria: https://www.mql5.com/en/docs/basis/ontester本文首发于FXEAR.com,原创内容,未经授权禁止转载。