From c0f6d68833a5d31e2b70b40267c9470e2f43d41f Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sat, 12 Jul 2025 00:10:22 +0700 Subject: [PATCH] Update scoring and progress --- .../Shared/Helpers/BacktestScore.cs | 61 ++++++++++--- .../organism/Backtest/backtestTable.tsx | 86 ++++++++++++------- .../organism/Charts/Fitness3DPlot.tsx | 15 ++-- .../organism/Charts/ScoreVsGeneration.tsx | 9 +- .../organism/Charts/TPvsSLvsPnL3DPlot.tsx | 10 +-- .../backtestPage/backtestGeneticBundle.tsx | 34 +++++--- 6 files changed, 142 insertions(+), 73 deletions(-) diff --git a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs index 317cc96..1c45fad 100644 --- a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs +++ b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs @@ -3,19 +3,20 @@ using static Managing.Common.Enums; public class BacktestScorer { - // Updated weights with more balanced distribution + // Updated weights with more emphasis on HODL comparison and risk management private static readonly Dictionary Weights = new Dictionary { - { "GrowthPercentage", 0.25 }, + { "GrowthPercentage", 0.20 }, { "SharpeRatio", 0.15 }, - { "MaxDrawdownUsd", 0.12 }, - { "HodlComparison", 0.05 }, - { "WinRate", 0.15 }, + { "MaxDrawdownUsd", 0.10 }, + { "HodlComparison", 0.15 }, // Increased from 0.05 to 0.15 + { "WinRate", 0.12 }, { "ProfitabilityBonus", 0.08 }, { "TradeCount", 0.05 }, { "RecoveryTime", 0.02 }, { "TestDuration", 0.03 }, - { "FeesImpact", 0.02 } + { "FeesImpact", 0.02 }, + { "RiskAdjustedReturn", 0.08 } // New component }; public static double CalculateTotalScore(BacktestScoringParams p) @@ -52,7 +53,8 @@ public class BacktestScorer { "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) } + { "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL) }, + { "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.InitialBalance) } }; return componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]); @@ -89,14 +91,25 @@ public class BacktestScorer penaltyMultiplier *= Math.Max(0.5, 1 - profitPenalty); } - // 5. Drawdown Penalty (Dynamic) + // 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); } - // 6. Test Duration Penalty (Dynamic) + // 6. Enhanced Drawdown vs PnL Penalty + if (p.TotalPnL > 0 && p.MaxDrawdown > 0) + { + var drawdownToPnLRatio = (double)(p.MaxDrawdown / (decimal)p.TotalPnL); + 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); + } + } + + // 7. Test Duration Penalty (Dynamic) var testDurationDays = (p.EndDate - p.StartDate).TotalDays; if (testDurationDays < 30) { @@ -154,8 +167,6 @@ public class BacktestScorer }; } - - private static double CalculateDrawdownUsdScore(decimal maxDrawdown, decimal initialBalance) { if (initialBalance <= 0) return 0; @@ -188,10 +199,16 @@ public class BacktestScorer private static double CalculateHodlComparisonScore(double strategyGrowth, double hodlGrowth) { var difference = strategyGrowth - hodlGrowth; + + // Much more aggressive scoring for HODL comparison return difference switch { - > 0 => 80 - (80 / (1 + difference / 3)), // Reduced max to 80 - _ => Math.Max(0, 20 + difference * 2) // Reduced base score + > 5 => 100, // Significantly outperform HODL (>5% better) + > 2 => 80 + (difference - 2) * 6.67, // 2-5% better: 80-100 points + > 0 => 40 + difference * 20, // 0-2% better: 40-80 points + > -2 => 20 + (difference + 2) * 10, // -2-0%: 20-40 points + > -5 => Math.Max(0, 10 + (difference + 5) * 3.33), // -5 to -2%: 0-20 points + _ => 0 // More than 5% worse than HODL = 0 points }; } @@ -273,4 +290,22 @@ public class BacktestScorer return feeEfficiency; } + + private static double CalculateRiskAdjustedReturnScore(double totalPnL, decimal maxDrawdown, decimal initialBalance) + { + if (initialBalance <= 0 || maxDrawdown <= 0) return 0; + + var pnlRatio = totalPnL / (double)maxDrawdown; + + // Score based on PnL to drawdown ratio + return pnlRatio switch + { + > 3 => 100, // Excellent risk-adjusted return (>3:1) + > 2 => 80 + (pnlRatio - 2) * 20, // 2-3:1: 80-100 points + > 1.5 => 60 + (pnlRatio - 1.5) * 40, // 1.5-2:1: 60-80 points + > 1 => 40 + (pnlRatio - 1) * 40, // 1-1.5:1: 40-60 points + > 0.5 => 20 + (pnlRatio - 0.5) * 40, // 0.5-1:1: 20-40 points + _ => Math.Max(0, pnlRatio * 40) // 0-0.5:1: 0-20 points + }; + } } \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index b1d1ab7..a6a3062 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -5,7 +5,7 @@ import useApiUrlStore from '../../../app/store/apiStore' import useBacktestStore from '../../../app/store/backtestStore' import type {Backtest} from '../../../generated/ManagingApi' import {BacktestClient} from '../../../generated/ManagingApi' -import {CardText, ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter, Table} from '../../mollecules' +import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter, Table} from '../../mollecules' import {UnifiedTradingModal} from '../index' import Toast from '../../mollecules/Toast/Toast' @@ -37,6 +37,9 @@ const BacktestTable: React.FC = ({list, isFetching, displayS medianCooldown: 0, }) + // Summary collapse state + const [isSummaryCollapsed, setIsSummaryCollapsed] = useState(true) + // Bot configuration modal state const [showBotConfigModal, setShowBotConfigModal] = useState(false) const [selectedBacktest, setSelectedBacktest] = useState(null) @@ -471,37 +474,56 @@ const BacktestTable: React.FC = ({list, isFetching, displayS ) : ( <> {list && list.length > 0 && displaySummary && ( - <> -
- -
-
- -
-
- -
- +
+ + + {!isSummaryCollapsed && ( +
+
+
+
Money Management
+
+ SL: {optimizedMoneyManagement.stopLoss.toFixed(2)}% + TP: {optimizedMoneyManagement.takeProfit.toFixed(2)}% + R/R: {(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)} +
+
+ +
+
Position Timing
+
+ Avg: {positionTimingStats.averageOpenTime.toFixed(1)}h + Median: {positionTimingStats.medianOpenTime.toFixed(1)}h + Losing: {positionTimingStats.losingPositionsAverageOpenTime.toFixed(1)}h +
+
+ +
+
Cooldown
+
+ Avg: {cooldownRecommendations.averageCooldown} candles + Median: {cooldownRecommendations.medianCooldown} candles +
+
+
+
+ )} +
)} = ({ backtests, theme }) => { +const Fitness3DPlot: React.FC = ({ backtests }) => { + const theme = useTheme().themeProperty() + const LOOPBACK_CONFIG = { singleIndicator: 1, multipleIndicators: { min: 5, max: 15 }, @@ -113,9 +116,9 @@ const Fitness3DPlot: React.FC = ({ backtests, theme }) => { layout={{ title: {text: 'Fitness Score vs Score vs Win Rate'}, scene: { - xaxis: {title: {text: 'Fitness Score'}}, - yaxis: {title: {text: 'Score'}}, - zaxis: {title: {text: 'Win Rate (%)'}}, + xaxis: {title: {text: 'Fitness Score'}, zerolinecolor: theme.secondary, color: theme.secondary}, + yaxis: {title: {text: 'Score'}, zerolinecolor: theme.secondary, color: theme.secondary}, + zaxis: {title: {text: 'Win Rate (%)'}, zerolinecolor: theme.secondary, color: theme.secondary}, }, width: 750, height: 600, @@ -126,8 +129,8 @@ const Fitness3DPlot: React.FC = ({ backtests, theme }) => { r: 0, t: 0, }, - paper_bgcolor: '#121212', - plot_bgcolor: theme.secondary, + paper_bgcolor: "rgba(0,0,0,0)", + plot_bgcolor: "rgba(0,0,0,0)", }} config={{displayModeBar: false}} /> diff --git a/src/Managing.WebApp/src/components/organism/Charts/ScoreVsGeneration.tsx b/src/Managing.WebApp/src/components/organism/Charts/ScoreVsGeneration.tsx index 8ff2176..286c639 100644 --- a/src/Managing.WebApp/src/components/organism/Charts/ScoreVsGeneration.tsx +++ b/src/Managing.WebApp/src/components/organism/Charts/ScoreVsGeneration.tsx @@ -1,13 +1,16 @@ import React from 'react' import Plot from 'react-plotly.js' import {Backtest} from '../../../generated/ManagingApi' +import useTheme from '../../../hooks/useTheme' interface ScoreVsGenerationProps { backtests: Backtest[] theme: { secondary: string } } -const ScoreVsGeneration: React.FC = ({ backtests, theme }) => { +const ScoreVsGeneration: React.FC = ({ backtests }) => { + const theme = useTheme().themeProperty() + // Helper function to get generation from metadata const getGeneration = (backtest: Backtest): number => { if (backtest.metadata && typeof backtest.metadata === 'object' && 'generation' in backtest.metadata) { @@ -115,7 +118,7 @@ const ScoreVsGeneration: React.FC = ({ backtests, theme color: theme.secondary } }, - gridcolor: theme.secondary, + gridcolor: theme.neutral, zerolinecolor: theme.secondary, color: theme.secondary }, @@ -126,7 +129,7 @@ const ScoreVsGeneration: React.FC = ({ backtests, theme color: theme.secondary } }, - gridcolor: theme.secondary, + gridcolor: theme.neutral, zerolinecolor: theme.secondary, color: theme.secondary }, diff --git a/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx b/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx index cadbd52..ae1cb21 100644 --- a/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx +++ b/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx @@ -86,9 +86,9 @@ const TPvsSLvsPnL3DPlot: React.FC = ({ backtests, theme layout={{ title: {text: 'Take Profit % vs Stop Loss % vs PnL'}, scene: { - xaxis: {title: {text: 'Take Profit (%)'}}, - yaxis: {title: {text: 'Stop Loss (%)'}}, - zaxis: {title: {text: 'PnL ($)'}}, + xaxis: {title: {text: 'Take Profit (%)'}, zerolinecolor: theme.secondary, color: theme.secondary}, + yaxis: {title: {text: 'Stop Loss (%)'}, zerolinecolor: theme.secondary, color: theme.secondary}, + zaxis: {title: {text: 'PnL ($)'}, zerolinecolor: theme.secondary, color: theme.secondary}, }, width: 750, height: 600, @@ -99,8 +99,8 @@ const TPvsSLvsPnL3DPlot: React.FC = ({ backtests, theme r: 0, t: 0, }, - paper_bgcolor: '#121212', - plot_bgcolor: theme.secondary, + paper_bgcolor: "rgba(0,0,0,0)", + plot_bgcolor: "rgba(0,0,0,0)", }} config={{displayModeBar: false}} /> diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx index 7e42502..52b3e83 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx @@ -248,20 +248,34 @@ const BacktestGeneticBundle: React.FC = () => { accessor: 'timeframe', }, { - Header: 'Progress', + Header: 'Progress & Status', accessor: 'currentGeneration', Cell: ({value, row}: { value: number, row: any }) => { const generations = row.original.generations const currentGen = value || 0 const percentage = generations > 0 ? Math.round((currentGen / generations) * 100) : 0 + const status = row.original.status + // Calculate color based on percentage (red to green) + const getProgressColor = (percent: number) => { + if (percent <= 25) return 'progress-error' + if (percent <= 50) return 'progress-warning' + if (percent <= 75) return 'progress-info' + return 'progress-success' + } + return ( -
-
- {currentGen} / {generations} +
+
+ + {status} + +
+ {currentGen} / {generations} +
@@ -290,15 +304,7 @@ const BacktestGeneticBundle: React.FC = () => { {value} ), }, - { - Header: 'Status', - accessor: 'status', - Cell: ({value}: { value: string }) => ( - - {value} - - ), - }, + { Header: 'Actions', accessor: 'actions',