diff --git a/src/Managing.Application.Tests/BotsTests.cs b/src/Managing.Application.Tests/BotsTests.cs index 38c2573..ac7f5dc 100644 --- a/src/Managing.Application.Tests/BotsTests.cs +++ b/src/Managing.Application.Tests/BotsTests.cs @@ -676,7 +676,6 @@ namespace Managing.Application.Tests var scoringParams = new BacktestScoringParams( sharpeRatio: (double)(backtestResult.Statistics?.SharpeRatio ?? 0), - maxDrawdownPc: (double)(backtestResult.Statistics?.MaxDrawdownPc ?? 0), growthPercentage: (double)backtestResult.GrowthPercentage, hodlPercentage: (double)backtestResult.HodlPercentage, winRate: backtestResult.WinRate / 100.0, // Convert percentage to decimal @@ -686,6 +685,7 @@ namespace Managing.Application.Tests maxDrawdownRecoveryTime: backtestResult.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero, maxDrawdown: backtestResult.Statistics?.MaxDrawdown ?? 0, initialBalance: config.BotTradingBalance, + tradingBalance: config.BotTradingBalance, startDate: backtestResult.StartDate, endDate: backtestResult.EndDate, timeframe: config.Timeframe diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index c4153eb..a96294c 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -302,7 +302,6 @@ namespace Managing.Application.Backtesting var fees = bot.GetTotalFees(); var scoringParams = new BacktestScoringParams( sharpeRatio: (double)stats.SharpeRatio, - maxDrawdownPc: (double)stats.MaxDrawdownPc, growthPercentage: (double)growthPercentage, hodlPercentage: (double)hodlPercentage, winRate: winRate, @@ -311,7 +310,8 @@ namespace Managing.Application.Backtesting tradeCount: bot.Positions.Count, maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime, maxDrawdown: stats.MaxDrawdown, - initialBalance: config.BotTradingBalance, + initialBalance: bot.WalletBalances.FirstOrDefault().Value, + tradingBalance: config.BotTradingBalance, startDate: candles[0].Date, endDate: candles.Last().Date, timeframe: config.Timeframe diff --git a/src/Managing.Domain/Backtests/BacktestScoringParams.cs b/src/Managing.Domain/Backtests/BacktestScoringParams.cs index 80efb92..fcef9b5 100644 --- a/src/Managing.Domain/Backtests/BacktestScoringParams.cs +++ b/src/Managing.Domain/Backtests/BacktestScoringParams.cs @@ -5,7 +5,6 @@ namespace Managing.Domain.Backtests; public class BacktestScoringParams { public double SharpeRatio { get; } - public double MaxDrawdownPc { get; } public double GrowthPercentage { get; } public double HodlPercentage { get; } public double WinRate { get; } @@ -17,14 +16,13 @@ public class BacktestScoringParams // New properties for enhanced scoring public decimal MaxDrawdown { get; } public decimal InitialBalance { get; } + public decimal TradingBalance { get; } public DateTime StartDate { get; } public DateTime EndDate { get; } - public decimal FeesPaid { get; } public Timeframe Timeframe { get; } public BacktestScoringParams( double sharpeRatio, - double maxDrawdownPc, double growthPercentage, double hodlPercentage, double winRate, @@ -34,12 +32,12 @@ public class BacktestScoringParams TimeSpan maxDrawdownRecoveryTime, decimal maxDrawdown = 0, decimal initialBalance = 0, + decimal tradingBalance = 0, DateTime startDate = default, DateTime endDate = default, Timeframe timeframe = Timeframe.OneHour) { SharpeRatio = sharpeRatio; - MaxDrawdownPc = maxDrawdownPc; GrowthPercentage = growthPercentage; HodlPercentage = hodlPercentage; WinRate = winRate; @@ -49,6 +47,7 @@ public class BacktestScoringParams MaxDrawdownRecoveryTime = maxDrawdownRecoveryTime; MaxDrawdown = maxDrawdown; InitialBalance = initialBalance; + TradingBalance = tradingBalance; StartDate = startDate; EndDate = endDate; Timeframe = timeframe; diff --git a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs index 6d307f3..3f93587 100644 --- a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs +++ b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs @@ -8,7 +8,6 @@ public class BacktestScorer { { "GrowthPercentage", 0.20 }, { "SharpeRatio", 0.15 }, - { "MaxDrawdownUsd", 0.15 }, { "HodlComparison", 0.10 }, // Increased from 0.05 to 0.15 { "WinRate", 0.12 }, { "ProfitabilityBonus", 0.08 }, @@ -16,7 +15,7 @@ public class BacktestScorer { "RecoveryTime", 0.02 }, { "TestDuration", 0.03 }, { "FeesImpact", 0.02 }, - { "RiskAdjustedReturn", 0.08 } // New component + { "RiskAdjustedReturn", 0.23 } // Increased from 0.08 to 0.23 (absorbed MaxDrawdownUsd weight) }; public static double CalculateTotalScore(BacktestScoringParams p) @@ -73,15 +72,14 @@ public class BacktestScorer { { "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage, result) }, { "SharpeRatio", CalculateSharpeScore(p.SharpeRatio, result) }, - { "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance, result) }, { "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage, result) }, { "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount, result) }, { "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage, result) }, { "TradeCount", CalculateTradeCountScore(p.TradeCount, result) }, { "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe, result) }, { "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe, result) }, - { "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL, result) }, - { "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.InitialBalance, result) } + { "FeesImpact", CalculateFeesImpactScore(p.Fees, p.TotalPnL, result) }, + { "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.TradingBalance, result) } }; var totalScore = componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]); @@ -102,15 +100,14 @@ public class BacktestScorer { "GrowthPercentage" => $"Growth of {p.GrowthPercentage:F2}% (target: 10% for full score)", "SharpeRatio" => $"Sharpe ratio of {p.SharpeRatio:F2} (target: 4.0 for full score)", - "MaxDrawdownUsd" => $"Max drawdown of ${p.MaxDrawdown:F2} ({p.MaxDrawdownPc:F1}% of balance)", "HodlComparison" => $"Strategy vs HODL: {p.GrowthPercentage:F2}% vs {p.HodlPercentage:F2}% (difference: {p.GrowthPercentage - p.HodlPercentage:F2}%)", - "WinRate" => $"Win rate of {p.WinRate * 100:F1}% with {p.TradeCount} trades", + "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.FeesPaid:F2} ({p.FeesPaid / p.InitialBalance * 100:F2}% of balance)", - "RiskAdjustedReturn" => $"PnL/Drawdown ratio: {p.TotalPnL / (double)p.MaxDrawdown:F2}:1", + "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})", _ => $"Component score: {score:F1}" }; } @@ -147,12 +144,16 @@ public class BacktestScorer } // 5. Drawdown Penalty (Dynamic) - Enhanced to consider PnL ratio - if (p.MaxDrawdownPc > 20) + if (p.TradingBalance > 0 && p.MaxDrawdown > 0) { - var drawdownPenalty = (p.MaxDrawdownPc - 20) * 0.02; // 2% penalty per 1% above 20% - var newMultiplier = Math.Max(0.3, 1 - drawdownPenalty); - penaltyMultiplier *= newMultiplier; - result.AddPenaltyCheck("High Drawdown", newMultiplier, $"Drawdown of {p.MaxDrawdownPc:F1}% above 20% threshold applied {drawdownPenalty:F1}% penalty"); + var drawdownPercentage = (double)(p.MaxDrawdown / p.TradingBalance * 100); + if (drawdownPercentage > 20) + { + var drawdownPenalty = (drawdownPercentage - 20) * 0.02; // 2% penalty per 1% above 20% + var newMultiplier = Math.Max(0.3, 1 - drawdownPenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("High Drawdown", newMultiplier, $"Drawdown of {drawdownPercentage:F1}% above 20% threshold applied {drawdownPenalty:F1}% penalty"); + } } // 6. Enhanced Drawdown vs PnL Penalty @@ -193,17 +194,17 @@ public class BacktestScorer private static double CalculateGrowthScore(double growthPercentage, BacktestScoringResult result) { - // More aggressive scoring - harder to reach 100 + // Negative growth always scores 0 if (growthPercentage < 0) { - return Math.Max(0, 20 + (growthPercentage * 1.5)); // -10% → 5, -20% → 0 + return 0; } - // Require minimum 10% growth for full score (increased from 5%) return growthPercentage switch { - < 5 => growthPercentage * 8, // 2% → 16, 4% → 32 - < 10 => 40 + (growthPercentage - 5) * 12, // 5% → 40, 7% → 64, 9% → 88 + < 5 => growthPercentage * 4, // 2% → 8, 4% → 16 + < 10 => 20 + (growthPercentage - 5) * 6, // 5% → 20, 7% → 32, 9% → 44 + < 20 => 50 + (growthPercentage - 10) * 5, // 10% → 50, 15% → 75, 19% → 95 _ => 100 }; } @@ -230,30 +231,23 @@ public class BacktestScorer private static double CalculateSharpeScore(double sharpeRatio, BacktestScoringResult result) { - return sharpeRatio switch + // Multiply Sharpe ratio by 100 for more accurate scoring + var adjustedSharpeRatio = sharpeRatio * 100; + + return adjustedSharpeRatio switch { < 0 => 0, > 4 => 100, // Increased threshold from 3 to 4 - _ => (sharpeRatio / 4) * 100 + _ => (adjustedSharpeRatio / 4) * 100 }; } - private static double CalculateDrawdownUsdScore(decimal maxDrawdown, decimal initialBalance, BacktestScoringResult result) - { - if (initialBalance <= 0) return 0; - - var drawdownPercentage = (double)(maxDrawdown / initialBalance * 100); - return drawdownPercentage switch - { - > 30 => 0, // 30% drawdown in USD = 0 score - _ => 100 - Math.Pow(drawdownPercentage / 30 * 100, 1.5) / 100 - }; - } + private static double CalculateWinRateScore(double winRate, int tradeCount, BacktestScoringResult result) { - // Base win rate score - var baseScore = winRate * 100; + // Use winRate as a decimal (e.g., 0.55 for 55%) + var baseScore = winRate; // Significance factor - more aggressive var significanceFactor = Math.Min(1, (tradeCount - 5) / 50.0); // Start at 5 trades, full significance at 55 trades @@ -338,45 +332,51 @@ public class BacktestScorer return 100; } - private static double CalculateFeesImpactScore(decimal feesPaid, decimal initialBalance, decimal totalPnL, BacktestScoringResult result) + private static double CalculateFeesImpactScore(double fees, double totalPnL, BacktestScoringResult result) { - if (initialBalance <= 0) return 0; + if (totalPnL <= 0) return 0; - var feesPercentage = (double)(feesPaid / initialBalance * 100); - var pnlPercentage = (double)(totalPnL / initialBalance * 100); + var feesPercentage = fees / totalPnL * 100; // If fees are higher than PnL, heavy penalty - if (feesPaid > totalPnL && totalPnL > 0) + if (fees > totalPnL) { return 0; } - // Fee efficiency score + // Fee efficiency score based on PnL var feeEfficiency = feesPercentage switch { - > 5 => 0, // More than 5% fees = 0 - > 2 => 50 - (feesPercentage - 2) * 16.67, // 2-5%: 50-0 points - _ => 100 - feesPercentage * 25 // 0-2%: 100-50 points + > 20 => 0, // More than 20% fees = 0 + > 10 => 50 - (feesPercentage - 10) * 5, // 10-20%: 50-0 points + _ => 100 - feesPercentage * 5 // 0-10%: 100-50 points }; return feeEfficiency; } - private static double CalculateRiskAdjustedReturnScore(double totalPnL, decimal maxDrawdown, decimal initialBalance, BacktestScoringResult result) + private static double CalculateRiskAdjustedReturnScore(double totalPnL, decimal maxDrawdown, decimal tradingBalance, BacktestScoringResult result) { - if (initialBalance <= 0 || maxDrawdown <= 0) return 0; + if (tradingBalance <= 0 || maxDrawdown <= 0) return 0; - var pnlRatio = totalPnL / (double)maxDrawdown; + // Calculate drawdown as percentage of trading balance + var drawdownPercentage = (double)(maxDrawdown / tradingBalance * 100); - // Score based on PnL to drawdown ratio - return pnlRatio switch + // Calculate PnL as percentage of trading balance + var pnlPercentage = (double)((decimal)totalPnL / tradingBalance * 100); + + // Score based on PnL percentage vs drawdown percentage ratio + var riskRewardRatio = pnlPercentage / drawdownPercentage; + + // Score based on risk-adjusted return (PnL vs Drawdown based on trading balance) + return riskRewardRatio switch { > 3 => 100, // Excellent risk-adjusted return (>3:1) - > 2 => 80 + (pnlRatio - 2) * 20, // 2-3:1: 80-100 points - > 1.5 => 60 + (pnlRatio - 1.5) * 40, // 1.5-2:1: 60-80 points - > 1 => 40 + (pnlRatio - 1) * 40, // 1-1.5:1: 40-60 points - > 0.5 => 20 + (pnlRatio - 0.5) * 40, // 0.5-1:1: 20-40 points - _ => Math.Max(0, pnlRatio * 40) // 0-0.5:1: 0-20 points + > 2 => 80 + (riskRewardRatio - 2) * 20, // 2-3:1: 80-100 points + > 1.5 => 60 + (riskRewardRatio - 1.5) * 40, // 1.5-2:1: 60-80 points + > 1 => 40 + (riskRewardRatio - 1) * 40, // 1-1.5:1: 40-60 points + > 0.5 => 20 + (riskRewardRatio - 0.5) * 40, // 0.5-1:1: 20-40 points + _ => Math.Max(0, riskRewardRatio * 40) // 0-0.5:1: 0-20 points }; } } \ No newline at end of file