Add scoring message
This commit is contained in:
@@ -16,4 +16,5 @@ public class LightBacktestResponse
|
|||||||
public decimal Fees { get; set; }
|
public decimal Fees { get; set; }
|
||||||
public double? SharpeRatio { get; set; }
|
public double? SharpeRatio { get; set; }
|
||||||
public double Score { get; set; }
|
public double Score { get; set; }
|
||||||
|
public string ScoreMessage { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -317,7 +317,7 @@ namespace Managing.Application.Backtesting
|
|||||||
timeframe: config.Timeframe
|
timeframe: config.Timeframe
|
||||||
);
|
);
|
||||||
|
|
||||||
var score = BacktestScorer.CalculateTotalScore(scoringParams);
|
var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams);
|
||||||
|
|
||||||
// Create backtest result with conditional candles and indicators values
|
// Create backtest result with conditional candles and indicators values
|
||||||
var result = new Backtest(config, bot.Positions, bot.Signals.ToList(),
|
var result = new Backtest(config, bot.Positions, bot.Signals.ToList(),
|
||||||
@@ -334,7 +334,8 @@ namespace Managing.Application.Backtesting
|
|||||||
IndicatorsValues = withCandles
|
IndicatorsValues = withCandles
|
||||||
? AggregateValues(indicatorsValues, bot.IndicatorsValues)
|
? AggregateValues(indicatorsValues, bot.IndicatorsValues)
|
||||||
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
|
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
|
||||||
Score = score,
|
Score = scoringResult.Score,
|
||||||
|
ScoreMessage = scoringResult.SummaryMessage,
|
||||||
Id = Guid.NewGuid().ToString(),
|
Id = Guid.NewGuid().ToString(),
|
||||||
RequestId = requestId,
|
RequestId = requestId,
|
||||||
Metadata = metadata,
|
Metadata = metadata,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ public class Backtest
|
|||||||
[Required] public double Score { get; set; }
|
[Required] public double Score { get; set; }
|
||||||
public string RequestId { get; set; }
|
public string RequestId { get; set; }
|
||||||
public object? Metadata { get; set; }
|
public object? Metadata { get; set; }
|
||||||
|
public string ScoreMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new TradingBotConfig based on this backtest's configuration for starting a live bot.
|
/// Creates a new TradingBotConfig based on this backtest's configuration for starting a live bot.
|
||||||
|
|||||||
114
src/Managing.Domain/Backtests/BacktestScoringResult.cs
Normal file
114
src/Managing.Domain/Backtests/BacktestScoringResult.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Managing.Domain.Backtests;
|
||||||
|
|
||||||
|
public class BacktestScoringResult
|
||||||
|
{
|
||||||
|
public double Score { get; set; }
|
||||||
|
public List<ScoringCheck> Checks { get; set; } = new();
|
||||||
|
public string SummaryMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public BacktestScoringResult(double score)
|
||||||
|
{
|
||||||
|
Score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCheck(string component, double score, double weight, string message, bool passed = true)
|
||||||
|
{
|
||||||
|
Checks.Add(new ScoringCheck
|
||||||
|
{
|
||||||
|
Component = component,
|
||||||
|
Score = score,
|
||||||
|
Weight = weight,
|
||||||
|
Message = message,
|
||||||
|
Passed = passed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEarlyExitCheck(string reason, string message)
|
||||||
|
{
|
||||||
|
Checks.Add(new ScoringCheck
|
||||||
|
{
|
||||||
|
Component = "Early Exit",
|
||||||
|
Score = 0,
|
||||||
|
Weight = 0,
|
||||||
|
Message = message,
|
||||||
|
Passed = false,
|
||||||
|
IsEarlyExit = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPenaltyCheck(string component, double penaltyMultiplier, string message)
|
||||||
|
{
|
||||||
|
Checks.Add(new ScoringCheck
|
||||||
|
{
|
||||||
|
Component = component,
|
||||||
|
Score = 0,
|
||||||
|
Weight = 0,
|
||||||
|
Message = message,
|
||||||
|
Passed = penaltyMultiplier >= 1.0,
|
||||||
|
IsPenalty = true,
|
||||||
|
PenaltyMultiplier = penaltyMultiplier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateSummaryMessage()
|
||||||
|
{
|
||||||
|
if (Score == 0)
|
||||||
|
{
|
||||||
|
var earlyExit = Checks.FirstOrDefault(c => c.IsEarlyExit);
|
||||||
|
if (earlyExit != null)
|
||||||
|
{
|
||||||
|
return $"Score: 0 - {earlyExit.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var passedChecks = Checks.Where(c => c.Passed && !c.IsEarlyExit && !c.IsPenalty).ToList();
|
||||||
|
var failedChecks = Checks.Where(c => !c.Passed && !c.IsEarlyExit && !c.IsPenalty).ToList();
|
||||||
|
var penalties = Checks.Where(c => c.IsPenalty).ToList();
|
||||||
|
|
||||||
|
var summary = new StringBuilder();
|
||||||
|
summary.AppendLine($"Final Score: {Score:F1}/100");
|
||||||
|
|
||||||
|
if (passedChecks.Any())
|
||||||
|
{
|
||||||
|
summary.AppendLine($"✅ Passed Checks ({passedChecks.Count}):");
|
||||||
|
foreach (var check in passedChecks.OrderByDescending(c => c.Score * c.Weight))
|
||||||
|
{
|
||||||
|
summary.AppendLine($" • {check.Component}: {check.Score:F1} points ({check.Message})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedChecks.Any())
|
||||||
|
{
|
||||||
|
summary.AppendLine($"❌ Failed Checks ({failedChecks.Count}):");
|
||||||
|
foreach (var check in failedChecks)
|
||||||
|
{
|
||||||
|
summary.AppendLine($" • {check.Component}: {check.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (penalties.Any())
|
||||||
|
{
|
||||||
|
summary.AppendLine($"⚠️ Applied Penalties ({penalties.Count}):");
|
||||||
|
foreach (var penalty in penalties)
|
||||||
|
{
|
||||||
|
summary.AppendLine($" • {penalty.Component}: {penalty.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScoringCheck
|
||||||
|
{
|
||||||
|
public string Component { get; set; } = string.Empty;
|
||||||
|
public double Score { get; set; }
|
||||||
|
public double Weight { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public bool Passed { get; set; }
|
||||||
|
public bool IsEarlyExit { get; set; }
|
||||||
|
public bool IsPenalty { get; set; }
|
||||||
|
public double PenaltyMultiplier { get; set; }
|
||||||
|
}
|
||||||
@@ -16,4 +16,5 @@ public class LightBacktest
|
|||||||
public decimal Fees { get; set; }
|
public decimal Fees { get; set; }
|
||||||
public double? SharpeRatio { get; set; }
|
public double? SharpeRatio { get; set; }
|
||||||
public double Score { get; set; }
|
public double Score { get; set; }
|
||||||
|
public string ScoreMessage { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -21,58 +21,101 @@ public class BacktestScorer
|
|||||||
|
|
||||||
public static double CalculateTotalScore(BacktestScoringParams p)
|
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
|
try
|
||||||
{
|
{
|
||||||
// Early exit for no positions
|
// Early exit for no positions
|
||||||
if (p.TradeCount == 0)
|
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
|
// Early exit for negative PnL - should be 0 score
|
||||||
if (p.TotalPnL <= 0)
|
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)
|
// Early exit if strategy significantly underperforms HODL (more than 2% worse)
|
||||||
if (p.GrowthPercentage < p.HodlPercentage - 2)
|
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 baseScore = CalculateBaseScore(p, result);
|
||||||
var finalScore = ApplyProfitabilityRules(baseScore, p);
|
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>
|
var componentScores = new Dictionary<string, double>
|
||||||
{
|
{
|
||||||
{ "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage) },
|
{ "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage, result) },
|
||||||
{ "SharpeRatio", CalculateSharpeScore(p.SharpeRatio) },
|
{ "SharpeRatio", CalculateSharpeScore(p.SharpeRatio, result) },
|
||||||
{ "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance) },
|
{ "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance, result) },
|
||||||
{ "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage) },
|
{ "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage, result) },
|
||||||
{ "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount) },
|
{ "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount, result) },
|
||||||
{ "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage) },
|
{ "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage, result) },
|
||||||
{ "TradeCount", CalculateTradeCountScore(p.TradeCount) },
|
{ "TradeCount", CalculateTradeCountScore(p.TradeCount, result) },
|
||||||
{ "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe) },
|
{ "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe, result) },
|
||||||
{ "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe) },
|
{ "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe, result) },
|
||||||
{ "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL) },
|
{ "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL, result) },
|
||||||
{ "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.InitialBalance) }
|
{ "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;
|
var penaltyMultiplier = 1.0;
|
||||||
|
|
||||||
@@ -80,30 +123,36 @@ public class BacktestScorer
|
|||||||
if (p.GrowthPercentage < 0)
|
if (p.GrowthPercentage < 0)
|
||||||
{
|
{
|
||||||
var negativePenalty = Math.Abs(p.GrowthPercentage) * 0.1; // 10% penalty per 1% loss
|
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)
|
// 3. Win Rate Validation (Dynamic)
|
||||||
if (p.WinRate < 0.3 && p.TradeCount > 10)
|
if (p.WinRate < 0.3 && p.TradeCount > 10)
|
||||||
{
|
{
|
||||||
var winRatePenalty = (0.3 - p.WinRate) * 0.5; // 50% penalty per 10% below 30%
|
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)
|
// 4. Minimum Profit Threshold (Dynamic)
|
||||||
if (p.GrowthPercentage < 2 && p.TradeCount > 5)
|
if (p.GrowthPercentage < 2 && p.TradeCount > 5)
|
||||||
{
|
{
|
||||||
var profitPenalty = (2 - p.GrowthPercentage) * 0.1; // 10% penalty per 1% below 2%
|
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
|
// 5. Drawdown Penalty (Dynamic) - Enhanced to consider PnL ratio
|
||||||
if (p.MaxDrawdownPc > 20)
|
if (p.MaxDrawdownPc > 20)
|
||||||
{
|
{
|
||||||
var drawdownPenalty = (p.MaxDrawdownPc - 20) * 0.02; // 2% penalty per 1% above 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
|
// 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
|
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
|
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)
|
if (testDurationDays < 30)
|
||||||
{
|
{
|
||||||
var durationPenalty = (30 - testDurationDays) * 0.02; // 2% penalty per day below 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)
|
// 8. HODL Underperformance Penalty (Dynamic)
|
||||||
@@ -130,13 +183,15 @@ public class BacktestScorer
|
|||||||
{
|
{
|
||||||
var hodlUnderperformance = p.HodlPercentage - p.GrowthPercentage;
|
var hodlUnderperformance = p.HodlPercentage - p.GrowthPercentage;
|
||||||
var hodlPenalty = hodlUnderperformance * 0.3; // 30% penalty per 1% underperformance
|
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;
|
return baseScore * penaltyMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double CalculateGrowthScore(double growthPercentage)
|
private static double CalculateGrowthScore(double growthPercentage, BacktestScoringResult result)
|
||||||
{
|
{
|
||||||
// More aggressive scoring - harder to reach 100
|
// More aggressive scoring - harder to reach 100
|
||||||
if (growthPercentage < 0)
|
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
|
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
|
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;
|
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
|
// Base win rate score
|
||||||
var baseScore = winRate * 100;
|
var baseScore = winRate * 100;
|
||||||
@@ -212,7 +267,7 @@ public class BacktestScorer
|
|||||||
return baseScore * significanceFactor;
|
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;
|
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
|
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;
|
var days = recoveryTime.TotalDays;
|
||||||
|
|
||||||
@@ -260,7 +315,7 @@ public class BacktestScorer
|
|||||||
return 100 - (days / maxRecoveryDays * 100);
|
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;
|
var durationDays = (endDate - startDate).TotalDays;
|
||||||
|
|
||||||
@@ -283,7 +338,7 @@ public class BacktestScorer
|
|||||||
return 100;
|
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;
|
if (initialBalance <= 0) return 0;
|
||||||
|
|
||||||
@@ -307,7 +362,7 @@ public class BacktestScorer
|
|||||||
return feeEfficiency;
|
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;
|
if (initialBalance <= 0 || maxDrawdown <= 0) return 0;
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ public class BacktestRepository : IBacktestRepository
|
|||||||
MaxDrawdown = b.Statistics?.MaxDrawdown,
|
MaxDrawdown = b.Statistics?.MaxDrawdown,
|
||||||
Fees = b.Fees,
|
Fees = b.Fees,
|
||||||
SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null,
|
SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null,
|
||||||
Score = b.Score
|
Score = b.Score,
|
||||||
|
ScoreMessage = b.ScoreMessage ?? string.Empty
|
||||||
});
|
});
|
||||||
|
|
||||||
return (mappedBacktests, (int)totalCount);
|
return (mappedBacktests, (int)totalCount);
|
||||||
@@ -284,7 +285,8 @@ public class BacktestRepository : IBacktestRepository
|
|||||||
MaxDrawdown = b.Statistics?.MaxDrawdown,
|
MaxDrawdown = b.Statistics?.MaxDrawdown,
|
||||||
Fees = b.Fees,
|
Fees = b.Fees,
|
||||||
SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null,
|
SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null,
|
||||||
Score = b.Score
|
Score = b.Score,
|
||||||
|
ScoreMessage = b.ScoreMessage ?? string.Empty
|
||||||
});
|
});
|
||||||
|
|
||||||
return (mappedBacktests, (int)totalCount);
|
return (mappedBacktests, (int)totalCount);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
|
|||||||
[BsonRepresentation(BsonType.Decimal128)]
|
[BsonRepresentation(BsonType.Decimal128)]
|
||||||
public decimal Fees { get; set; }
|
public decimal Fees { get; set; }
|
||||||
public double Score { get; set; }
|
public double Score { get; set; }
|
||||||
|
public string ScoreMessage { get; set; } = string.Empty;
|
||||||
public string Identifier { get; set; }
|
public string Identifier { get; set; }
|
||||||
public string RequestId { get; set; }
|
public string RequestId { get; set; }
|
||||||
public string? Metadata { get; set; }
|
public string? Metadata { get; set; }
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ public static class MongoMappers
|
|||||||
StartDate = b.StartDate,
|
StartDate = b.StartDate,
|
||||||
EndDate = b.EndDate,
|
EndDate = b.EndDate,
|
||||||
Score = b.Score,
|
Score = b.Score,
|
||||||
|
ScoreMessage = b.ScoreMessage ?? string.Empty,
|
||||||
RequestId = b.RequestId,
|
RequestId = b.RequestId,
|
||||||
Metadata = string.IsNullOrEmpty(b.Metadata) ? null : JsonSerializer.Deserialize<object>(b.Metadata)
|
Metadata = string.IsNullOrEmpty(b.Metadata) ? null : JsonSerializer.Deserialize<object>(b.Metadata)
|
||||||
};
|
};
|
||||||
@@ -179,6 +180,7 @@ public static class MongoMappers
|
|||||||
StartDate = result.StartDate,
|
StartDate = result.StartDate,
|
||||||
EndDate = result.EndDate,
|
EndDate = result.EndDate,
|
||||||
Score = result.Score,
|
Score = result.Score,
|
||||||
|
ScoreMessage = result.ScoreMessage ?? string.Empty,
|
||||||
RequestId = result.RequestId,
|
RequestId = result.RequestId,
|
||||||
Metadata = result.Metadata == null ? null : JsonSerializer.Serialize(result.Metadata)
|
Metadata = result.Metadata == null ? null : JsonSerializer.Serialize(result.Metadata)
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user