diff --git a/README.md b/README.md index 9a217c1..6f1e3ec 100644 --- a/README.md +++ b/README.md @@ -389,47 +389,50 @@ The backtest scoring system evaluates strategy performance using a comprehensive | Component | Weight | Description | |-----------|--------|-------------| -| **Growth Percentage** | 25% | Primary performance metric based on total return | -| **Sharpe Ratio** | 15% | Risk-adjusted return measure | -| **Max Drawdown (USD)** | 12% | Maximum capital loss in absolute terms | -| **Win Rate** | 15% | Percentage of profitable trades (weighted by trade count) | +| **Growth Percentage** | 20% | Primary performance metric based on total return | +| **Sharpe Ratio** | 15% | Risk-adjusted return measure (multiplied by 100 for accuracy) | +| **Hodl Comparison** | 10% | Performance vs buy-and-hold strategy | +| **Win Rate** | 12% | Percentage of profitable trades (weighted by trade count) | | **Profitability Bonus** | 8% | Additional reward for positive returns | -| **Hodl Comparison** | 5% | Performance vs buy-and-hold strategy | | **Trade Count** | 5% | Sufficient trading activity validation | | **Recovery Time** | 2% | Time to recover from maximum drawdown | | **Test Duration** | 3% | Adequate testing period validation | -| **Fees Impact** | 2% | Trading cost efficiency | +| **Fees Impact** | 2% | Trading cost efficiency (based on PnL ratio) | +| **Risk Adjusted Return** | 23% | PnL vs MaxDD ratio based on trading balance | #### Component Scoring Details -**Growth Percentage (25%)** -- **Negative Returns**: Linear penalty (20 + growth% × 1.5) -- **0-5%**: Linear scale (0-40 points) -- **5-10%**: Accelerated scale (40-100 points) -- **10%+**: Full score (100 points) +**Growth Percentage (20%)** +- **Negative Returns**: 0 points (no partial credit for losses) +- **0-5%**: Linear scale (0-20 points) +- **5-10%**: Accelerated scale (20-50 points) +- **10-20%**: Linear scale (50-100 points) +- **20%+**: Full score (100 points) **Sharpe Ratio (15%)** +- **Input**: Sharpe ratio is multiplied by 100 for more accurate scoring - **Negative**: 0 points - **0-4**: Linear scale (0-100 points) - **4+**: Full score (100 points) -**Max Drawdown USD (12%)** -- **0-30%**: Exponential penalty (100 - (drawdown%/30 × 100)^1.5) -- **30%+**: 0 points +**Hodl Comparison (10%)** +- **+5%+ better than HODL**: 100 points +- **+2-5% better**: 80-100 points +- **+0-2% better**: 40-80 points +- **-1-0% worse**: 20-40 points +- **-2 to -1% worse**: 0-20 points +- **-2%+ worse**: 0 points (handled by early exit) -**Win Rate (15%)** -- **Base Score**: Win rate percentage +**Win Rate (12%)** +- **Input**: Win rate as percentage (e.g., 23.45 for 23.45%) +- **Base Score**: Win rate converted to decimal (23.45% → 0.2345) - **Trade Count Factor**: Full significance at 55+ trades, reduced for fewer trades - **Minimum Trade Penalty**: 50% penalty for <10 trades **Profitability Bonus (8%)** -- **Positive Returns**: Logarithmic bonus (50 × (1 - 1/(1 + growth%/30))) +- **Positive Returns**: Logarithmic bonus (max 50 points) - **Negative Returns**: 0 points -**Hodl Comparison (5%)** -- **Outperforms Hodl**: 0-80 points based on margin -- **Underperforms Hodl**: 0-20 points based on underperformance - **Trade Count (5%)** - **<5 trades**: 0 points - **5-10 trades**: Linear scale (0-50 points) @@ -456,25 +459,41 @@ The backtest scoring system evaluates strategy performance using a comprehensive - **Optimal duration**: 3× minimum duration **Fees Impact (2%)** -- **0-2% fees**: Linear penalty (100-50 points) -- **2-5% fees**: Linear penalty (50-0 points) -- **5%+ fees**: 0 points +- **Based on**: Fees as percentage of PnL (not initial balance) +- **0-10% fees**: Linear scale (100-50 points) +- **10-20% fees**: Linear scale (50-0 points) +- **20%+ fees**: 0 points - **Fees > PnL**: 0 points +**Risk Adjusted Return (23%)** +- **Based on**: Trading balance (not initial balance) +- **Calculation**: PnL percentage vs MaxDD percentage ratio +- **>3:1 ratio**: 100 points +- **2-3:1 ratio**: 80-100 points +- **1.5-2:1 ratio**: 60-80 points +- **1-1.5:1 ratio**: 40-60 points +- **0.5-1:1 ratio**: 20-40 points +- **<0.5:1 ratio**: 0-20 points + #### Dynamic Penalty System The scoring system applies dynamic penalties based on performance thresholds: +**Early Exit Conditions** +- **No Trades**: Automatic 0 score +- **Negative PnL**: Automatic 0 score +- **HODL Underperformance**: Automatic 0 score if >2% worse than HODL + **Profitability Rules** - **Negative Growth**: 10% penalty per 1% loss -- **Negative Absolute PnL**: 70% penalty - **Low Win Rate**: 50% penalty per 10% below 30% (for 10+ trades) - **Low Profit**: 10% penalty per 1% below 2% (for 5+ trades) -- **High Drawdown**: 2% penalty per 1% above 20% +- **High Drawdown**: 2% penalty per 1% above 20% of trading balance +- **Poor Risk/Reward**: 20% penalty per 0.5 ratio above 1.5:1 (MaxDD/PnL) - **Short Test Duration**: 2% penalty per day below 30 days +- **HODL Underperformance**: 30% penalty per 1% underperformance **Special Rules** -- **No Positions**: Automatic 0 score - **Score Clamping**: Final score clamped between 0-100 - **Error Handling**: Returns 0 for any calculation errors @@ -482,11 +501,12 @@ The scoring system applies dynamic penalties based on performance thresholds: The system prioritizes: 1. **Consistent profitability** over high-risk gains -2. **Risk management** through drawdown control +2. **Risk management** through drawdown control based on trading balance 3. **Statistical significance** through adequate trade counts 4. **Timeframe-appropriate** expectations for recovery and duration -5. **Cost efficiency** through fee management +5. **Cost efficiency** through fee management relative to PnL 6. **Realistic performance** through dynamic penalties +7. **HODL outperformance** as a baseline requirement This comprehensive approach ensures that high-scoring strategies demonstrate robust, sustainable performance across multiple dimensions rather than relying on single metrics or short-term luck. diff --git a/src/Managing.Application/Shared/MessengerService.cs b/src/Managing.Application/Shared/MessengerService.cs index 5ca789f..89f28da 100644 --- a/src/Managing.Application/Shared/MessengerService.cs +++ b/src/Managing.Application/Shared/MessengerService.cs @@ -170,7 +170,8 @@ public class MessengerService : IMessengerService } } - public async Task SendGeneticAlgorithmNotification(GeneticRequest request, double bestFitness, object? bestChromosome) + public async Task SendGeneticAlgorithmNotification(GeneticRequest request, double bestFitness, + object? bestChromosome) { try { @@ -263,7 +264,7 @@ public class MessengerService : IMessengerService $"📊 Total Trades: {tradeCount}\n" + $"💰 Final PnL: ${finalPnl:F2}\n" + $"📈 Growth: {growthPercentage:F1}%\n" + - $"📉 Max Drawdown: ${maxDrawdown:C}\n" + + $"📉 Max Drawdown: ${maxDrawdown:N}\n" + $"📊 Sharpe Ratio: {sharpeRatio:F2}\n\n" + $"🆔 Backtest ID: {backtest.Id}"; @@ -272,11 +273,11 @@ public class MessengerService : IMessengerService private string BuildGeneticAlgorithmMessage(GeneticRequest request, double bestFitness, object? bestChromosome) { - var duration = request.CompletedAt.HasValue - ? request.CompletedAt.Value - request.CreatedAt + var duration = request.CompletedAt.HasValue + ? request.CompletedAt.Value - request.CreatedAt : TimeSpan.Zero; - var indicators = request.EligibleIndicators.Any() + var indicators = request.EligibleIndicators.Any() ? string.Join(", ", request.EligibleIndicators.Select(i => i.ToString())) : "N/A"; diff --git a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs index 3f93587..fd47b5a 100644 --- a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs +++ b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs @@ -98,16 +98,16 @@ public class BacktestScorer { return component switch { - "GrowthPercentage" => $"Growth of {p.GrowthPercentage:F2}% (target: 10% for full score)", - "SharpeRatio" => $"Sharpe ratio of {p.SharpeRatio:F2} (target: 4.0 for full score)", - "HodlComparison" => $"Strategy vs HODL: {p.GrowthPercentage:F2}% vs {p.HodlPercentage:F2}% (difference: {p.GrowthPercentage - p.HodlPercentage:F2}%)", - "WinRate" => $"Win rate of {p.WinRate:F2} with {p.TradeCount} trades", // Show as decimal - "ProfitabilityBonus" => $"Bonus for positive growth of {p.GrowthPercentage:F2}%", - "TradeCount" => $"{p.TradeCount} trades executed (minimum 5, optimal 50+)", - "RecoveryTime" => $"Recovery time: {p.MaxDrawdownRecoveryTime.TotalDays:F1} days", - "TestDuration" => $"Test duration: {(p.EndDate - p.StartDate).TotalDays:F1} days", - "FeesImpact" => $"Fees: ${p.Fees:F2} ({(p.TotalPnL > 0 ? p.Fees / p.TotalPnL * 100 : 0):F2}% of PnL)", - "RiskAdjustedReturn" => $"PnL/TradingBalance Drawdown ratio: {p.TotalPnL / (double)p.MaxDrawdown:F2}:1 (MaxDD: ${p.MaxDrawdown:F2} vs PnL: ${p.TotalPnL:F2})", + "GrowthPercentage" => $"Growth of {p.GrowthPercentage:F2}% (target: 20% for full score, 5% for 20 points, 10% for 50 points)", + "SharpeRatio" => $"Sharpe ratio of {p.SharpeRatio * 100:F2}, target: 4.0 for full score", + "HodlComparison" => $"Strategy vs HODL: {p.GrowthPercentage:F2}% vs {p.HodlPercentage:F2}% (difference: {p.GrowthPercentage - p.HodlPercentage:F2}%, target: +5% for full score)", + "WinRate" => $"Win rate of {p.WinRate:F2}% with {p.TradeCount} trades (significance factor: {Math.Min(1, (p.TradeCount - 5) / 50.0):F2})", + "ProfitabilityBonus" => $"Bonus for positive growth of {p.GrowthPercentage:F2}% (max 50 points)", + "TradeCount" => $"{p.TradeCount} trades executed (5-10: 0-50 points, 10-50: 50-100 points, 50+: 100 points)", + "RecoveryTime" => $"Recovery time: {p.MaxDrawdownRecoveryTime.TotalDays:F1} days (timeframe-adjusted max: {GetMaxRecoveryDays(p.Timeframe):F1} days)", + "TestDuration" => $"Test duration: {(p.EndDate - p.StartDate).TotalDays:F1} days (min: {GetMinTestDays(p.Timeframe):F1}, optimal: {GetMinTestDays(p.Timeframe) * 3:F1} days)", + "FeesImpact" => $"Fees: ${p.Fees:F2} ({(p.TotalPnL > 0 ? p.Fees / p.TotalPnL * 100 : 0):F2}% of PnL, target: below 10% for full score)", + "RiskAdjustedReturn" => $"Risk/Reward: {p.TotalPnL / (double)p.MaxDrawdown:F2}:1 (PnL: ${p.TotalPnL:F2}, MaxDD: ${p.MaxDrawdown:F2}, target: >3:1 for full score)", _ => $"Component score: {score:F1}" }; } @@ -126,12 +126,12 @@ public class BacktestScorer } // 3. Win Rate Validation (Dynamic) - if (p.WinRate < 0.3 && p.TradeCount > 10) + if (p.WinRate < 30 && p.TradeCount > 10) // winRate is percentage, so 30 = 30% { - var winRatePenalty = (0.3 - p.WinRate) * 0.5; // 50% penalty per 10% below 30% + var winRatePenalty = (30 - p.WinRate) * 0.5; // 50% penalty per 10% below 30% var newMultiplier = Math.Max(0.2, 1 - winRatePenalty); penaltyMultiplier *= newMultiplier; - result.AddPenaltyCheck("Low Win Rate", newMultiplier, $"Win rate of {p.WinRate * 100:F1}% below 30% threshold applied {winRatePenalty:F1}% penalty"); + result.AddPenaltyCheck("Low Win Rate", newMultiplier, $"Win rate of {p.WinRate:F2}% below 30% threshold applied {winRatePenalty:F1}% penalty"); } // 4. Minimum Profit Threshold (Dynamic) @@ -246,8 +246,9 @@ public class BacktestScorer private static double CalculateWinRateScore(double winRate, int tradeCount, BacktestScoringResult result) { - // Use winRate as a decimal (e.g., 0.55 for 55%) - var baseScore = winRate; + // winRate is passed as percentage (e.g., 23.45 for 23.45%), convert to decimal for scoring + var winRateDecimal = winRate / 100.0; + var baseScore = winRateDecimal; // Significance factor - more aggressive var significanceFactor = Math.Min(1, (tradeCount - 5) / 50.0); // Start at 5 trades, full significance at 55 trades @@ -379,4 +380,33 @@ public class BacktestScorer _ => Math.Max(0, riskRewardRatio * 40) // 0-0.5:1: 0-20 points }; } + + // Helper methods for message generation + private static double GetMaxRecoveryDays(Timeframe timeframe) + { + return timeframe switch + { + Timeframe.FiveMinutes => 3.0, + Timeframe.FifteenMinutes => 5.0, + Timeframe.ThirtyMinutes => 10.0, + Timeframe.OneHour => 15.0, + Timeframe.FourHour => 30.0, + Timeframe.OneDay => 90.0, + _ => 30.0 + }; + } + + private static double GetMinTestDays(Timeframe timeframe) + { + return timeframe switch + { + Timeframe.FiveMinutes => 14.0, + Timeframe.FifteenMinutes => 28.0, + Timeframe.ThirtyMinutes => 56.0, + Timeframe.OneHour => 84.0, + Timeframe.FourHour => 120.0, + Timeframe.OneDay => 90.0, + _ => 21.0 + }; + } } \ No newline at end of file