Update scoring

This commit is contained in:
2025-07-17 21:53:19 +07:00
parent 7de18673f9
commit 7bf7f783a9
4 changed files with 59 additions and 60 deletions

View File

@@ -676,7 +676,6 @@ namespace Managing.Application.Tests
var scoringParams = new BacktestScoringParams( var scoringParams = new BacktestScoringParams(
sharpeRatio: (double)(backtestResult.Statistics?.SharpeRatio ?? 0), sharpeRatio: (double)(backtestResult.Statistics?.SharpeRatio ?? 0),
maxDrawdownPc: (double)(backtestResult.Statistics?.MaxDrawdownPc ?? 0),
growthPercentage: (double)backtestResult.GrowthPercentage, growthPercentage: (double)backtestResult.GrowthPercentage,
hodlPercentage: (double)backtestResult.HodlPercentage, hodlPercentage: (double)backtestResult.HodlPercentage,
winRate: backtestResult.WinRate / 100.0, // Convert percentage to decimal winRate: backtestResult.WinRate / 100.0, // Convert percentage to decimal
@@ -686,6 +685,7 @@ namespace Managing.Application.Tests
maxDrawdownRecoveryTime: backtestResult.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero, maxDrawdownRecoveryTime: backtestResult.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
maxDrawdown: backtestResult.Statistics?.MaxDrawdown ?? 0, maxDrawdown: backtestResult.Statistics?.MaxDrawdown ?? 0,
initialBalance: config.BotTradingBalance, initialBalance: config.BotTradingBalance,
tradingBalance: config.BotTradingBalance,
startDate: backtestResult.StartDate, startDate: backtestResult.StartDate,
endDate: backtestResult.EndDate, endDate: backtestResult.EndDate,
timeframe: config.Timeframe timeframe: config.Timeframe

View File

@@ -302,7 +302,6 @@ namespace Managing.Application.Backtesting
var fees = bot.GetTotalFees(); var fees = bot.GetTotalFees();
var scoringParams = new BacktestScoringParams( var scoringParams = new BacktestScoringParams(
sharpeRatio: (double)stats.SharpeRatio, sharpeRatio: (double)stats.SharpeRatio,
maxDrawdownPc: (double)stats.MaxDrawdownPc,
growthPercentage: (double)growthPercentage, growthPercentage: (double)growthPercentage,
hodlPercentage: (double)hodlPercentage, hodlPercentage: (double)hodlPercentage,
winRate: winRate, winRate: winRate,
@@ -311,7 +310,8 @@ namespace Managing.Application.Backtesting
tradeCount: bot.Positions.Count, tradeCount: bot.Positions.Count,
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime, maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
maxDrawdown: stats.MaxDrawdown, maxDrawdown: stats.MaxDrawdown,
initialBalance: config.BotTradingBalance, initialBalance: bot.WalletBalances.FirstOrDefault().Value,
tradingBalance: config.BotTradingBalance,
startDate: candles[0].Date, startDate: candles[0].Date,
endDate: candles.Last().Date, endDate: candles.Last().Date,
timeframe: config.Timeframe timeframe: config.Timeframe

View File

@@ -5,7 +5,6 @@ namespace Managing.Domain.Backtests;
public class BacktestScoringParams public class BacktestScoringParams
{ {
public double SharpeRatio { get; } public double SharpeRatio { get; }
public double MaxDrawdownPc { get; }
public double GrowthPercentage { get; } public double GrowthPercentage { get; }
public double HodlPercentage { get; } public double HodlPercentage { get; }
public double WinRate { get; } public double WinRate { get; }
@@ -17,14 +16,13 @@ public class BacktestScoringParams
// New properties for enhanced scoring // New properties for enhanced scoring
public decimal MaxDrawdown { get; } public decimal MaxDrawdown { get; }
public decimal InitialBalance { get; } public decimal InitialBalance { get; }
public decimal TradingBalance { get; }
public DateTime StartDate { get; } public DateTime StartDate { get; }
public DateTime EndDate { get; } public DateTime EndDate { get; }
public decimal FeesPaid { get; }
public Timeframe Timeframe { get; } public Timeframe Timeframe { get; }
public BacktestScoringParams( public BacktestScoringParams(
double sharpeRatio, double sharpeRatio,
double maxDrawdownPc,
double growthPercentage, double growthPercentage,
double hodlPercentage, double hodlPercentage,
double winRate, double winRate,
@@ -34,12 +32,12 @@ public class BacktestScoringParams
TimeSpan maxDrawdownRecoveryTime, TimeSpan maxDrawdownRecoveryTime,
decimal maxDrawdown = 0, decimal maxDrawdown = 0,
decimal initialBalance = 0, decimal initialBalance = 0,
decimal tradingBalance = 0,
DateTime startDate = default, DateTime startDate = default,
DateTime endDate = default, DateTime endDate = default,
Timeframe timeframe = Timeframe.OneHour) Timeframe timeframe = Timeframe.OneHour)
{ {
SharpeRatio = sharpeRatio; SharpeRatio = sharpeRatio;
MaxDrawdownPc = maxDrawdownPc;
GrowthPercentage = growthPercentage; GrowthPercentage = growthPercentage;
HodlPercentage = hodlPercentage; HodlPercentage = hodlPercentage;
WinRate = winRate; WinRate = winRate;
@@ -49,6 +47,7 @@ public class BacktestScoringParams
MaxDrawdownRecoveryTime = maxDrawdownRecoveryTime; MaxDrawdownRecoveryTime = maxDrawdownRecoveryTime;
MaxDrawdown = maxDrawdown; MaxDrawdown = maxDrawdown;
InitialBalance = initialBalance; InitialBalance = initialBalance;
TradingBalance = tradingBalance;
StartDate = startDate; StartDate = startDate;
EndDate = endDate; EndDate = endDate;
Timeframe = timeframe; Timeframe = timeframe;

View File

@@ -8,7 +8,6 @@ public class BacktestScorer
{ {
{ "GrowthPercentage", 0.20 }, { "GrowthPercentage", 0.20 },
{ "SharpeRatio", 0.15 }, { "SharpeRatio", 0.15 },
{ "MaxDrawdownUsd", 0.15 },
{ "HodlComparison", 0.10 }, // Increased from 0.05 to 0.15 { "HodlComparison", 0.10 }, // Increased from 0.05 to 0.15
{ "WinRate", 0.12 }, { "WinRate", 0.12 },
{ "ProfitabilityBonus", 0.08 }, { "ProfitabilityBonus", 0.08 },
@@ -16,7 +15,7 @@ public class BacktestScorer
{ "RecoveryTime", 0.02 }, { "RecoveryTime", 0.02 },
{ "TestDuration", 0.03 }, { "TestDuration", 0.03 },
{ "FeesImpact", 0.02 }, { "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) public static double CalculateTotalScore(BacktestScoringParams p)
@@ -73,15 +72,14 @@ public class BacktestScorer
{ {
{ "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage, result) }, { "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage, result) },
{ "SharpeRatio", CalculateSharpeScore(p.SharpeRatio, result) }, { "SharpeRatio", CalculateSharpeScore(p.SharpeRatio, result) },
{ "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance, result) },
{ "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage, result) }, { "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage, result) },
{ "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount, result) }, { "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount, result) },
{ "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage, result) }, { "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage, result) },
{ "TradeCount", CalculateTradeCountScore(p.TradeCount, result) }, { "TradeCount", CalculateTradeCountScore(p.TradeCount, result) },
{ "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe, result) }, { "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe, result) },
{ "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe, result) }, { "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe, result) },
{ "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL, result) }, { "FeesImpact", CalculateFeesImpactScore(p.Fees, p.TotalPnL, result) },
{ "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.InitialBalance, result) } { "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.TradingBalance, result) }
}; };
var totalScore = componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]); 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)", "GrowthPercentage" => $"Growth of {p.GrowthPercentage:F2}% (target: 10% for full score)",
"SharpeRatio" => $"Sharpe ratio of {p.SharpeRatio:F2} (target: 4.0 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}%)", "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}%", "ProfitabilityBonus" => $"Bonus for positive growth of {p.GrowthPercentage:F2}%",
"TradeCount" => $"{p.TradeCount} trades executed (minimum 5, optimal 50+)", "TradeCount" => $"{p.TradeCount} trades executed (minimum 5, optimal 50+)",
"RecoveryTime" => $"Recovery time: {p.MaxDrawdownRecoveryTime.TotalDays:F1} days", "RecoveryTime" => $"Recovery time: {p.MaxDrawdownRecoveryTime.TotalDays:F1} days",
"TestDuration" => $"Test duration: {(p.EndDate - p.StartDate).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)", "FeesImpact" => $"Fees: ${p.Fees:F2} ({(p.TotalPnL > 0 ? p.Fees / p.TotalPnL * 100 : 0):F2}% of PnL)",
"RiskAdjustedReturn" => $"PnL/Drawdown ratio: {p.TotalPnL / (double)p.MaxDrawdown:F2}:1", "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}" _ => $"Component score: {score:F1}"
}; };
} }
@@ -147,12 +144,16 @@ public class BacktestScorer
} }
// 5. Drawdown Penalty (Dynamic) - Enhanced to consider PnL ratio // 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 drawdownPercentage = (double)(p.MaxDrawdown / p.TradingBalance * 100);
var newMultiplier = Math.Max(0.3, 1 - drawdownPenalty); if (drawdownPercentage > 20)
penaltyMultiplier *= newMultiplier; {
result.AddPenaltyCheck("High Drawdown", newMultiplier, $"Drawdown of {p.MaxDrawdownPc:F1}% above 20% threshold applied {drawdownPenalty:F1}% penalty"); 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 // 6. Enhanced Drawdown vs PnL Penalty
@@ -193,17 +194,17 @@ public class BacktestScorer
private static double CalculateGrowthScore(double growthPercentage, BacktestScoringResult result) private static double CalculateGrowthScore(double growthPercentage, BacktestScoringResult result)
{ {
// More aggressive scoring - harder to reach 100 // Negative growth always scores 0
if (growthPercentage < 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 return growthPercentage switch
{ {
< 5 => growthPercentage * 8, // 2% → 16, 4% → 32 < 5 => growthPercentage * 4, // 2% → 8, 4% → 16
< 10 => 40 + (growthPercentage - 5) * 12, // 5% → 40, 7% → 64, 9% → 88 < 10 => 20 + (growthPercentage - 5) * 6, // 5% → 20, 7% → 32, 9% → 44
< 20 => 50 + (growthPercentage - 10) * 5, // 10% → 50, 15% → 75, 19% → 95
_ => 100 _ => 100
}; };
} }
@@ -230,30 +231,23 @@ public class BacktestScorer
private static double CalculateSharpeScore(double sharpeRatio, BacktestScoringResult result) 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, < 0 => 0,
> 4 => 100, // Increased threshold from 3 to 4 > 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) private static double CalculateWinRateScore(double winRate, int tradeCount, BacktestScoringResult result)
{ {
// Base win rate score // Use winRate as a decimal (e.g., 0.55 for 55%)
var baseScore = winRate * 100; var baseScore = winRate;
// Significance factor - more aggressive // Significance factor - more aggressive
var significanceFactor = Math.Min(1, (tradeCount - 5) / 50.0); // Start at 5 trades, full significance at 55 trades 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; 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 feesPercentage = fees / totalPnL * 100;
var pnlPercentage = (double)(totalPnL / initialBalance * 100);
// If fees are higher than PnL, heavy penalty // If fees are higher than PnL, heavy penalty
if (feesPaid > totalPnL && totalPnL > 0) if (fees > totalPnL)
{ {
return 0; return 0;
} }
// Fee efficiency score // Fee efficiency score based on PnL
var feeEfficiency = feesPercentage switch var feeEfficiency = feesPercentage switch
{ {
> 5 => 0, // More than 5% fees = 0 > 20 => 0, // More than 20% fees = 0
> 2 => 50 - (feesPercentage - 2) * 16.67, // 2-5%: 50-0 points > 10 => 50 - (feesPercentage - 10) * 5, // 10-20%: 50-0 points
_ => 100 - feesPercentage * 25 // 0-2%: 100-50 points _ => 100 - feesPercentage * 5 // 0-10%: 100-50 points
}; };
return feeEfficiency; 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 // Calculate PnL as percentage of trading balance
return pnlRatio switch 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) > 3 => 100, // Excellent risk-adjusted return (>3:1)
> 2 => 80 + (pnlRatio - 2) * 20, // 2-3:1: 80-100 points > 2 => 80 + (riskRewardRatio - 2) * 20, // 2-3:1: 80-100 points
> 1.5 => 60 + (pnlRatio - 1.5) * 40, // 1.5-2:1: 60-80 points > 1.5 => 60 + (riskRewardRatio - 1.5) * 40, // 1.5-2:1: 60-80 points
> 1 => 40 + (pnlRatio - 1) * 40, // 1-1.5:1: 40-60 points > 1 => 40 + (riskRewardRatio - 1) * 40, // 1-1.5:1: 40-60 points
> 0.5 => 20 + (pnlRatio - 0.5) * 40, // 0.5-1:1: 20-40 points > 0.5 => 20 + (riskRewardRatio - 0.5) * 40, // 0.5-1:1: 20-40 points
_ => Math.Max(0, pnlRatio * 40) // 0-0.5:1: 0-20 points _ => Math.Max(0, riskRewardRatio * 40) // 0-0.5:1: 0-20 points
}; };
} }
} }