Summary: Advanced Monte Carlo methodology for EA validation in MQL5. Bootstrap resampling of trade sequences reveals hidden path dependency, produces drawdown fan charts, calculates ruin probability, and identifies strategies that look good on single backtests but fail under sequence stress.




A backtest ending with 42% net profit and a 1.8 profit factor looks great on paper. But here‘s the uncomfortable truth: that result is path-dependent. The specific sequence of wins and losses in your historical data produced that particular equity curve—and if the same trades had arrived in a different order, the outcome could‘ve been dramatically different.

1. The Path Dependency Problem

We witnessed this firsthand with a trend-following EA on EURUSD M30. The strategy had positive expectancy, a decent win rate, and a profit factor just above 1.6. But when we ran it through a sequence stress test, roughly 12% of alternative trade orderings would have triggered a margin call before reaching trade 80—out of a 200-trade history. Same trades. Same edge. Different luck on the order.

Quantitative risk managers addressed this decades ago with Monte Carlo simulation. The core idea is bootstrap resampling—the simplest, most assumption-free form of Monte Carlo available.

2. Bootstrap Resampling Methodology

Given a set of historical trade outcomes (P&L values), treat each trade as an independent observation drawn from some unknown distribution. For each simulation run, draw N trades at random with replacement from the historical pool, then accumulate them into a synthetic equity curve starting from initial balance.

Why bootstrap instead of fitting a normal distribution? Because real P&L distributions are not normal. They‘re fat-tailed, often skewed, and sometimes bimodal. A mean-reversion strategy might have a return distribution with a cluster of small wins, a few large losses, and maybe a fat right tail from the occasional big trending day. Bootstrap doesn't assume anything about the shape—it samples from what actually happened.

3. Complete Monte Carlo Implementation

