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(
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

View File

@@ -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

View File

@@ -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;

View File

@@ -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 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 {p.MaxDrawdownPc:F1}% above 20% threshold applied {drawdownPenalty:F1}% penalty");
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
};
}
}