diff --git a/css/style.css b/css/style.css index b303ff5..64778bc 100644 --- a/css/style.css +++ b/css/style.css @@ -814,4 +814,175 @@ tbody tr:hover { .loading { animation: pulse 2s ease-in-out infinite; +} + +/* Walk-Forward Analysis Badges */ +.wf-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + margin-left: 10px; +} + +.wf-badge-pass { + background: rgba(72, 187, 120, 0.15); + color: var(--accent-success); + border: 1px solid rgba(72, 187, 120, 0.3); +} + +.wf-badge-fail { + background: rgba(237, 137, 54, 0.15); + color: var(--accent-warning); + border: 1px solid rgba(237, 137, 54, 0.3); +} + +.wf-icon { + font-size: 14px; + font-weight: 700; +} + +.wf-text { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.wf-score { + background: rgba(255, 255, 255, 0.1); + padding: 2px 6px; + border-radius: 10px; + font-size: 11px; + font-weight: 700; +} + +/* Walk-Forward Results Tab */ +.wf-summary { + display: flex; + gap: 30px; + margin-bottom: 30px; + align-items: center; +} + +.robustness-gauge { + text-align: center; + padding: 25px; + background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%); + border-radius: 15px; + color: white; + min-width: 160px; + box-shadow: var(--shadow); +} + +.gauge-value { + font-size: 52px; + font-weight: 700; + line-height: 1; +} + +.gauge-label { + font-size: 13px; + opacity: 0.95; + margin-top: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.wf-status { + flex: 1; + padding: 20px 25px; + border-radius: 12px; + font-size: 16px; +} + +.status-pass { + background: rgba(72, 187, 120, 0.15); + border-left: 4px solid var(--accent-success); + color: var(--accent-success); +} + +.status-fail { + background: rgba(245, 101, 101, 0.15); + border-left: 4px solid var(--accent-danger); + color: var(--accent-danger); +} + +.comparison-table { + margin: 25px 0; + background: var(--bg-secondary); + border-radius: 12px; + overflow: hidden; +} + +.comparison-table table { + width: 100%; + border-collapse: collapse; +} + +.comparison-table th, +.comparison-table td { + padding: 15px 20px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.comparison-table th { + background: var(--bg-card); + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 1px; +} + +.comparison-table tbody tr:hover { + background: var(--bg-hover); +} + +.comparison-table .good { + color: var(--accent-success); + font-weight: 600; +} + +.comparison-table .warning { + color: var(--accent-warning); + font-weight: 600; +} + +.comparison-table .bad { + color: var(--accent-danger); + font-weight: 600; +} + +.wf-charts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 25px; + margin-top: 30px; +} + +.wf-chart-container { + background: var(--bg-secondary); + padding: 20px; + border-radius: 12px; +} + +.wf-chart-container h4 { + margin-bottom: 15px; + color: var(--accent-primary); + font-size: 16px; +} + +@media (max-width: 768px) { + .wf-charts { + grid-template-columns: 1fr; + } + + .wf-summary { + flex-direction: column; + align-items: stretch; + } } \ No newline at end of file diff --git a/index.html b/index.html index 216c7d2..32d8226 100644 --- a/index.html +++ b/index.html @@ -211,6 +211,29 @@ Minimum trade count (10-200) + + +
+

Walk-Forward Analysis

+
+ + Validate strategies on out-of-sample data to detect overfitting +
+
+ + +
+ Training: 0 bars + Testing: 0 bars +
+ Higher ratio = more data for training, less for validation +
+
@@ -297,21 +320,22 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/ga-engine.js b/js/ga-engine.js index 6aa767d..e13e706 100644 --- a/js/ga-engine.js +++ b/js/ga-engine.js @@ -14,9 +14,15 @@ class GeneticOptimizer { minTrades: config.minTrades || 30, minPF: config.minPF || 1.5, maxDD: config.maxDD || 25, - minWR: config.minWR || 45 + minWR: config.minWR || 45, + // Walk-Forward Analysis settings + enableWalkForward: config.enableWalkForward || false, + trainingRatio: config.trainingRatio || 0.7 }; + // Debug: Log walk-forward config + console.log('🔧 GA Config - Walk-Forward:', this.config.enableWalkForward, 'Ratio:', this.config.trainingRatio); + this.population = []; this.foundStrategies = []; this.isRunning = false; @@ -40,10 +46,38 @@ class GeneticOptimizer { * Evaluate fitness for all strategies */ evaluatePopulation() { + // Log walk-forward status (only once per generation) + if (this.currentGeneration === 0) { + console.log('🔬 Walk-Forward Analysis:', this.config.enableWalkForward ? 'ENABLED' : 'DISABLED'); + if (this.config.enableWalkForward) { + console.log('📊 Training Ratio:', (this.config.trainingRatio * 100) + '%'); + } + } + for (const strategy of this.population) { + // Regular backtest on full data const backtester = new Backtester(this.data, strategy, this.symbol); strategy.metrics = backtester.run(); - strategy.fitness = this.calculateFitness(strategy.metrics); + + // Walk-forward analysis (if enabled) + if (this.config.enableWalkForward) { + const wfAnalysis = new WalkForwardAnalysis( + this.data, + this.config.trainingRatio || 0.7 + ); + strategy.walkForward = wfAnalysis.analyze(strategy, this.symbol); + + // Penalize strategies that fail walk-forward validation + if (!strategy.walkForward.passed) { + strategy.fitness = this.calculateFitness(strategy.metrics) * 0.5; // 50% penalty + } else { + // Bonus for high robustness + const robustnessBonus = 1 + (strategy.walkForward.robustness / 200); // Up to 50% bonus + strategy.fitness = this.calculateFitness(strategy.metrics) * robustnessBonus; + } + } else { + strategy.fitness = this.calculateFitness(strategy.metrics); + } } // Sort by fitness (descending) diff --git a/js/main.js b/js/main.js index dde4fb1..0f9de62 100644 --- a/js/main.js +++ b/js/main.js @@ -281,7 +281,10 @@ async function startGeneration() { minPF: parseFloat(document.getElementById('min-pf').value), minWR: parseFloat(document.getElementById('min-wr').value), maxDD: parseFloat(document.getElementById('max-dd').value), - minTrades: parseInt(document.getElementById('min-trades').value) + minTrades: parseInt(document.getElementById('min-trades').value), + // Walk-Forward Analysis settings + enableWalkForward: document.getElementById('enable-walkforward').checked, + trainingRatio: parseInt(document.getElementById('training-ratio').value) / 100 }; // Show progress section @@ -469,9 +472,30 @@ function createStrategyCard(strategy, number) { const card = document.createElement('div'); card.className = 'strategy-card'; + + // Generate walk-forward badge HTML (only if walk-forward was enabled and data exists) + let wfBadgeHTML = ''; + if (strategy.walkForward && typeof strategy.walkForward === 'object') { + const wf = strategy.walkForward; + if (wf.robustness !== undefined && wf.passed !== undefined) { + const badgeClass = wf.passed ? 'wf-badge-pass' : 'wf-badge-fail'; + const badgeIcon = wf.passed ? '✓' : '⚠'; + const badgeText = wf.passed ? 'Robust' : 'Overfitted'; + wfBadgeHTML = ` +
+ ${badgeIcon} + ${badgeText} + ${wf.robustness} +
+ `; + } + } + + const name = `Strategy #${number}`; card.innerHTML = `
${name}
+ ${wfBadgeHTML}
@@ -750,10 +774,41 @@ function formatTime(seconds) { return `${minutes}m ${secs}s`; } +/** + * Update training/testing split display + */ +function updateTrainingSplit() { + const ratio = parseInt(document.getElementById('training-ratio').value); + document.getElementById('training-percent').textContent = ratio; + + // Update bar counts if data is loaded + if (appData.csvData) { + const totalBars = appData.csvData.length; + const trainingBars = Math.floor(totalBars * (ratio / 100)); + const testingBars = totalBars - trainingBars; + + document.getElementById('training-bars').textContent = trainingBars.toLocaleString(); + document.getElementById('testing-bars').textContent = testingBars.toLocaleString(); + } +} + +/** + * Toggle walk-forward settings visibility + */ +function toggleWalkForwardSettings() { + const enabled = document.getElementById('enable-walkforward').checked; + const settings = document.getElementById('walkforward-settings'); + if (settings) { + settings.style.display = enabled ? 'block' : 'none'; + } +} + // Explicitly expose functions to global scope for inline onclick handlers window.downloadStrategy = downloadStrategy; window.downloadAllStrategies = downloadAllStrategies; window.toggleStrategySelection = toggleStrategySelection; window.showComparison = showComparison; +window.updateTrainingSplit = updateTrainingSplit; +window.toggleWalkForwardSettings = toggleWalkForwardSettings; window.hideComparison = hideComparison; window.toggleCustomBarsInput = toggleCustomBarsInput; diff --git a/js/strategy.js b/js/strategy.js index eb7815c..95a8690 100644 --- a/js/strategy.js +++ b/js/strategy.js @@ -100,6 +100,8 @@ class Strategy { newStrategy.closeAtOpposite = this.closeAtOpposite; newStrategy.metrics = this.metrics ? JSON.parse(JSON.stringify(this.metrics)) : {}; newStrategy.fitness = this.fitness || 0; + // Copy walk-forward analysis results + newStrategy.walkForward = this.walkForward ? JSON.parse(JSON.stringify(this.walkForward)) : undefined; return newStrategy; } diff --git a/js/walk-forward.js b/js/walk-forward.js new file mode 100644 index 0000000..0412177 --- /dev/null +++ b/js/walk-forward.js @@ -0,0 +1,203 @@ +/** + * Walk-Forward Analysis Module + * Validates strategies on out-of-sample data to detect overfitting + */ + +class WalkForwardAnalysis { + constructor(data, trainingRatio = 0.7) { + this.data = data; + this.trainingRatio = trainingRatio; + this.splitIndex = Math.floor(data.length * trainingRatio); + } + + /** + * Split data into training and testing periods + * @returns {Object} { training: Array, testing: Array } + */ + split() { + return { + training: this.data.slice(0, this.splitIndex), + testing: this.data.slice(this.splitIndex), + splitIndex: this.splitIndex, + trainingBars: this.splitIndex, + testingBars: this.data.length - this.splitIndex + }; + } + + /** + * Run walk-forward analysis on a strategy + * @param {Object} strategy - Strategy to analyze + * @param {string} symbol - Trading symbol + * @returns {Object} Analysis results + */ + analyze(strategy, symbol = '') { + const { training, testing } = this.split(); + + // Backtest on training data + const trainingBacktest = new Backtester(training, strategy, symbol); + const trainingResults = trainingBacktest.run(); + + // Backtest on testing data + const testingBacktest = new Backtester(testing, strategy, symbol); + const testingResults = testingBacktest.run(); + + // Calculate degradation + const degradation = this.calculateDegradation(trainingResults, testingResults); + + // Calculate robustness score + const robustnessScore = this.calculateRobustness(degradation, trainingResults, testingResults); + + // Determine if strategy passed + const passed = robustnessScore >= 60; + + return { + training: trainingResults, + testing: testingResults, + degradation: degradation, + robustness: robustnessScore, + passed: passed, + splitInfo: { + trainingBars: training.length, + testingBars: testing.length, + trainingRatio: this.trainingRatio + } + }; + } + + /** + * Calculate performance degradation between training and testing + * @param {Object} train - Training results + * @param {Object} test - Testing results + * @returns {Object} Degradation metrics + */ + calculateDegradation(train, test) { + return { + profitFactor: this.calcPercentDegradation(train.profitFactor, test.profitFactor), + winRate: train.winRate - test.winRate, + maxDrawdown: test.maxDrawdown - train.maxDrawdown, + netProfit: this.calcPercentDegradation(train.netProfit, test.netProfit), + totalTrades: test.totalTrades, + avgWin: this.calcPercentDegradation(train.avgWin, test.avgWin), + avgLoss: this.calcPercentDegradation(train.avgLoss, test.avgLoss) + }; + } + + /** + * Calculate percentage degradation + * @param {number} trainValue - Training value + * @param {number} testValue - Testing value + * @returns {number} Degradation percentage + */ + calcPercentDegradation(trainValue, testValue) { + if (trainValue === 0) return 0; + return ((trainValue - testValue) / trainValue * 100); + } + + /** + * Calculate robustness score (0-100) + * Higher score = more robust strategy + * @param {Object} degradation - Degradation metrics + * @param {Object} train - Training results + * @param {Object} test - Testing results + * @returns {number} Robustness score + */ + calculateRobustness(degradation, train, test) { + // Profit Factor score (40% weight) + // Lower degradation = higher score + const pfDeg = Math.abs(degradation.profitFactor); + let pfScore = 100; + if (pfDeg > 50) pfScore = 0; + else if (pfDeg > 30) pfScore = 30; + else if (pfDeg > 15) pfScore = 60; + else pfScore = 100; + + // Win Rate score (30% weight) + const wrDeg = Math.abs(degradation.winRate); + let wrScore = 100; + if (wrDeg > 20) wrScore = 0; + else if (wrDeg > 10) wrScore = 40; + else if (wrDeg > 5) wrScore = 70; + else wrScore = 100; + + // Drawdown score (30% weight) + // For drawdown, increase is bad + const ddDeg = degradation.maxDrawdown; + let ddScore = 100; + if (ddDeg > 15) ddScore = 0; + else if (ddDeg > 10) ddScore = 40; + else if (ddDeg > 5) ddScore = 70; + else if (ddDeg < -5) ddScore = 100; // Better drawdown in testing + else ddScore = 85; + + // Additional checks + // Penalize if testing has too few trades + let tradesPenalty = 0; + if (test.totalTrades < 20) { + tradesPenalty = 20; + } else if (test.totalTrades < 30) { + tradesPenalty = 10; + } + + // Bonus if testing performs better + let bonus = 0; + if (test.profitFactor > train.profitFactor && test.winRate >= train.winRate) { + bonus = 10; + } + + // Weighted average + const score = (pfScore * 0.4) + (wrScore * 0.3) + (ddScore * 0.3) - tradesPenalty + bonus; + + return Math.max(0, Math.min(100, Math.round(score))); + } + + /** + * Get degradation severity level + * @param {number} value - Degradation value + * @param {boolean} inverse - If true, higher is worse (for drawdown) + * @returns {string} 'good', 'warning', or 'bad' + */ + getDegradationLevel(value, inverse = false) { + const absValue = Math.abs(value); + + if (inverse) { + // For drawdown increase + if (value < 0) return 'good'; // Improved + if (absValue < 5) return 'good'; + if (absValue < 10) return 'warning'; + return 'bad'; + } else { + // For profit factor, win rate degradation + if (absValue < 10) return 'good'; + if (absValue < 20) return 'warning'; + return 'bad'; + } + } + + /** + * Generate summary text for walk-forward results + * @param {Object} results - Walk-forward results + * @returns {string} Summary text + */ + generateSummary(results) { + const { robustness, passed, degradation } = results; + + if (passed) { + if (robustness >= 80) { + return 'Excellent! Strategy shows strong robustness with minimal overfitting.'; + } else if (robustness >= 70) { + return 'Good! Strategy performs well on out-of-sample data.'; + } else { + return 'Acceptable. Strategy shows reasonable robustness but monitor performance.'; + } + } else { + if (robustness < 40) { + return 'Warning! High overfitting risk. Strategy may not perform well in live trading.'; + } else { + return 'Caution. Strategy shows some overfitting. Consider re-optimization or more data.'; + } + } + } +} + +// Export for use in other modules +window.WalkForwardAnalysis = WalkForwardAnalysis; diff --git a/manual.html b/manual.html index dfcb1e2..3109e4e 100644 --- a/manual.html +++ b/manual.html @@ -191,6 +191,7 @@
  • Exporting Data from MT4/MT5
  • Uploading Data
  • Configuring Strategy Generation
  • +
  • Walk-Forward Analysis
  • Generating Strategies
  • Viewing Results
  • Downloading Strategies
  • @@ -338,6 +339,102 @@ +

    🔬 Walk-Forward Analysis

    +

    Walk-forward analysis validates strategies on out-of-sample data to detect overfitting and ensure real-world + performance.

    + +
    + What is Overfitting? A strategy that performs well on historical data but fails in live + trading due to being too closely fitted to past price patterns. +
    + +

    How It Works

    +
      +
    1. Data Splitting: Historical data is divided into training and testing periods
    2. +
    3. Training Period: Strategy is optimized on this data (e.g., 70% of bars)
    4. +
    5. Testing Period: Strategy is validated on unseen data (e.g., 30% of bars)
    6. +
    7. Performance Comparison: Metrics are compared between training and testing
    8. +
    9. Robustness Score: A score (0-100) indicates how well the strategy generalizes
    10. +
    + +

    Configuration

    + + +

    Understanding the Badges

    +

    Each strategy card displays a walk-forward badge showing its robustness:

    + + + + + + + + + + + + + + + + + + + + + + + +
    BadgeScore RangeMeaningRecommendation
    ✓ ROBUST (Green)60-100Strategy passed validationSafe to use for live trading
    âš  OVERFITTED (Orange)0-59Strategy failed validationAvoid - likely curve-fitted
    + +

    Robustness Score Interpretation

    + + +

    What Gets Measured

    +

    The robustness score is calculated based on degradation in:

    + + +
    + Important: A strategy with excellent training performance but poor testing performance is + likely overfitted and should be avoided. +
    + +

    Best Practices

    + + +

    Impact on Strategy Generation

    +

    When walk-forward is enabled:

    + +

    🎯 Generating Strategies

    1. Review your settings