Add score

This commit is contained in:
2025-03-01 17:24:16 +07:00
parent e16f0a2e5d
commit bcf9d21b0a
7 changed files with 212 additions and 4 deletions

View File

@@ -164,6 +164,23 @@ namespace Managing.Application.Backtesting
var stats = TradingHelpers.GetStatistics(bot.WalletBalances); var stats = TradingHelpers.GetStatistics(bot.WalletBalances);
var growthPercentage = TradingHelpers.GetGrowthFromInitalBalance(balance, finalPnl); var growthPercentage = TradingHelpers.GetGrowthFromInitalBalance(balance, finalPnl);
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last()); 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, var result = new Backtest(ticker, scenario.Name, bot.Positions, bot.Signals.ToList(), timeframe, candles,
bot.BotType, account.Name) bot.BotType, account.Name)
{ {
@@ -176,9 +193,11 @@ namespace Managing.Application.Backtesting
Statistics = stats, Statistics = stats,
OptimizedMoneyManagement = optimizedMoneyManagement, OptimizedMoneyManagement = optimizedMoneyManagement,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
StrategiesValues = AggregateValues(strategiesValues, bot.StrategiesValues) StrategiesValues = AggregateValues(strategiesValues, bot.StrategiesValues),
Score = score
}; };
return result; return result;
} }

View File

@@ -50,7 +50,8 @@ public class Backtest
[Required] public MoneyManagement OptimizedMoneyManagement { get; set; } [Required] public MoneyManagement OptimizedMoneyManagement { get; set; }
[Required] public MoneyManagement MoneyManagement { get; set; } [Required] public MoneyManagement MoneyManagement { get; set; }
public Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; } [Required] public Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; }
[Required] public double Score { get; set; }
public string GetStringReport() public string GetStringReport()
{ {

View File

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

View File

@@ -0,0 +1,128 @@
using Managing.Domain.Backtests;
public class BacktestScorer
{
// Updated weights without ProfitEfficiency
private static readonly Dictionary<string, double> Weights = new Dictionary<string, double>
{
{ "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<string, double>
{
{ "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);
}
}

View File

@@ -52,7 +52,7 @@ public class LaggingSTC : Strategy
* - Ends at previous candle to avoid inclusion of current break * - Ends at previous candle to avoid inclusion of current break
* - Dynamic sizing for early dataset cases */ * - Dynamic sizing for early dataset cases */
// Calculate the lookback window ending at previousCandle (excludes currentCandle) // 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 int windowStart = Math.Max(0, i - windowSize); // Ensure no negative indices
var lookbackWindow = stcCandles var lookbackWindow = stcCandles
.Skip(windowStart) .Skip(windowStart)

View File

@@ -61,6 +61,13 @@ const BacktestTable: React.FC<IBacktestCards> = ({ 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( const columns = React.useMemo(
() => [ () => [
{ {
@@ -92,12 +99,28 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
// Build our expander column // Build our expander column
id: 'expander', id: 'expander',
}, },
{
Header: 'Score',
accessor: 'score',
Cell: ({ cell }: any) => (
<span style={{
color: getScoreColor(cell.row.values.score),
fontWeight: 500,
display: 'inline-block',
width: '60px'
}}>
{cell.row.values.score.toFixed(2)}
</span>
),
disableFilters: true,
},
{ {
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
Header: 'Ticker', Header: 'Ticker',
accessor: 'ticker', accessor: 'ticker',
disableSortBy: true, disableSortBy: true,
}, },
{ {
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
Header: 'Timeframe', Header: 'Timeframe',

View File

@@ -2003,7 +2003,8 @@ export interface Backtest {
walletBalances: KeyValuePairOfDateTimeAndDecimal[]; walletBalances: KeyValuePairOfDateTimeAndDecimal[];
optimizedMoneyManagement: MoneyManagement; optimizedMoneyManagement: MoneyManagement;
moneyManagement: MoneyManagement; moneyManagement: MoneyManagement;
strategiesValues?: { [key in keyof typeof StrategyType]?: StrategiesResultBase; } | null; strategiesValues: { [key in keyof typeof StrategyType]?: StrategiesResultBase; };
score: number;
} }
export enum Ticker { export enum Ticker {