diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index 68b2ec0..865d43e 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -164,6 +164,23 @@ namespace Managing.Application.Backtesting var stats = TradingHelpers.GetStatistics(bot.WalletBalances); var growthPercentage = TradingHelpers.GetGrowthFromInitalBalance(balance, finalPnl); var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last()); + + var scoringParams = new BacktestScoringParams( + sharpeRatio: (double)stats.SharpeRatio, + maxDrawdownPc: (double)stats.MaxDrawdownPc, + growthPercentage: (double)growthPercentage, + hodlPercentage: (double)hodlPercentage, + winRate: winRate, + totalPnL: (double)finalPnl, + fees: (double)bot.GetTotalFees(), + tradeCount: bot.Positions.Count, + maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime + ); + +// Then calculate the score + var score = BacktestScorer.CalculateTotalScore(scoringParams); + + var result = new Backtest(ticker, scenario.Name, bot.Positions, bot.Signals.ToList(), timeframe, candles, bot.BotType, account.Name) { @@ -176,9 +193,11 @@ namespace Managing.Application.Backtesting Statistics = stats, OptimizedMoneyManagement = optimizedMoneyManagement, MoneyManagement = moneyManagement, - StrategiesValues = AggregateValues(strategiesValues, bot.StrategiesValues) + StrategiesValues = AggregateValues(strategiesValues, bot.StrategiesValues), + Score = score }; + return result; } diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs index a1af0e2..8132467 100644 --- a/src/Managing.Domain/Backtests/Backtest.cs +++ b/src/Managing.Domain/Backtests/Backtest.cs @@ -50,7 +50,8 @@ public class Backtest [Required] public MoneyManagement OptimizedMoneyManagement { get; set; } [Required] public MoneyManagement MoneyManagement { get; set; } - public Dictionary StrategiesValues { get; set; } + [Required] public Dictionary StrategiesValues { get; set; } + [Required] public double Score { get; set; } public string GetStringReport() { diff --git a/src/Managing.Domain/Backtests/BacktestScoringParams.cs b/src/Managing.Domain/Backtests/BacktestScoringParams.cs new file mode 100644 index 0000000..31f1e8a --- /dev/null +++ b/src/Managing.Domain/Backtests/BacktestScoringParams.cs @@ -0,0 +1,36 @@ +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; } + public double TotalPnL { get; } + public double Fees { get; } + public int TradeCount { get; } + public TimeSpan MaxDrawdownRecoveryTime { get; } + + public BacktestScoringParams( + double sharpeRatio, + double maxDrawdownPc, + double growthPercentage, + double hodlPercentage, + double winRate, + double totalPnL, + double fees, + int tradeCount, + TimeSpan maxDrawdownRecoveryTime) + { + SharpeRatio = sharpeRatio; + MaxDrawdownPc = maxDrawdownPc; + GrowthPercentage = growthPercentage; + HodlPercentage = hodlPercentage; + WinRate = winRate; + TotalPnL = totalPnL; + Fees = fees; + TradeCount = tradeCount; + MaxDrawdownRecoveryTime = maxDrawdownRecoveryTime; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs new file mode 100644 index 0000000..6dc3f2d --- /dev/null +++ b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs @@ -0,0 +1,128 @@ +using Managing.Domain.Backtests; + +public class BacktestScorer +{ + // Updated weights without ProfitEfficiency + private static readonly Dictionary Weights = new Dictionary + { + { "SharpeRatio", 0.22 }, // Increased weight + { "GrowthPercentage", 0.22 }, // Increased weight + { "MaxDrawdownPc", 0.15 }, + { "WinRate", 0.15 }, // Increased weight + { "HodlComparison", 0.15 }, // Increased weight + { "TradeCount", 0.06 }, + { "RecoveryTime", 0.04 }, + { "RiskAdjustedGrowth", 0.01 } + }; + + public static double CalculateTotalScore(BacktestScoringParams p) + { + try + { + // Detect inactive strategies + if (IsInactiveStrategy(p)) + { + return Math.Min(CalculateBaseScore(p) * 0.3, 40); + } + + var score = CalculateBaseScore(p); + return double.Clamp(score, 0, 100); + } + catch + { + return 0; + } + } + + private static double CalculateBaseScore(BacktestScoringParams p) + { + var componentScores = new Dictionary + { + { "SharpeRatio", CalculateSharpeScore(p.SharpeRatio) }, + { "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage) }, + { "MaxDrawdownPc", CalculateDrawdownScore(p.MaxDrawdownPc) }, + { "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount) }, + { "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage) }, + { "TradeCount", CalculateTradeCountScore(p.TradeCount) }, + { "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime) }, + { "RiskAdjustedGrowth", CalculateRiskAdjustedGrowthScore(p.GrowthPercentage, p.MaxDrawdownPc) } + }; + + return componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]); + } + + private static bool IsInactiveStrategy(BacktestScoringParams p) + { + // Detect strategies with no economic value + return (p.GrowthPercentage <= p.HodlPercentage && + p.TotalPnL <= 0) || + p.TradeCount < 3; + } + + private static double CalculateSharpeScore(double sharpeRatio) + { + return sharpeRatio switch + { + < 0 => 0, + > 3 => 100, + _ => (sharpeRatio / 3) * 100 + }; + } + + private static double CalculateGrowthScore(double growthPercentage) + { + if (growthPercentage < 0) + return Math.Max(0, 50 + (growthPercentage * 2)); // -5% → 40, -25% → 0 + + return Math.Min(100, (Math.Log(1 + growthPercentage) / Math.Log(1 + 1000)) * 100); + } + + private static double CalculateDrawdownScore(double maxDrawdownPc) + { + return maxDrawdownPc switch + { + > 90 => 0, + _ => 100 - Math.Pow(maxDrawdownPc / 90 * 100, 2) / 100 + }; + } + + private static double CalculateWinRateScore(double winRate, int tradeCount) + { + var baseScore = winRate * 100; + var significanceFactor = Math.Min(1, tradeCount / 100.0); + return baseScore * significanceFactor; + } + + private static double CalculateHodlComparisonScore(double strategyGrowth, double hodlGrowth) + { + var difference = strategyGrowth - hodlGrowth; + return difference switch + { + > 0 => 100 - (100 / (1 + difference / 5)), + _ => Math.Max(0, 30 + difference * 3) + }; + } + + private static double CalculateTradeCountScore(int tradeCount) + { + return Math.Min(100, Math.Max(0, (tradeCount - 10) * 0.5)); + } + + private static double CalculateRecoveryScore(TimeSpan recoveryTime) + { + var days = recoveryTime.TotalDays; + return days switch + { + < 0 => 100, + > 365 => 0, + _ => 100 - (days / 365 * 100) + }; + } + + private static double CalculateRiskAdjustedGrowthScore(double growth, double drawdown) + { + if (drawdown == 0) return 100; + var ratio = growth / drawdown; + return Math.Min(ratio * 10, 100); + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Strategies/LaggingSTC.cs b/src/Managing.Domain/Strategies/LaggingSTC.cs index 7d0a7fb..e8a4dba 100644 --- a/src/Managing.Domain/Strategies/LaggingSTC.cs +++ b/src/Managing.Domain/Strategies/LaggingSTC.cs @@ -52,7 +52,7 @@ public class LaggingSTC : Strategy * - Ends at previous candle to avoid inclusion of current break * - Dynamic sizing for early dataset cases */ // Calculate the lookback window ending at previousCandle (excludes currentCandle) - int windowSize = 32; + int windowSize = 40; int windowStart = Math.Max(0, i - windowSize); // Ensure no negative indices var lookbackWindow = stcCandles .Skip(windowStart) diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index 9065ab0..f845044 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -61,6 +61,13 @@ const BacktestTable: React.FC = ({ list, isFetching }) => { }) } + const getScoreColor = (score: number) => { + if (score >= 75) return '#08C25F'; // success + if (score >= 50) return '#B0DB43'; // info + if (score >= 25) return '#EB6F22'; // warning + return '#FF5340'; // error + }; + const columns = React.useMemo( () => [ { @@ -92,12 +99,28 @@ const BacktestTable: React.FC = ({ list, isFetching }) => { // Build our expander column id: 'expander', }, + { + Header: 'Score', + accessor: 'score', + Cell: ({ cell }: any) => ( + + {cell.row.values.score.toFixed(2)} + + ), + disableFilters: true, + }, { Filter: SelectColumnFilter, Header: 'Ticker', accessor: 'ticker', disableSortBy: true, }, + { Filter: SelectColumnFilter, Header: 'Timeframe', diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 0806927..fcab89d 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -2003,7 +2003,8 @@ export interface Backtest { walletBalances: KeyValuePairOfDateTimeAndDecimal[]; optimizedMoneyManagement: MoneyManagement; moneyManagement: MoneyManagement; - strategiesValues?: { [key in keyof typeof StrategyType]?: StrategiesResultBase; } | null; + strategiesValues: { [key in keyof typeof StrategyType]?: StrategiesResultBase; }; + score: number; } export enum Ticker {