Files
2025-12-26 21:17:09 +03:30

277 lines
9.0 KiB
JavaScript

/**
* Monte Carlo Simulation - Analyzes strategy robustness through trade randomization
*
* This module shuffles trade sequences thousands of times to calculate:
* - Distribution of possible outcomes
* - Confidence intervals (5th-95th percentile)
* - Risk of ruin probability
* - Expected vs worst-case scenarios
*/
class MonteCarloSimulation {
constructor(trades, iterations = 1000, rorThreshold = 0.2) {
this.trades = trades; // Array of trade objects with profit/loss
this.iterations = iterations;
this.rorThreshold = rorThreshold; // Risk of ruin threshold (default 20%)
this.startingEquity = 10000;
}
/**
* Run the full Monte Carlo simulation
* @returns {Object} Statistical results
*/
run() {
const startTime = performance.now();
const results = [];
for (let i = 0; i < this.iterations; i++) {
const shuffled = this.shuffleTrades();
const equity = this.calculateEquityCurve(shuffled);
results.push({
finalEquity: equity[equity.length - 1],
maxEquity: Math.max(...equity),
minEquity: Math.min(...equity),
maxDrawdown: this.calculateMaxDrawdown(equity),
equityCurve: equity
});
}
const statistics = this.calculateStatistics(results);
const endTime = performance.now();
return {
iterations: this.iterations,
statistics,
distribution: results.map(r => r.finalEquity),
drawdownDistribution: results.map(r => r.maxDrawdown),
riskOfRuin: this.calculateRiskOfRuin(results),
confidence: this.calculateConfidenceIntervals(results),
executionTime: endTime - startTime
};
}
/**
* Shuffle trades using Fisher-Yates algorithm
* @returns {Array} Shuffled copy of trades
*/
shuffleTrades() {
const shuffled = [...this.trades];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
/**
* Calculate equity curve for a sequence of trades
* @param {Array} trades - Ordered array of trades
* @returns {Array} Equity values over time
*/
calculateEquityCurve(trades) {
const equity = [this.startingEquity];
let currentEquity = this.startingEquity;
for (const trade of trades) {
currentEquity += trade.profit;
equity.push(currentEquity);
}
return equity;
}
/**
* Calculate maximum drawdown from equity curve
* @param {Array} equity - Equity curve
* @returns {Number} Max drawdown percentage
*/
calculateMaxDrawdown(equity) {
let maxEquity = equity[0];
let maxDD = 0;
for (const value of equity) {
if (value > maxEquity) {
maxEquity = value;
}
const drawdown = ((maxEquity - value) / maxEquity) * 100;
if (drawdown > maxDD) {
maxDD = drawdown;
}
}
return maxDD;
}
/**
* Calculate comprehensive statistics from results
* @param {Array} results - Array of simulation results
* @returns {Object} Statistical measures
*/
calculateStatistics(results) {
const finalEquities = results.map(r => r.finalEquity);
const drawdowns = results.map(r => r.maxDrawdown);
return {
equity: {
mean: this.mean(finalEquities),
median: this.median(finalEquities),
stdDev: this.standardDeviation(finalEquities),
min: Math.min(...finalEquities),
max: Math.max(...finalEquities),
percentile5: this.getPercentile(finalEquities, 5),
percentile25: this.getPercentile(finalEquities, 25),
percentile50: this.getPercentile(finalEquities, 50),
percentile75: this.getPercentile(finalEquities, 75),
percentile95: this.getPercentile(finalEquities, 95)
},
drawdown: {
mean: this.mean(drawdowns),
median: this.median(drawdowns),
stdDev: this.standardDeviation(drawdowns),
min: Math.min(...drawdowns),
max: Math.max(...drawdowns),
percentile5: this.getPercentile(drawdowns, 5),
percentile25: this.getPercentile(drawdowns, 25),
percentile50: this.getPercentile(drawdowns, 50),
percentile75: this.getPercentile(drawdowns, 75),
percentile95: this.getPercentile(drawdowns, 95)
}
};
}
/**
* Calculate risk of ruin (probability of losing X% of capital)
* @param {Array} results - Simulation results
* @returns {Number} Risk of ruin percentage
*/
calculateRiskOfRuin(results) {
const ruinEquity = this.startingEquity * (1 - this.rorThreshold);
const ruinCount = results.filter(r => r.minEquity <= ruinEquity).length;
return (ruinCount / results.length) * 100;
}
/**
* Calculate confidence intervals
* @param {Array} results - Simulation results
* @returns {Object} Confidence interval data
*/
calculateConfidenceIntervals(results) {
const finalEquities = results.map(r => r.finalEquity);
return {
range90: {
lower: this.getPercentile(finalEquities, 5),
upper: this.getPercentile(finalEquities, 95)
},
range50: {
lower: this.getPercentile(finalEquities, 25),
upper: this.getPercentile(finalEquities, 75)
}
};
}
/**
* Calculate mean (average)
* @param {Array} data - Numeric array
* @returns {Number} Mean value
*/
mean(data) {
return data.reduce((sum, val) => sum + val, 0) / data.length;
}
/**
* Calculate median (50th percentile)
* @param {Array} data - Numeric array
* @returns {Number} Median value
*/
median(data) {
return this.getPercentile(data, 50);
}
/**
* Calculate standard deviation
* @param {Array} data - Numeric array
* @returns {Number} Standard deviation
*/
standardDeviation(data) {
const avg = this.mean(data);
const squareDiffs = data.map(value => Math.pow(value - avg, 2));
const avgSquareDiff = this.mean(squareDiffs);
return Math.sqrt(avgSquareDiff);
}
/**
* Calculate percentile value
* @param {Array} data - Numeric array
* @param {Number} percentile - Percentile to calculate (0-100)
* @returns {Number} Percentile value
*/
getPercentile(data, percentile) {
const sorted = [...data].sort((a, b) => a - b);
const index = (percentile / 100) * (sorted.length - 1);
const lower = Math.floor(index);
const upper = Math.ceil(index);
const weight = index - lower;
if (lower === upper) {
return sorted[lower];
}
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
}
/**
* Generate summary text for results
* @param {Object} results - Monte Carlo results
* @returns {String} Human-readable summary
*/
static generateSummary(results) {
const { statistics, riskOfRuin, confidence } = results;
const expectedReturn = ((statistics.equity.mean - 10000) / 10000 * 100).toFixed(2);
let summary = `Monte Carlo Analysis (${results.iterations} iterations):\n\n`;
summary += `Expected Return: ${expectedReturn}%\n`;
summary += `90% Confidence Range: $${confidence.range90.lower.toFixed(2)} - $${confidence.range90.upper.toFixed(2)}\n`;
summary += `Risk of Ruin (${(results.rorThreshold * 100)}%): ${riskOfRuin.toFixed(2)}%\n\n`;
if (riskOfRuin < 5) {
summary += `✅ Low risk - Strategy shows good robustness`;
} else if (riskOfRuin < 15) {
summary += `⚠️ Moderate risk - Acceptable but monitor closely`;
} else {
summary += `❌ High risk - Consider reducing position size or avoiding`;
}
return summary;
}
/**
* Extract trades from strategy backtest results
* @param {Object} strategy - Strategy with metrics
* @returns {Array} Array of trade objects
*/
static extractTrades(strategy) {
if (!strategy.metrics || !strategy.metrics.trades) {
throw new Error('Strategy must have backtest results with trades');
}
const trades = strategy.metrics.trades.map(trade => ({
profit: trade.profit,
type: trade.type,
entry: trade.entry,
exit: trade.exit
}));
return trades;
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = MonteCarloSimulation;
}