Summary: Advanced Monte Carlo method for EA stress testing using trade sequence bootstrapping. Learn to measure ruin probability, generate percentile fan charts, and identify hidden path-dependency risks that standard backtests miss.
A backtest showing 42% net profit and a 1.8 profit factor looks great on paper. But here is 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 have been dramatically different.
1. The Path Dependency Problem
We observed 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. However, when we ran it through a sequence stress test, approximately 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. Just different luck on the order. That is the problem nobody talks about at the backtest stage.
2. Bootstrap Resampling Method
The solution is Monte Carlo simulation with bootstrap resampling—the simplest, most assumption-free form of Monte Carlo available.
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 the initial balance. Repeat 1000 times, and obtain 1000 plausible equity paths.
Why bootstrap instead of a parametric model (e.g., fitting a normal distribution)? Because real P&L distributions are not normal. They are fat-tailed, often skewed, and sometimes bimodal. Bootstrap does not assume anything about the shape—it samples from what actually happened.
3. Complete Monte Carlo Risk Assessor Implementation
```cpp
//+------------------------------------------------------------------+
//| MonteCarlo_RiskAssessor.mq5 |
//| Stress tests trade sequences using bootstrap resampling |
//+------------------------------------------------------------------+
#property script_show_inputs
//--- Core simulation inputs
input string InpCSVFile = "trades.csv"; // CSV file in MQL5\Files\
input int InpSimulations = 1000; // Number of MC simulations
input double InpInitialBalance = 10000.0; // Starting account balance ($)
input double InpRuinThreshold = 0.20; // Ruin = 20% drawdown
//--- Commission & slippage stress-test
input bool InpSlippageEnabled = false;
input double InpCommission = 2; // Fixed commission per trade ($)
input double InpSlippageMax = 3; // Max random slippage per trade ($)
//--- CSV export
input bool InpExportCSV = true;
input string InpExportFile = "mc_results.csv";
//--- Global data arrays
double g_Profits[]; // Trade P&L values 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 row 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]); // Assuming column 1 is P&L
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;
PrintFormat("Loaded %d trades from %s", g_TradeCount, fileName);
return true;
}
//+------------------------------------------------------------------+
//| Core 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;
double maxEquity = InpInitialBalance;
double maxDrawdown = 0;
for(int t = 0; t < g_TradeCount; t++) {
int rIdx = MathRand() % g_TradeCount;
double pnl = g_Profits[rIdx];
// Apply commission and slippage if enabled
if(InpSlippageEnabled) {
double slip = ((double)MathRand() / 32767.0) * InpSlippageMax;
pnl -= (InpCommission + slip);
}
equity[t+1] = equity[t] + pnl;
// Track drawdown
if(equity[t+1] > maxEquity)
maxEquity = equity[t+1];
double currentDD = (maxEquity - equity[t+1]) / maxEquity * 100;
if(currentDD > maxDrawdown)
maxDrawdown = currentDD;
// Store in flattened matrix
g_AllCurves[sim * curveLen + (t+1)] = equity[t+1];
}
g_FinalEquities[sim] = equity[curveLen - 1];
g_MaxDrawdowns[sim] = maxDrawdown;
}
}
//+------------------------------------------------------------------+
//| Calculate percentile from sorted array |
//+------------------------------------------------------------------+
double Percentile(double &array[], double p) {
int size = ArraySize(array);
if(size == 0) return 0;
double sorted[];
ArrayCopy(sorted, array);
ArraySort(sorted);
double index = (size - 1) * p;
int lower = (int)MathFloor(index);
int upper = (int)MathCeil(index);
if(lower == upper)
return sorted[lower];
double weight = index - lower;
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
}
//+------------------------------------------------------------------+
//| Compute risk metrics from simulation results |
//+------------------------------------------------------------------+
void ComputeRiskMetrics() {
// Sort for percentile calculations
double sortedFinalEq[];
ArrayCopy(sortedFinalEq, g_FinalEquities);
ArraySort(sortedFinalEq);
double sortedDD[];
ArrayCopy(sortedDD, g_MaxDrawdowns);
ArraySort(sortedDD);
// Percentiles for final equity
double p05Eq = Percentile(sortedFinalEq, 0.05);
double p50Eq = Percentile(sortedFinalEq, 0.50);
double p95Eq = Percentile(sortedFinalEq, 0.95);
// Percentiles for max drawdown
double p50DD = Percentile(sortedDD, 0.50);
double p95DD = Percentile(sortedDD, 0.95);
// Value at Risk (5%)
double varAmount = InpInitialBalance - p05Eq;
// Probability of ruin (drawdown exceeding threshold)
int ruinCount = 0;
for(int i = 0; i < InpSimulations; i++) {
if(g_MaxDrawdowns[i] >= InpRuinThreshold * 100)
ruinCount++;
}
double probRuin = (double)ruinCount / InpSimulations;
// Output results
Print("=== Monte Carlo Risk Assessment Results ===");
PrintFormat("Simulations run: %d", InpSimulations);
PrintFormat("Initial balance: $%.2f", InpInitialBalance);
PrintFormat("Ruin threshold: %.1f%%", InpRuinThreshold * 100);
Print("");
Print("--- Final Equity Distribution ---");
PrintFormat("5th percentile: $%.2f", p05Eq);
PrintFormat("50th percentile (median): $%.2f", p50Eq);
PrintFormat("95th percentile: $%.2f", p95Eq);
Print("");
Print("--- Drawdown Distribution ---");
PrintFormat("Median max drawdown: %.1f%%", p50DD);
PrintFormat("95th percentile max drawdown: %.1f%%", p95DD);
Print("");
Print("--- Risk Metrics ---");
PrintFormat("Value at Risk (5%%): $%.2f", varAmount);
PrintFormat("Probability of ruin (>%.1f%% DD): %.1f%%", InpRuinThreshold * 100, probRuin * 100);
}
//+------------------------------------------------------------------+
//| Export fan chart data to CSV |
//+------------------------------------------------------------------+
void ExportFanChartData() {
if(!InpExportCSV) return;
int handle = FileOpen(InpExportFile, FILE_WRITE | FILE_CSV | FILE_ANSI, ',');
if(handle == INVALID_HANDLE) {
Print("Failed to create export file");
return;
}
int curveLen = g_TradeCount + 1;
// Write header
FileWrite(handle, "TradeNum", "P5", "P25", "P50", "P75", "P95");
// Calculate percentiles at each trade step
for(int step = 0; step < curveLen; step++) {
double values[];
ArrayResize(values, InpSimulations);
for(int sim = 0; sim < InpSimulations; sim++) {
values[sim] = g_AllCurves[sim * curveLen + step];
}
double p05 = Percentile(values, 0.05);
double p25 = Percentile(values, 0.25);
double p50 = Percentile(values, 0.50);
double p75 = Percentile(values, 0.75);
double p95 = Percentile(values, 0.95);
FileWrite(handle, step, p05, p25, p50, p75, p95);
}
FileClose(handle);
PrintFormat("Fan chart data exported to: %s", InpExportFile);
}
//+------------------------------------------------------------------+
//| Script entry point |
//+------------------------------------------------------------------+
void OnStart() {
if(!LoadTradesFromCSV(InpCSVFile)) {
Print("Failed to load trade data. Exiting.");
return;
}
PrintFormat("Starting Monte Carlo simulation with %d simulations...", InpSimulations);
uint startTime = GetTickCount();
RunMonteCarloSimulation();
uint elapsed = GetTickCount() - startTime;
PrintFormat("Simulation completed in %.2f seconds", elapsed / 1000.0);
ComputeRiskMetrics();
ExportFanChartData();
}
```
4. Interpreting The Fan Chart
The five percentile curves—5th, 25th, 50th, 75th, and 95th—form the fan chart that anchors the output. 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 series means the strategy is sensitive to sequence effects—worth knowing before going live.
5. Four Core Risk Metrics
From the simulation set, four critical metrics emerge:
| Metric | Description |
|--------|-------------|
| Median Max Drawdown | The typical worst peak-to-trough decline |
| Stress Drawdown (95th percentile) | Drawdown exceeded in only 5% of runs |
| Value at Risk (5%) | Initial Balance − P5_Final_Equity |
| Probability of Ruin | Fraction of simulations exceeding ruin threshold |
Reference: MQL5 Community, "Stress Testing Trade Sequences with Monte Carlo" (mql5.com/articles/22291); Pardo, Robert. "The Evaluation and Optimization of Trading Strategies" (Wiley, 2008).