Add scoring message
This commit is contained in:
@@ -21,58 +21,101 @@ public class BacktestScorer
|
||||
|
||||
public static double CalculateTotalScore(BacktestScoringParams p)
|
||||
{
|
||||
var result = CalculateDetailedScore(p);
|
||||
return result.Score;
|
||||
}
|
||||
|
||||
public static BacktestScoringResult CalculateDetailedScore(BacktestScoringParams p)
|
||||
{
|
||||
var result = new BacktestScoringResult(0);
|
||||
|
||||
try
|
||||
{
|
||||
// Early exit for no positions
|
||||
if (p.TradeCount == 0)
|
||||
{
|
||||
return 0;
|
||||
result.AddEarlyExitCheck("No Trades", "No trading positions were taken during the backtest period. A strategy must execute trades to be evaluated.");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Early exit for negative PnL - should be 0 score
|
||||
if (p.TotalPnL <= 0)
|
||||
{
|
||||
return 0;
|
||||
result.AddEarlyExitCheck("Negative PnL", $"Total profit/loss is negative (${p.TotalPnL:F2}). Only profitable strategies receive scores.");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Early exit if strategy significantly underperforms HODL (more than 2% worse)
|
||||
if (p.GrowthPercentage < p.HodlPercentage - 2)
|
||||
{
|
||||
return 0;
|
||||
result.AddEarlyExitCheck("HODL Underperformance", $"Strategy growth ({p.GrowthPercentage:F2}%) significantly underperforms HODL ({p.HodlPercentage:F2}%) by more than 2%. Buy-and-hold would have been more profitable.");
|
||||
return result;
|
||||
}
|
||||
|
||||
var baseScore = CalculateBaseScore(p);
|
||||
var finalScore = ApplyProfitabilityRules(baseScore, p);
|
||||
var baseScore = CalculateBaseScore(p, result);
|
||||
var finalScore = ApplyProfitabilityRules(baseScore, p, result);
|
||||
|
||||
return double.Clamp(finalScore, 0, 100);
|
||||
result.Score = double.Clamp(finalScore, 0, 100);
|
||||
result.SummaryMessage = result.GenerateSummaryMessage();
|
||||
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
return 0;
|
||||
result.AddEarlyExitCheck("Calculation Error", $"An error occurred during score calculation: {ex.Message}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static double CalculateBaseScore(BacktestScoringParams p)
|
||||
private static double CalculateBaseScore(BacktestScoringParams p, BacktestScoringResult result)
|
||||
{
|
||||
var componentScores = new Dictionary<string, double>
|
||||
{
|
||||
{ "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage) },
|
||||
{ "SharpeRatio", CalculateSharpeScore(p.SharpeRatio) },
|
||||
{ "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance) },
|
||||
{ "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage) },
|
||||
{ "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount) },
|
||||
{ "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage) },
|
||||
{ "TradeCount", CalculateTradeCountScore(p.TradeCount) },
|
||||
{ "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe) },
|
||||
{ "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe) },
|
||||
{ "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL) },
|
||||
{ "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.InitialBalance) }
|
||||
{ "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) }
|
||||
};
|
||||
|
||||
return componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]);
|
||||
var totalScore = componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]);
|
||||
|
||||
// Add component scores to result
|
||||
foreach (var kvp in componentScores)
|
||||
{
|
||||
var passed = kvp.Value > 0;
|
||||
result.AddCheck(kvp.Key, kvp.Value, Weights[kvp.Key], GetComponentMessage(kvp.Key, kvp.Value, p), passed);
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
private static double ApplyProfitabilityRules(double baseScore, BacktestScoringParams p)
|
||||
private static string GetComponentMessage(string component, double score, BacktestScoringParams p)
|
||||
{
|
||||
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)",
|
||||
"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",
|
||||
"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",
|
||||
_ => $"Component score: {score:F1}"
|
||||
};
|
||||
}
|
||||
|
||||
private static double ApplyProfitabilityRules(double baseScore, BacktestScoringParams p, BacktestScoringResult result)
|
||||
{
|
||||
var penaltyMultiplier = 1.0;
|
||||
|
||||
@@ -80,30 +123,36 @@ public class BacktestScorer
|
||||
if (p.GrowthPercentage < 0)
|
||||
{
|
||||
var negativePenalty = Math.Abs(p.GrowthPercentage) * 0.1; // 10% penalty per 1% loss
|
||||
penaltyMultiplier *= Math.Max(0.1, 1 - negativePenalty);
|
||||
var newMultiplier = Math.Max(0.1, 1 - negativePenalty);
|
||||
penaltyMultiplier *= newMultiplier;
|
||||
result.AddPenaltyCheck("Negative Growth", newMultiplier, $"Negative growth of {p.GrowthPercentage:F2}% applied {negativePenalty:F1}% penalty");
|
||||
}
|
||||
|
||||
// Note: Negative PnL is now handled by early exit in CalculateTotalScore
|
||||
|
||||
// 3. Win Rate Validation (Dynamic)
|
||||
if (p.WinRate < 0.3 && p.TradeCount > 10)
|
||||
{
|
||||
var winRatePenalty = (0.3 - p.WinRate) * 0.5; // 50% penalty per 10% below 30%
|
||||
penaltyMultiplier *= Math.Max(0.2, 1 - winRatePenalty);
|
||||
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");
|
||||
}
|
||||
|
||||
// 4. Minimum Profit Threshold (Dynamic)
|
||||
if (p.GrowthPercentage < 2 && p.TradeCount > 5)
|
||||
{
|
||||
var profitPenalty = (2 - p.GrowthPercentage) * 0.1; // 10% penalty per 1% below 2%
|
||||
penaltyMultiplier *= Math.Max(0.5, 1 - profitPenalty);
|
||||
var newMultiplier = Math.Max(0.5, 1 - profitPenalty);
|
||||
penaltyMultiplier *= newMultiplier;
|
||||
result.AddPenaltyCheck("Low Profit", newMultiplier, $"Growth of {p.GrowthPercentage:F2}% below 2% minimum applied {profitPenalty:F1}% penalty");
|
||||
}
|
||||
|
||||
// 5. Drawdown Penalty (Dynamic) - Enhanced to consider PnL ratio
|
||||
if (p.MaxDrawdownPc > 20)
|
||||
{
|
||||
var drawdownPenalty = (p.MaxDrawdownPc - 20) * 0.02; // 2% penalty per 1% above 20%
|
||||
penaltyMultiplier *= Math.Max(0.3, 1 - drawdownPenalty);
|
||||
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");
|
||||
}
|
||||
|
||||
// 6. Enhanced Drawdown vs PnL Penalty
|
||||
@@ -113,7 +162,9 @@ public class BacktestScorer
|
||||
if (drawdownToPnLRatio > 1.5) // If drawdown is more than 150% of PnL
|
||||
{
|
||||
var ratioPenalty = (drawdownToPnLRatio - 1.5) * 0.2; // 20% penalty per 0.5 ratio above 1.5
|
||||
penaltyMultiplier *= Math.Max(0.2, 1 - ratioPenalty);
|
||||
var newMultiplier = Math.Max(0.2, 1 - ratioPenalty);
|
||||
penaltyMultiplier *= newMultiplier;
|
||||
result.AddPenaltyCheck("Poor Risk/Reward", newMultiplier, $"Drawdown/PnL ratio of {drawdownToPnLRatio:F2}:1 above 1.5:1 threshold applied {ratioPenalty:F1}% penalty");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +173,9 @@ public class BacktestScorer
|
||||
if (testDurationDays < 30)
|
||||
{
|
||||
var durationPenalty = (30 - testDurationDays) * 0.02; // 2% penalty per day below 30
|
||||
penaltyMultiplier *= Math.Max(0.5, 1 - durationPenalty);
|
||||
var newMultiplier = Math.Max(0.5, 1 - durationPenalty);
|
||||
penaltyMultiplier *= newMultiplier;
|
||||
result.AddPenaltyCheck("Short Test Duration", newMultiplier, $"Test duration of {testDurationDays:F1} days below 30-day minimum applied {durationPenalty:F1}% penalty");
|
||||
}
|
||||
|
||||
// 8. HODL Underperformance Penalty (Dynamic)
|
||||
@@ -130,13 +183,15 @@ public class BacktestScorer
|
||||
{
|
||||
var hodlUnderperformance = p.HodlPercentage - p.GrowthPercentage;
|
||||
var hodlPenalty = hodlUnderperformance * 0.3; // 30% penalty per 1% underperformance
|
||||
penaltyMultiplier *= Math.Max(0.1, 1 - hodlPenalty);
|
||||
var newMultiplier = Math.Max(0.1, 1 - hodlPenalty);
|
||||
penaltyMultiplier *= newMultiplier;
|
||||
result.AddPenaltyCheck("HODL Underperformance", newMultiplier, $"Underperforming HODL by {hodlUnderperformance:F2}% applied {hodlPenalty:F1}% penalty");
|
||||
}
|
||||
|
||||
return baseScore * penaltyMultiplier;
|
||||
}
|
||||
|
||||
private static double CalculateGrowthScore(double growthPercentage)
|
||||
private static double CalculateGrowthScore(double growthPercentage, BacktestScoringResult result)
|
||||
{
|
||||
// More aggressive scoring - harder to reach 100
|
||||
if (growthPercentage < 0)
|
||||
@@ -164,7 +219,7 @@ public class BacktestScorer
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateProfitabilityBonus(double growthPercentage)
|
||||
private static double CalculateProfitabilityBonus(double growthPercentage, BacktestScoringResult result)
|
||||
{
|
||||
return growthPercentage switch
|
||||
{
|
||||
@@ -173,7 +228,7 @@ public class BacktestScorer
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateSharpeScore(double sharpeRatio)
|
||||
private static double CalculateSharpeScore(double sharpeRatio, BacktestScoringResult result)
|
||||
{
|
||||
return sharpeRatio switch
|
||||
{
|
||||
@@ -183,7 +238,7 @@ public class BacktestScorer
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateDrawdownUsdScore(decimal maxDrawdown, decimal initialBalance)
|
||||
private static double CalculateDrawdownUsdScore(decimal maxDrawdown, decimal initialBalance, BacktestScoringResult result)
|
||||
{
|
||||
if (initialBalance <= 0) return 0;
|
||||
|
||||
@@ -195,7 +250,7 @@ public class BacktestScorer
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateWinRateScore(double winRate, int tradeCount)
|
||||
private static double CalculateWinRateScore(double winRate, int tradeCount, BacktestScoringResult result)
|
||||
{
|
||||
// Base win rate score
|
||||
var baseScore = winRate * 100;
|
||||
@@ -212,7 +267,7 @@ public class BacktestScorer
|
||||
return baseScore * significanceFactor;
|
||||
}
|
||||
|
||||
private static double CalculateHodlComparisonScore(double strategyGrowth, double hodlGrowth)
|
||||
private static double CalculateHodlComparisonScore(double strategyGrowth, double hodlGrowth, BacktestScoringResult result)
|
||||
{
|
||||
var difference = strategyGrowth - hodlGrowth;
|
||||
|
||||
@@ -228,7 +283,7 @@ public class BacktestScorer
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateTradeCountScore(int tradeCount)
|
||||
private static double CalculateTradeCountScore(int tradeCount, BacktestScoringResult result)
|
||||
{
|
||||
return tradeCount switch
|
||||
{
|
||||
@@ -239,7 +294,7 @@ public class BacktestScorer
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateRecoveryScore(TimeSpan recoveryTime, Timeframe timeframe)
|
||||
private static double CalculateRecoveryScore(TimeSpan recoveryTime, Timeframe timeframe, BacktestScoringResult result)
|
||||
{
|
||||
var days = recoveryTime.TotalDays;
|
||||
|
||||
@@ -260,7 +315,7 @@ public class BacktestScorer
|
||||
return 100 - (days / maxRecoveryDays * 100);
|
||||
}
|
||||
|
||||
private static double CalculateTestDurationScore(DateTime startDate, DateTime endDate, Timeframe timeframe)
|
||||
private static double CalculateTestDurationScore(DateTime startDate, DateTime endDate, Timeframe timeframe, BacktestScoringResult result)
|
||||
{
|
||||
var durationDays = (endDate - startDate).TotalDays;
|
||||
|
||||
@@ -283,7 +338,7 @@ public class BacktestScorer
|
||||
return 100;
|
||||
}
|
||||
|
||||
private static double CalculateFeesImpactScore(decimal feesPaid, decimal initialBalance, decimal totalPnL)
|
||||
private static double CalculateFeesImpactScore(decimal feesPaid, decimal initialBalance, decimal totalPnL, BacktestScoringResult result)
|
||||
{
|
||||
if (initialBalance <= 0) return 0;
|
||||
|
||||
@@ -307,7 +362,7 @@ public class BacktestScorer
|
||||
return feeEfficiency;
|
||||
}
|
||||
|
||||
private static double CalculateRiskAdjustedReturnScore(double totalPnL, decimal maxDrawdown, decimal initialBalance)
|
||||
private static double CalculateRiskAdjustedReturnScore(double totalPnL, decimal maxDrawdown, decimal initialBalance, BacktestScoringResult result)
|
||||
{
|
||||
if (initialBalance <= 0 || maxDrawdown <= 0) return 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user