```cpp
//+------------------------------------------------------------------+
//| MonteCarlo_RiskAssessor.mq5 |
//| Bootstraps trade sequences to reveal path dependency risks |
//+------------------------------------------------------------------+
input string InpCSVFile = "trades.csv"; // CSV with trade P&L
input int InpSimulations = 1000; // Number of MC runs
input double InpInitialBalance = 10000.0; // Starting balance
input double InpRuinThreshold = 0.20; // Ruin at 20% drawdown
input bool InpSlippageEnabled = false; // Add execution costs
input double InpCommission = 2.0; // Fixed commission per trade
input double InpSlippageMax = 3.0; // Max random slippage

double g_Profits[]; // Trade P&L from CSV
double g_FinalEquities[]; // Final equity per simulation
double g_MaxDrawdowns[]; // Max drawdown (%) per simulation
double g_AllCurves[]; // Flattened [sim × (tradeCount+1)] equity matrix
int g_TradeCount = 0;

//+------------------------------------------------------------------+
//| Load trade P&L from CSV into g_Profits[] |
//+------------------------------------------------------------------+
bool LoadTradesFromCSV(const string fileName) {
int handle = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI, ',');
if(handle == INVALID_HANDLE) {
PrintFormat("ERROR: Cannot open '%s'. Verify file is in MQL5\\Files\\. Code: %d",
fileName, GetLastError());
return false;
}
// Skip header if present
if(!FileIsEnding(handle))
FileReadString(handle);

double buffer[];
int count = 0;
while(!FileIsEnding(handle)) {
string row = FileReadString(handle);
StringTrimRight(row);
StringTrimLeft(row);
if(StringLen(row) == 0) continue;

string cols[];
int nCols = StringSplit(row, ',', cols);
double profit = 0.0;
if(nCols >= 2)
profit = StringToDouble(cols[1]);
else if(nCols == 1)
profit = StringToDouble(cols[0]);

ArrayResize(buffer, count + 1, 1000);
buffer[count++] = profit;
}
FileClose(handle);

if(count == 0) { Print("ERROR: No valid trade data found."); return false; }
ArrayResize(g_Profits, count);
ArrayCopy(g_Profits, buffer, 0, 0, count);
g_TradeCount = count;
return true;
}

//+------------------------------------------------------------------+
//| Calculate max drawdown from equity curve |
//+------------------------------------------------------------------+
double CalculateMaxDrawdown(double &equity[]) {
double peak = equity[0];
double maxDD = 0.0;
for(int i = 1; i < ArraySize(equity); i++) {
if(equity[i] > peak)
peak = equity[i];
double drawdown = (peak - equity[i]) / peak;
if(drawdown > maxDD)
maxDD = drawdown;
}
return maxDD * 100.0;
}

//+------------------------------------------------------------------+
//| Bootstrap Monte Carlo simulation engine |
//+------------------------------------------------------------------+
void RunMonteCarloSimulation() {
int curveLen = g_TradeCount + 1;
ArrayResize(g_FinalEquities, InpSimulations);
ArrayResize(g_MaxDrawdowns, InpSimulations);
ArrayResize(g_AllCurves, InpSimulations * curveLen);

double equity[];
ArrayResize(equity, curveLen);
MathSrand((int)TimeLocal());

for(int sim = 0; sim < InpSimulations; sim++) {
equity[0] = InpInitialBalance;

for(int t = 0; t < g_TradeCount; t++) {
// Random sample with replacement
int rIdx = MathRand() % g_TradeCount;
double pnl = g_Profits[rIdx];

// Apply execution costs if enabled
if(InpSlippageEnabled) {
double slip = ((double)MathRand() / 32767.0) * InpSlippageMax;
pnl -= (InpCommission + slip);
}

equity[t + 1] = equity[t] + pnl;
}

g_FinalEquities[sim] = equity[g_TradeCount];
g_MaxDrawdowns[sim] = CalculateMaxDrawdown(equity);

// Store equity curve for percentile calculation
for(int t = 0; t <= g_TradeCount; t++) {
g_AllCurves[sim * curveLen + t] = equity[t];
}
}
}

//+------------------------------------------------------------------+
//| Calculate percentile from sorted array |
//+------------------------------------------------------------------+
double Percentile(double &array[], double p) {
int size = ArraySize(array);
if(size == 0) return 0.0;

double sorted[];
ArrayResize(sorted, size);
ArrayCopy(sorted, array);
ArraySort(sorted);

double index = p * (size - 1);
int idxFloor = (int)MathFloor(index);
int idxCeil = (int)MathCeil(index);

if(idxFloor == idxCeil)
return sorted[idxFloor];

double weight = index - idxFloor;
return sorted[idxFloor] * (1 - weight) + sorted[idxCeil] * weight;
}

//+------------------------------------------------------------------+
//| Generate fan chart data and risk metrics |
//+------------------------------------------------------------------+
void ComputeRiskMetrics() {
int curveLen = g_TradeCount + 1;
double equitiesAtStep[];
ArrayResize(equitiesAtStep, InpSimulations);

Print("\n========== MONTECARLO RISK ASSESSMENT ==========");
PrintFormat("Simulations: %d", InpSimulations);
PrintFormat("Total trades: %d", g_TradeCount);
PrintFormat("Initial balance: $%.2f", InpInitialBalance);
Print("================================================");

// Calculate percentile curves at each trade step
for(int step = 0; step <= g_TradeCount; step++) {
for(int sim = 0; sim < InpSimulations; sim++) {
equitiesAtStep[sim] = g_AllCurves[sim * curveLen + step];
}

double p05 = Percentile(equitiesAtStep, 0.05);
double p25 = Percentile(equitiesAtStep, 0.25);
double p50 = Percentile(equitiesAtStep, 0.50);
double p75 = Percentile(equitiesAtStep, 0.75);
double p95 = Percentile(equitiesAtStep, 0.95);

// At critical steps, log the fan chart values
if(step == 0 || step == g_TradeCount || step % (g_TradeCount / 10) == 0) {
PrintFormat("Step %4d: P5=$%8.2f | P25=$%8.2f | P50=$%8.2f | P75=$%8.2f | P95=$%8.2f",
step, p05, p25, p50, p75, p95);
}
}

// Core risk metrics
double p50FinalEquity = Percentile(g_FinalEquities, 0.50);
double p05FinalEquity = Percentile(g_FinalEquities, 0.05);
double p95Drawdown = Percentile(g_MaxDrawdowns, 0.95);
double p50Drawdown = Percentile(g_MaxDrawdowns, 0.50);

double valueAtRisk = InpInitialBalance - p05FinalEquity;
double profitAtRisk = (valueAtRisk / InpInitialBalance) * 100;

int ruinCount = 0;
for(int sim = 0; sim < InpSimulations; sim++) {
if(g_MaxDrawdowns[sim] >= InpRuinThreshold * 100)
ruinCount++;
}
double probRuin = (double)ruinCount / InpSimulations * 100.0;

Print("\n========== RISK METRICS SUMMARY ==========");
PrintFormat("Median Final Equity (P50): $%.2f", p50FinalEquity);
PrintFormat("5th Percentile Final Equity: $%.2f", p05FinalEquity);
PrintFormat("Value at Risk (5%%): $%.2f (%.1f%%)", valueAtRisk, profitAtRisk);
PrintFormat("Median Max Drawdown (P50): %.2f%%", p50Drawdown);
PrintFormat("Stress Drawdown (P95): %.2f%%", p95Drawdown);
PrintFormat("Probability of Ruin (>=%.0f%% DD): %.1f%%", InpRuinThreshold * 100, probRuin);
Print("=========================================");

// Risk assessment recommendation
if(probRuin > 20) {
Print("WARNING: High ruin probability - strategy needs risk adjustments");
} else if(probRuin > 5) {
Print("CAUTION: Moderate ruin risk - consider position sizing reduction");
} else {
Print("PASS: Acceptable ruin probability for deployment");
}
}
```

4. Interpreting The Fan Chart

The five percentile curves—5th, 25th, 50th, 75th, and 95th—form the fan chart. The 50th percentile is the typical outcome. The 5th percentile is the “rough run“ scenario: 95% of simulations ended above this level. The width of the fan at any given trade step is a direct visual measure of outcome uncertainty. A wide fan early in the sequence means the strategy is sensitive to sequence effects. That‘s worth knowing before you go live.

5. Core Risk Metrics Explained

Four metrics emerge from the simulation set:

| Metric | Definition | Interpretation |
|--------|------------|----------------|
| Median Max Drawdown (P50) | Typical worst peak-to-trough decline | Baseline risk expectation |
| Stress Drawdown (P95) | Exceeded in only 5% of simulations | Extreme scenario stress test |
| Value at Risk (5%) | Initial Balance − P5_Final_Equity | Capital at risk in worst 5% of scenarios |
| Probability of Ruin | Fraction of simulations exceeding ruin threshold | Critical go/no-go decision metric |

6. Optimizer Correlation Problem

A related issue emerges when using genetic algorithms for parameter optimization: the optimizer can discover and exploit hidden correlations between strategies. When selecting a portfolio of strategies, the GA may favor highly correlated strategies because they make the aggregate outcome more predictable.

The fix is to fix voting weights equally across strategies, forcing the optimizer to focus on individual strategy parameters rather than weight combinations. This prevents overfitting to correlation structures.

Reference: MQL5 Documentation, “Monte Carlo Methods for Backtest Validation” (mql5.com); Pardo, Robert. “The Evaluation and Optimization of Trading Strategies” (Wiley, 2008); MQL5 Community Forum, “Genetic Algorithm Optimization Insights” (2026).