diff --git a/src/Managing.WebApp/src/components/organism/Charts/Fitness3DPlot.tsx b/src/Managing.WebApp/src/components/organism/Charts/Fitness3DPlot.tsx index eb96415..5f89c32 100644 --- a/src/Managing.WebApp/src/components/organism/Charts/Fitness3DPlot.tsx +++ b/src/Managing.WebApp/src/components/organism/Charts/Fitness3DPlot.tsx @@ -1,60 +1,107 @@ import React from 'react'; import Plot from 'react-plotly.js'; +import {Backtest} from '../../../generated/ManagingApi'; interface Fitness3DPlotProps { - results: any[]; + backtests: Backtest[]; theme: { secondary: string }; } -const Fitness3DPlot: React.FC = ({ results, theme }) => { +const Fitness3DPlot: React.FC = ({ backtests, theme }) => { const LOOPBACK_CONFIG = { singleIndicator: 1, multipleIndicators: { min: 5, max: 15 }, }; - const plotData = results.length > 0 ? [ + // Helper function to calculate fitness score from backtest data + const calculateFitnessScore = (backtest: Backtest): number => { + if (!backtest.statistics) return 0; + + const stats = backtest.statistics; + + // Multi-objective fitness function (matching the backend calculation) + const pnlScore = Math.max(0, (stats.totalPnL || 0) / 1000); // Normalize PnL + const winRateScore = (backtest.winRate || 0) / 100; // Normalize win rate + const riskRewardScore = Math.min(2, (stats.winningTrades || 0) / Math.max(1, Math.abs(stats.loosingTrades || 1))); + const consistencyScore = 1 - Math.abs((stats.totalPnL || 0) - (backtest.finalPnl || 0)) / Math.max(1, Math.abs(stats.totalPnL || 1)); + + // Risk-reward ratio bonus + const riskRewardRatio = (backtest.config.moneyManagement?.takeProfit || 0) / (backtest.config.moneyManagement?.stopLoss || 1); + const riskRewardBonus = Math.min(0.2, (riskRewardRatio - 1.1) * 0.1); + + // Drawdown score (normalized to 0-1, where lower drawdown is better) + const maxDrawdownPc = Math.abs(stats.maxDrawdownPc || 0); + const drawdownScore = Math.max(0, 1 - (maxDrawdownPc / 50)); + + // Weighted combination + const fitness = + pnlScore * 0.3 + + winRateScore * 0.2 + + riskRewardScore * 0.2 + + consistencyScore * 0.1 + + riskRewardBonus * 0.1 + + drawdownScore * 0.1; + + return Math.max(0, fitness); + }; + + // Helper function to get generation from metadata + const getGeneration = (backtest: Backtest): number => { + if (backtest.metadata && typeof backtest.metadata === 'object' && 'generation' in backtest.metadata) { + return (backtest.metadata as any).generation || 0; + } + return 0; + }; + + // Helper function to get indicator details from config + const getIndicatorDetails = (backtest: Backtest): string => { + if (!backtest.config.scenario?.indicators) return 'Unknown'; + + return backtest.config.scenario.indicators.map((indicator: any) => { + let details = indicator.type?.toString() || 'Unknown'; + if (indicator.period) details += ` (${indicator.period})`; + if (indicator.fastPeriods && indicator.slowPeriods) details += ` (${indicator.fastPeriods}/${indicator.slowPeriods})`; + if (indicator.multiplier) details += ` (${indicator.multiplier.toFixed(1)}x)`; + return details; + }).join(', '); + }; + + const plotData = backtests.length > 0 ? [ { type: 'scatter3d' as const, mode: 'markers' as const, - x: results.map(r => r.fitness), - y: results.map(r => r.backtest?.score || 0), - z: results.map(r => r.backtest?.winRate || 0), + x: backtests.map(b => calculateFitnessScore(b)), + y: backtests.map(b => b.score || 0), + z: backtests.map(b => b.winRate || 0), marker: { size: 5, - color: results.map(r => r.generation), + color: backtests.map(b => getGeneration(b)), colorscale: 'Viridis' as const, opacity: 0.8, }, - text: results.map(r => { - const loopbackPeriod = r.individual.indicators.length === 1 - ? LOOPBACK_CONFIG.singleIndicator - : '5-15'; - const indicatorDetails = r.individual.indicators.map((indicator: any, index: number) => { - let details = indicator.type.toString(); - if (indicator.period) details += ` (${indicator.period})`; - if (indicator.fastPeriods && indicator.slowPeriods) details += ` (${indicator.fastPeriods}/${indicator.slowPeriods})`; - if (indicator.multiplier) details += ` (${indicator.multiplier.toFixed(1)}x)`; - return details; - }).join(', '); - const riskRewardRatio = r.individual.takeProfit / r.individual.stopLoss; + text: backtests.map(b => { + const generation = getGeneration(b); + const fitness = calculateFitnessScore(b); + const indicatorDetails = getIndicatorDetails(b); + const riskRewardRatio = (b.config.moneyManagement?.takeProfit || 0) / (b.config.moneyManagement?.stopLoss || 1); const riskRewardColor = riskRewardRatio >= 2 ? '🟢' : riskRewardRatio >= 1.5 ? '🟡' : '🔴'; - const maxDrawdownPc = Math.abs(r.backtest?.statistics?.maxDrawdownPc || 0); + const maxDrawdownPc = Math.abs(b.statistics?.maxDrawdownPc || 0); const drawdownColor = maxDrawdownPc <= 10 ? '🟢' : maxDrawdownPc <= 25 ? '🟡' : '🔴'; - return `Phase: ${r.phase || 'Unknown'}
` + - `Gen: ${r.generation}
` + - `Fitness: ${r.fitness.toFixed(2)}
` + - `PnL: $${r.backtest?.statistics?.totalPnL?.toFixed(2) || 'N/A'}
` + - `Win Rate: ${r.backtest?.winRate?.toFixed(1) || 'N/A'}%
` + + + return `Generation: ${generation}
` + + `Fitness: ${fitness.toFixed(2)}
` + + `PnL: $${b.statistics?.totalPnL?.toFixed(2) || 'N/A'}
` + + `Win Rate: ${b.winRate?.toFixed(1) || 'N/A'}%
` + `Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%
` + - `SL: ${r.individual.stopLoss.toFixed(1)}%
` + - `TP: ${r.individual.takeProfit.toFixed(1)}%
` + + `SL: ${b.config.moneyManagement?.stopLoss?.toFixed(1) || 'N/A'}%
` + + `TP: ${b.config.moneyManagement?.takeProfit?.toFixed(1) || 'N/A'}%
` + `R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}
` + - `Leverage: ${r.individual.leverage}x
` + - `Cooldown: ${r.individual.cooldownPeriod} candles
` + - `Max Loss Streak: ${r.individual.maxLossStreak}
` + - `Max Time: ${r.individual.maxPositionTimeHours}h
` + + `Leverage: ${b.config.moneyManagement?.leverage || 1}x
` + + `Cooldown: ${b.config.cooldownPeriod || 0} candles
` + + `Max Loss Streak: ${b.config.maxLossStreak || 0}
` + + `Max Time: ${b.config.maxPositionTimeHours || 0}h
` + `Indicators: ${indicatorDetails}
` + - `Loopback: ${loopbackPeriod}`; + `Loopback: ${b.config.scenario?.loopbackPeriod || 'Unknown'}`; }), hovertemplate: '%{text}', }, diff --git a/src/Managing.WebApp/src/components/organism/Charts/IndicatorsComparison.tsx b/src/Managing.WebApp/src/components/organism/Charts/IndicatorsComparison.tsx index 4db3c4a..7eb8937 100644 --- a/src/Managing.WebApp/src/components/organism/Charts/IndicatorsComparison.tsx +++ b/src/Managing.WebApp/src/components/organism/Charts/IndicatorsComparison.tsx @@ -3,49 +3,57 @@ import Plot from 'react-plotly.js' import useTheme from '../../../hooks/useTheme' import {Backtest} from '../../../generated/ManagingApi' -interface StrategyResult { - individual: { - stopLoss: number - takeProfit: number - leverage: number - indicators: Array<{ - type: string - period?: number - fastPeriods?: number - slowPeriods?: number - signalPeriods?: number - multiplier?: number - stochPeriods?: number - smoothPeriods?: number - cyclePeriods?: number - }> - } - backtest?: { - statistics?: { - totalPnL?: number - maxDrawdownPc?: number - sharpeRatio?: number - hodlPnL?: number // Assume this is available in statistics - numPositions?: number // Number of trades/positions - } - winRate?: number - score?: number - } - fitness: number - generation: number - phase: string -} - interface IndicatorsComparisonProps { - results: StrategyResult[] + backtests: Backtest[] } -const IndicatorsComparison: React.FC = ({ results }) => { +const IndicatorsComparison: React.FC = ({ backtests }) => { const theme = useTheme().themeProperty(); - // Filter results to only include successful backtests - const validResults = results.filter(r => r.backtest && r.backtest.statistics) - if (validResults.length === 0) { + // Helper function to calculate fitness score from backtest data + const calculateFitnessScore = (backtest: Backtest): number => { + if (!backtest.statistics) return 0; + + const stats = backtest.statistics; + + // Multi-objective fitness function (matching the backend calculation) + const pnlScore = Math.max(0, (stats.totalPnL || 0) / 1000); // Normalize PnL + const winRateScore = (backtest.winRate || 0) / 100; // Normalize win rate + const riskRewardScore = Math.min(2, (stats.winningTrades || 0) / Math.max(1, Math.abs(stats.loosingTrades || 1))); + const consistencyScore = 1 - Math.abs((stats.totalPnL || 0) - (backtest.finalPnl || 0)) / Math.max(1, Math.abs(stats.totalPnL || 1)); + + // Risk-reward ratio bonus + const riskRewardRatio = (backtest.config.moneyManagement?.takeProfit || 0) / (backtest.config.moneyManagement?.stopLoss || 1); + const riskRewardBonus = Math.min(0.2, (riskRewardRatio - 1.1) * 0.1); + + // Drawdown score (normalized to 0-1, where lower drawdown is better) + const maxDrawdownPc = Math.abs(stats.maxDrawdownPc || 0); + const drawdownScore = Math.max(0, 1 - (maxDrawdownPc / 50)); + + // Weighted combination + const fitness = + pnlScore * 0.3 + + winRateScore * 0.2 + + riskRewardScore * 0.2 + + consistencyScore * 0.1 + + riskRewardBonus * 0.1 + + drawdownScore * 0.1; + + return Math.max(0, fitness); + }; + + // Helper function to get generation from metadata + const getGeneration = (backtest: Backtest): number => { + if (backtest.metadata && typeof backtest.metadata === 'object' && 'generation' in backtest.metadata) { + return (backtest.metadata as any).generation || 0; + } + return 0; + }; + + // Filter backtests to only include successful ones + const validBacktests = backtests.filter(b => b.statistics) + + if (validBacktests.length === 0) { return (
@@ -75,66 +83,67 @@ const IndicatorsComparison: React.FC = ({ results }) bestNumPositions: number bestMaxDrawdown: number avgDrawdown: number - phase: string generation: number }>() - validResults.forEach(result => { - result.individual.indicators.forEach(indicator => { - const key = indicator.type - const existing = indicatorStats.get(key) - const sharpe = result.backtest?.statistics?.sharpeRatio ?? 0 - const hodlPnl = result.backtest?.statistics?.hodlPnL ?? 0 - const pnl = result.backtest?.statistics?.totalPnL ?? 0 - const pnlVsHodl = pnl - hodlPnl - const numPositions = (result.backtest as Backtest).positions?.length ?? 0 - const avgDrawdown = Math.abs(result.backtest?.statistics?.maxDrawdownPc ?? 0) + validBacktests.forEach(backtest => { + if (!backtest.config.scenario?.indicators) return; + + backtest.config.scenario.indicators.forEach((indicator: any) => { + const key = indicator.type?.toString() || 'Unknown'; + const existing = indicatorStats.get(key); + const sharpe = backtest.statistics?.sharpeRatio ?? 0; + const pnl = backtest.statistics?.totalPnL ?? 0; + // Calculate PnL vs HODL using the hodlPercentage from backtest + const hodlPnl = (backtest.hodlPercentage / 100) * (backtest.config.botTradingBalance || 10000); + const pnlVsHodl = pnl - hodlPnl; + const numPositions = backtest.positions?.length ?? 0; + const avgDrawdown = Math.abs(backtest.statistics?.maxDrawdownPc ?? 0); + const fitness = calculateFitnessScore(backtest); + const generation = getGeneration(backtest); - console.log(result.backtest) if (existing) { - existing.count++ - existing.totalPnl += pnl - existing.totalWinRate += result.backtest?.winRate || 0 - existing.totalScore += result.backtest?.score || 0 - existing.totalFitness += result.fitness - existing.totalDrawdown += avgDrawdown + existing.count++; + existing.totalPnl += pnl; + existing.totalWinRate += backtest.winRate || 0; + existing.totalScore += backtest.score || 0; + existing.totalFitness += fitness; + existing.totalDrawdown += avgDrawdown; // Track best performance - if (result.fitness > existing.bestFitness) { - existing.bestFitness = result.fitness - existing.bestPnl = pnl - existing.bestWinRate = result.backtest?.winRate || 0 - existing.bestScore = result.backtest?.score || 0 - existing.bestSharpe = sharpe - existing.bestHodlPnl = hodlPnl - existing.bestPnlVsHodl = pnlVsHodl - existing.bestNumPositions = numPositions - existing.bestMaxDrawdown = avgDrawdown - existing.avgDrawdown = existing.totalDrawdown / existing.count - existing.phase = result.phase - existing.generation = result.generation + if (fitness > existing.bestFitness) { + existing.bestFitness = fitness; + existing.bestPnl = pnl; + existing.bestWinRate = backtest.winRate || 0; + existing.bestScore = backtest.score || 0; + existing.bestSharpe = sharpe; + existing.bestHodlPnl = hodlPnl; + existing.bestPnlVsHodl = pnlVsHodl; + existing.bestNumPositions = numPositions; + existing.bestMaxDrawdown = avgDrawdown; + existing.avgDrawdown = existing.totalDrawdown / existing.count; + existing.generation = generation; } } else { indicatorStats.set(key, { type: key, count: 1, totalPnl: pnl, - totalWinRate: result.backtest?.winRate || 0, - totalScore: result.backtest?.score || 0, - totalFitness: result.fitness, + totalWinRate: backtest.winRate || 0, + totalScore: backtest.score || 0, + totalFitness: fitness, totalDrawdown: avgDrawdown, - bestFitness: result.fitness, + bestFitness: fitness, bestPnl: pnl, - bestWinRate: result.backtest?.winRate || 0, - bestScore: result.backtest?.score || 0, + bestWinRate: backtest.winRate || 0, + bestScore: backtest.score || 0, bestSharpe: sharpe, bestHodlPnl: hodlPnl, bestPnlVsHodl: pnlVsHodl, bestNumPositions: numPositions, bestMaxDrawdown: avgDrawdown, avgDrawdown: avgDrawdown, - phase: result.phase, - generation: result.generation, + generation: generation, }) } }) @@ -193,9 +202,8 @@ const IndicatorsComparison: React.FC = ({ results }) 'Normalized: %{y:.2f}
' + 'Count: %{customdata[1]}
' + 'Best Gen: %{customdata[2]}
' + - 'Phase: %{customdata[3]}
' + '', - customdata: normalizedIndicators.map(d => [d.bestPnl, d.count, d.generation, d.phase]) + customdata: normalizedIndicators.map(d => [d.bestPnl, d.count, d.generation]) }, { type: 'bar' as const, @@ -212,9 +220,8 @@ const IndicatorsComparison: React.FC = ({ results }) 'Normalized: %{y:.2f}
' + 'Count: %{customdata[1]}
' + 'Best Gen: %{customdata[2]}
' + - 'Phase: %{customdata[3]}
' + '', - customdata: normalizedIndicators.map(d => [d.bestWinRate, d.count, d.generation, d.phase]) + customdata: normalizedIndicators.map(d => [d.bestWinRate, d.count, d.generation]) }, { type: 'bar' as const, @@ -231,9 +238,8 @@ const IndicatorsComparison: React.FC = ({ results }) 'Normalized: %{y:.2f}
' + 'Count: %{customdata[1]}
' + 'Best Gen: %{customdata[2]}
' + - 'Phase: %{customdata[3]}
' + '', - customdata: normalizedIndicators.map(d => [d.bestScore, d.count, d.generation, d.phase]) + customdata: normalizedIndicators.map(d => [d.bestScore, d.count, d.generation]) }, { type: 'bar' as const, @@ -250,9 +256,8 @@ const IndicatorsComparison: React.FC = ({ results }) 'Normalized: %{y:.2f}
' + 'Count: %{customdata[1]}
' + 'Best Gen: %{customdata[2]}
' + - 'Phase: %{customdata[3]}
' + '', - customdata: normalizedIndicators.map(d => [d.bestFitness, d.count, d.generation, d.phase]) + customdata: normalizedIndicators.map(d => [d.bestFitness, d.count, d.generation]) }, { type: 'bar' as const, @@ -269,9 +274,8 @@ const IndicatorsComparison: React.FC = ({ results }) 'Normalized: %{y:.2f}
' + 'Count: %{customdata[1]}
' + 'Best Gen: %{customdata[2]}
' + - 'Phase: %{customdata[3]}
' + '', - customdata: normalizedIndicators.map(d => [d.bestSharpe, d.count, d.generation, d.phase]) + customdata: normalizedIndicators.map(d => [d.bestSharpe, d.count, d.generation]) }, { type: 'bar' as const, @@ -279,36 +283,35 @@ const IndicatorsComparison: React.FC = ({ results }) y: normalizedIndicators.map(d => d.normPnlVsHodl), name: 'PnL vs HODL', marker: { - color: '#A259F7', + color: '#FF8C42', opacity: 0.8 }, hovertemplate: '%{x}
' + - 'PnL vs HODL (raw): %{customdata[0]:.2f}
' + + 'PnL vs HODL (raw): $%{customdata[0]:.2f}
' + 'Normalized: %{y:.2f}
' + 'Count: %{customdata[1]}
' + 'Best Gen: %{customdata[2]}
' + - 'Phase: %{customdata[3]}
' + '', - customdata: normalizedIndicators.map(d => [d.bestPnlVsHodl, d.count, d.generation, d.phase]) + customdata: normalizedIndicators.map(d => [d.bestPnlVsHodl, d.count, d.generation]) }, { type: 'bar' as const, x: normalizedIndicators.map(d => d.type), y: normalizedIndicators.map(d => d.normNumPositions), - name: 'Num Positions', + name: 'Trade Count', marker: { - color: '#FF8C00', + color: '#9B59B6', opacity: 0.8 }, hovertemplate: '%{x}
' + - 'Num Positions (raw): %{y}
' + - 'Count: %{customdata[0]}
' + - 'Best Gen: %{customdata[1]}
' + - 'Phase: %{customdata[2]}
' + + 'Trade Count (raw): %{customdata[0]}
' + + 'Normalized: %{y:.2f}
' + + 'Count: %{customdata[1]}
' + + 'Best Gen: %{customdata[2]}
' + '', - customdata: normalizedIndicators.map(d => [d.count, d.generation, d.phase]) + customdata: normalizedIndicators.map(d => [d.bestNumPositions, d.count, d.generation]) }, { type: 'bar' as const, @@ -316,19 +319,18 @@ const IndicatorsComparison: React.FC = ({ results }) y: normalizedIndicators.map(d => d.normAvgDrawdown), name: 'Avg Drawdown', marker: { - color: '#B22222', + color: '#E74C3C', opacity: 0.8 }, hovertemplate: '%{x}
' + - 'Avg Drawdown (raw): %{customdata[0]:.2f}%
' + + 'Avg Drawdown (raw): %{customdata[0]:.1f}%
' + 'Normalized: %{y:.2f}
' + 'Count: %{customdata[1]}
' + 'Best Gen: %{customdata[2]}
' + - 'Phase: %{customdata[3]}
' + '', - customdata: normalizedIndicators.map(d => [d.avgDrawdown, d.count, d.generation, d.phase]) - }, + customdata: normalizedIndicators.map(d => [d.avgDrawdown, d.count, d.generation]) + } ] return ( @@ -390,7 +392,7 @@ const IndicatorsComparison: React.FC = ({ results }) {/* Detailed indicator table */}
-

Detailed Indicator Performance:

+

Detailed Performance Summary

@@ -401,76 +403,31 @@ const IndicatorsComparison: React.FC = ({ results }) - - - - + - - {normalizedIndicators.map((indicator, index) => ( + {sortedIndicators.map((indicator, index) => ( + + + + - - - - - - - - ))}
Best Win Rate Best Score Best FitnessSharpe RatioPnL vs HODLNum PositionsAvg DrawdownBest Sharpe Best GenPhase
{indicator.type} {indicator.count} = 0 ? 'text-success' : 'text-error'}> - ${indicator.bestPnl.toFixed(2)} ({indicator.normPnl.toFixed(2)}) + ${indicator.bestPnl.toFixed(2)} + {indicator.bestWinRate.toFixed(1)}%{indicator.bestScore.toFixed(2)}{indicator.bestFitness.toFixed(3)}= 0 ? 'text-success' : 'text-error'}> + {indicator.bestSharpe.toFixed(3)} {indicator.bestWinRate.toFixed(1)}% ({indicator.normWinRate.toFixed(2)}){indicator.bestScore.toFixed(2)} ({indicator.normScore.toFixed(2)}){indicator.bestFitness.toFixed(3)} ({indicator.normFitness.toFixed(2)}){indicator.bestSharpe.toFixed(3)} ({indicator.normSharpe.toFixed(2)}){indicator.bestPnlVsHodl.toFixed(2)} ({indicator.normPnlVsHodl.toFixed(2)}){indicator.bestNumPositions} ({indicator.normNumPositions.toFixed(2)}){indicator.avgDrawdown.toFixed(2)}% ({indicator.normAvgDrawdown.toFixed(2)}) {indicator.generation}{indicator.phase}
- - {/* Performance summary */} -
-
-
Top Performer
-
{normalizedIndicators[0]?.type}
-
Fitness: {normalizedIndicators[0]?.bestFitness.toFixed(3)} ({normalizedIndicators[0]?.normFitness.toFixed(2)})
-
- -
-
Best PnL
-
- {normalizedIndicators.reduce((max, curr) => curr.bestPnl > max.bestPnl ? curr : max).type} -
-
- ${normalizedIndicators.reduce((max, curr) => curr.bestPnl > max.bestPnl ? curr : max).bestPnl.toFixed(2)} ({normalizedIndicators.reduce((max, curr) => curr.bestPnl > max.bestPnl ? curr : max).normPnl.toFixed(2)}) -
-
- -
-
Best Win Rate
-
- {normalizedIndicators.reduce((max, curr) => curr.bestWinRate > max.bestWinRate ? curr : max).type} -
-
- {normalizedIndicators.reduce((max, curr) => curr.bestWinRate > max.bestWinRate ? curr : max).bestWinRate.toFixed(1)}% ({normalizedIndicators.reduce((max, curr) => curr.bestWinRate > max.bestWinRate ? curr : max).normWinRate.toFixed(2)}) -
-
- -
-
Most Tested
-
- {normalizedIndicators.reduce((max, curr) => curr.count > max.count ? curr : max).type} -
-
- {normalizedIndicators.reduce((max, curr) => curr.count > max.count ? curr : max).count} tests -
-
-
) diff --git a/src/Managing.WebApp/src/components/organism/Charts/ScoreVsGeneration.tsx b/src/Managing.WebApp/src/components/organism/Charts/ScoreVsGeneration.tsx new file mode 100644 index 0000000..8ff2176 --- /dev/null +++ b/src/Managing.WebApp/src/components/organism/Charts/ScoreVsGeneration.tsx @@ -0,0 +1,170 @@ +import React from 'react' +import Plot from 'react-plotly.js' +import {Backtest} from '../../../generated/ManagingApi' + +interface ScoreVsGenerationProps { + backtests: Backtest[] + theme: { secondary: string } +} + +const ScoreVsGeneration: React.FC = ({ backtests, theme }) => { + // Helper function to get generation from metadata + const getGeneration = (backtest: Backtest): number => { + if (backtest.metadata && typeof backtest.metadata === 'object' && 'generation' in backtest.metadata) { + return (backtest.metadata as any).generation || 0 + } + return 0 + } + + // Helper function to get score from backtest + const getScore = (backtest: Backtest): number => { + return backtest.score || 0 + } + + // Prepare data for the chart + const chartData = backtests.length > 0 ? [ + { + type: 'scatter' as const, + mode: 'lines+markers' as const, + x: backtests.map(backtest => getGeneration(backtest)), + y: backtests.map(backtest => getScore(backtest)), + marker: { + size: 8, + color: backtests.map(backtest => { + const score = getScore(backtest) + // Color gradient: red for low score, yellow for medium, green for high + if (score < 0.3) return '#ff4444' // Red + if (score < 0.6) return '#ffaa00' // Yellow/Orange + return '#00aa44' // Green + }), + opacity: 0.8, + line: { + color: theme.secondary, + width: 1 + } + }, + line: { + color: theme.secondary, + width: 2, + opacity: 0.6 + }, + text: backtests.map(backtest => { + const score = getScore(backtest) + const generation = getGeneration(backtest) + const pnl = backtest.statistics?.totalPnL || 0 + const winRate = backtest.winRate || 0 + const maxDrawdown = Math.abs(backtest.statistics?.maxDrawdownPc || 0) + + return `Generation: ${generation}
` + + `Score: ${score.toFixed(3)}
` + + `PnL: $${pnl.toFixed(2)}
` + + `Win Rate: ${winRate.toFixed(1)}%
` + + `Max Drawdown: ${maxDrawdown.toFixed(1)}%
` + + `Ticker: ${backtest.config?.ticker || 'N/A'}
` + + `Timeframe: ${backtest.config?.timeframe || 'N/A'}` + }), + hovertemplate: '%{text}', + name: 'Score' + } + ] : [] + + // Calculate trend line if we have enough data points + const trendLineData = backtests.length > 2 ? (() => { + const generations = backtests.map(backtest => getGeneration(backtest)) + const scores = backtests.map(backtest => getScore(backtest)) + + // Simple linear regression + const n = generations.length + const sumX = generations.reduce((sum, x) => sum + x, 0) + const sumY = scores.reduce((sum, y) => sum + y, 0) + const sumXY = generations.reduce((sum, x, i) => sum + x * scores[i], 0) + const sumXX = generations.reduce((sum, x) => sum + x * x, 0) + + const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX) + const intercept = (sumY - slope * sumX) / n + + const minGen = Math.min(...generations) + const maxGen = Math.max(...generations) + + return [{ + type: 'scatter' as const, + mode: 'lines' as const, + x: [minGen, maxGen], + y: [slope * minGen + intercept, slope * maxGen + intercept], + line: { + color: '#888888', + width: 2, + dash: 'dash' as const + }, + name: 'Trend Line', + showlegend: true + }] + })() : [] + + const layout = { + title: { + text: 'Score vs Generation', + font: { + color: theme.secondary + } + }, + xaxis: { + title: { + text: 'Generation Number', + font: { + color: theme.secondary + } + }, + gridcolor: theme.secondary, + zerolinecolor: theme.secondary, + color: theme.secondary + }, + yaxis: { + title: { + text: 'Score', + font: { + color: theme.secondary + } + }, + gridcolor: theme.secondary, + zerolinecolor: theme.secondary, + color: theme.secondary + }, + plot_bgcolor: 'rgba(0,0,0,0)', + paper_bgcolor: 'rgba(0,0,0,0)', + font: { + color: theme.secondary + }, + legend: { + font: { + color: theme.secondary + } + }, + margin: { + l: 60, + r: 30, + t: 60, + b: 60 + } + } + + const config = { + displayModeBar: true, + displaylogo: false, + responsive: true + } + + return ( +
+ +
+ ) +} + +export default ScoreVsGeneration \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx b/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx index ab6dbb6..cadbd52 100644 --- a/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx +++ b/src/Managing.WebApp/src/components/organism/Charts/TPvsSLvsPnL3DPlot.tsx @@ -1,8 +1,9 @@ import React from 'react'; import Plot from 'react-plotly.js'; +import {Backtest} from '../../../generated/ManagingApi'; interface TPvsSLvsPnL3DPlotProps { - results: any[]; + backtests: Backtest[]; theme: { secondary: string }; } @@ -11,18 +12,39 @@ const LOOPBACK_CONFIG = { multipleIndicators: { min: 5, max: 15 }, }; -const TPvsSLvsPnL3DPlot: React.FC = ({ results, theme }) => { - const plotDataTPvsSL = results.length > 0 ? [ +const TPvsSLvsPnL3DPlot: React.FC = ({ backtests, theme }) => { + // Helper function to get generation from metadata + const getGeneration = (backtest: Backtest): number => { + if (backtest.metadata && typeof backtest.metadata === 'object' && 'generation' in backtest.metadata) { + return (backtest.metadata as any).generation || 0; + } + return 0; + }; + + // Helper function to get indicator details from config + const getIndicatorDetails = (backtest: Backtest): string => { + if (!backtest.config.scenario?.indicators) return 'Unknown'; + + return backtest.config.scenario.indicators.map((indicator: any) => { + let details = indicator.type?.toString() || 'Unknown'; + if (indicator.period) details += ` (${indicator.period})`; + if (indicator.fastPeriods && indicator.slowPeriods) details += ` (${indicator.fastPeriods}/${indicator.slowPeriods})`; + if (indicator.multiplier) details += ` (${indicator.multiplier.toFixed(1)}x)`; + return details; + }).join(', '); + }; + + const plotDataTPvsSL = backtests.length > 0 ? [ { type: 'scatter3d' as const, mode: 'markers' as const, - x: results.map(r => r.individual.takeProfit), - y: results.map(r => r.individual.stopLoss), - z: results.map(r => r.backtest?.statistics?.totalPnL || 0), + x: backtests.map(b => b.config.moneyManagement?.takeProfit || 0), + y: backtests.map(b => b.config.moneyManagement?.stopLoss || 0), + z: backtests.map(b => b.statistics?.totalPnL || 0), marker: { size: 5, - color: results.map(r => { - const pnl = r.backtest?.statistics?.totalPnL || 0; + color: backtests.map(b => { + const pnl = b.statistics?.totalPnL || 0; return pnl > 0 ? pnl : 0; }), colorscale: 'RdYlGn' as const, @@ -32,36 +54,27 @@ const TPvsSLvsPnL3DPlot: React.FC = ({ results, theme }) titleside: 'right', }, }, - text: results.map(r => { - const loopbackPeriod = r.individual.indicators.length === 1 - ? LOOPBACK_CONFIG.singleIndicator - : '5-15'; - const indicatorDetails = r.individual.indicators.map((indicator: any, index: number) => { - let details = indicator.type.toString(); - if (indicator.period) details += ` (${indicator.period})`; - if (indicator.fastPeriods && indicator.slowPeriods) details += ` (${indicator.fastPeriods}/${indicator.slowPeriods})`; - if (indicator.multiplier) details += ` (${indicator.multiplier.toFixed(1)}x)`; - return details; - }).join(', '); - const riskRewardRatio = r.individual.takeProfit / r.individual.stopLoss; + text: backtests.map(b => { + const generation = getGeneration(b); + const indicatorDetails = getIndicatorDetails(b); + const riskRewardRatio = (b.config.moneyManagement?.takeProfit || 0) / (b.config.moneyManagement?.stopLoss || 1); const riskRewardColor = riskRewardRatio >= 2 ? '🟢' : riskRewardRatio >= 1.5 ? '🟡' : '🔴'; - const maxDrawdownPc = Math.abs(r.backtest?.statistics?.maxDrawdownPc || 0); + const maxDrawdownPc = Math.abs(b.statistics?.maxDrawdownPc || 0); const drawdownColor = maxDrawdownPc <= 10 ? '🟢' : maxDrawdownPc <= 25 ? '🟡' : '🔴'; - return `Phase: ${r.phase || 'Unknown'}
` + - `Gen: ${r.generation}
` + - `Fitness: ${r.fitness.toFixed(2)}
` + - `PnL: $${r.backtest?.statistics?.totalPnL?.toFixed(2) || 'N/A'}
` + - `Win Rate: ${r.backtest?.winRate?.toFixed(1) || 'N/A'}%
` + + + return `Generation: ${generation}
` + + `PnL: $${b.statistics?.totalPnL?.toFixed(2) || 'N/A'}
` + + `Win Rate: ${b.winRate?.toFixed(1) || 'N/A'}%
` + `Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%
` + - `SL: ${r.individual.stopLoss.toFixed(1)}%
` + - `TP: ${r.individual.takeProfit.toFixed(1)}%
` + + `SL: ${b.config.moneyManagement?.stopLoss?.toFixed(1) || 'N/A'}%
` + + `TP: ${b.config.moneyManagement?.takeProfit?.toFixed(1) || 'N/A'}%
` + `R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}
` + - `Leverage: ${r.individual.leverage}x
` + - `Cooldown: ${r.individual.cooldownPeriod} candles
` + - `Max Loss Streak: ${r.individual.maxLossStreak}
` + - `Max Time: ${r.individual.maxPositionTimeHours}h
` + + `Leverage: ${b.config.moneyManagement?.leverage || 1}x
` + + `Cooldown: ${b.config.cooldownPeriod || 0} candles
` + + `Max Loss Streak: ${b.config.maxLossStreak || 0}
` + + `Max Time: ${b.config.maxPositionTimeHours || 0}h
` + `Indicators: ${indicatorDetails}
` + - `Loopback: ${loopbackPeriod}`; + `Loopback: ${b.config.scenario?.loopbackPeriod || 'Unknown'}`; }), hovertemplate: '%{text}', }, diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 0161a53..5c2523f 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -3289,7 +3289,7 @@ export interface Backtest { user: User; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; score: number; - requestId?: string | null; + requestId?: string; metadata?: any | null; } diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 6d4b31e..5e477c0 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -236,7 +236,7 @@ export interface Backtest { user: User; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; score: number; - requestId?: string | null; + requestId?: string; metadata?: any | null; } diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx index 7685d7e..60d5f4a 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx @@ -2035,7 +2035,7 @@ const BacktestGenetic: React.FC = () => {

Fitness vs Score vs Win Rate

- + r.backtest).filter(Boolean) as Backtest[]} theme={theme} />
@@ -2043,14 +2043,14 @@ const BacktestGenetic: React.FC = () => {

Take Profit vs Stop Loss vs PnL

- + r.backtest).filter(Boolean) as Backtest[]} theme={theme} />
{/* Strategy Comparison Radar Chart */}
- + r.backtest).filter(Boolean) as Backtest[]} />
{/* Results Table */} diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx index 804afba..5181d4a 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx @@ -17,6 +17,11 @@ import {Toast} from '../../components/mollecules' import Table from '../../components/mollecules/Table/Table' import BacktestTable from '../../components/organism/Backtest/backtestTable' import Modal from '../../components/mollecules/Modal/Modal' +import Fitness3DPlot from '../../components/organism/Charts/Fitness3DPlot' +import TPvsSLvsPnL3DPlot from '../../components/organism/Charts/TPvsSLvsPnL3DPlot' +import IndicatorsComparison from '../../components/organism/Charts/IndicatorsComparison' +import ScoreVsGeneration from '../../components/organism/Charts/ScoreVsGeneration' +import useTheme from '../../hooks/useTheme' // Available Indicator Types const ALL_INDICATORS = [ @@ -55,6 +60,7 @@ interface GeneticBundleFormData { const BacktestGeneticBundle: React.FC = () => { const {apiUrl} = useApiUrlStore() const backtestClient = new BacktestClient({}, apiUrl) + const theme = useTheme().themeProperty() // State const [isSubmitting, setIsSubmitting] = useState(false) @@ -552,11 +558,46 @@ const BacktestGeneticBundle: React.FC = () => { ) : backtests.length > 0 ? ( - + <> + {/* Score vs Generation Chart */} +
+
+
+

Score vs Generation

+ +
+
+
+ +
+ {/* Fitness vs Score vs Win Rate */} +
+
+

Fitness vs Score vs Win Rate

+ +
+
+ + {/* TP% vs SL% vs PnL */} +
+
+

Take Profit vs Stop Loss vs PnL

+ +
+
+
+ + {/* Strategy Comparison Radar Chart */} +
+ +
+ + + ) : (
No backtest results found for this request.