Add score
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
36
src/Managing.Domain/Backtests/BacktestScoringParams.cs
Normal file
36
src/Managing.Domain/Backtests/BacktestScoringParams.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/Managing.Domain/Shared/Helpers/BacktestScore.cs
Normal file
128
src/Managing.Domain/Shared/Helpers/BacktestScore.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user