Add chart for genetic bundle

This commit is contained in:
2025-07-11 20:03:24 +07:00
parent 47ef0cf2d5
commit 8a2b0ba323
8 changed files with 461 additions and 233 deletions

View File

@@ -1,60 +1,107 @@
import React from 'react'; import React from 'react';
import Plot from 'react-plotly.js'; import Plot from 'react-plotly.js';
import {Backtest} from '../../../generated/ManagingApi';
interface Fitness3DPlotProps { interface Fitness3DPlotProps {
results: any[]; backtests: Backtest[];
theme: { secondary: string }; theme: { secondary: string };
} }
const Fitness3DPlot: React.FC<Fitness3DPlotProps> = ({ results, theme }) => { const Fitness3DPlot: React.FC<Fitness3DPlotProps> = ({ backtests, theme }) => {
const LOOPBACK_CONFIG = { const LOOPBACK_CONFIG = {
singleIndicator: 1, singleIndicator: 1,
multipleIndicators: { min: 5, max: 15 }, multipleIndicators: { min: 5, max: 15 },
}; };
const plotData = results.length > 0 ? [ // Helper function to calculate fitness score from backtest data
{ const calculateFitnessScore = (backtest: Backtest): number => {
type: 'scatter3d' as const, if (!backtest.statistics) return 0;
mode: 'markers' as const,
x: results.map(r => r.fitness), const stats = backtest.statistics;
y: results.map(r => r.backtest?.score || 0),
z: results.map(r => r.backtest?.winRate || 0), // Multi-objective fitness function (matching the backend calculation)
marker: { const pnlScore = Math.max(0, (stats.totalPnL || 0) / 1000); // Normalize PnL
size: 5, const winRateScore = (backtest.winRate || 0) / 100; // Normalize win rate
color: results.map(r => r.generation), const riskRewardScore = Math.min(2, (stats.winningTrades || 0) / Math.max(1, Math.abs(stats.loosingTrades || 1)));
colorscale: 'Viridis' as const, const consistencyScore = 1 - Math.abs((stats.totalPnL || 0) - (backtest.finalPnl || 0)) / Math.max(1, Math.abs(stats.totalPnL || 1));
opacity: 0.8,
}, // Risk-reward ratio bonus
text: results.map(r => { const riskRewardRatio = (backtest.config.moneyManagement?.takeProfit || 0) / (backtest.config.moneyManagement?.stopLoss || 1);
const loopbackPeriod = r.individual.indicators.length === 1 const riskRewardBonus = Math.min(0.2, (riskRewardRatio - 1.1) * 0.1);
? LOOPBACK_CONFIG.singleIndicator
: '5-15'; // Drawdown score (normalized to 0-1, where lower drawdown is better)
const indicatorDetails = r.individual.indicators.map((indicator: any, index: number) => { const maxDrawdownPc = Math.abs(stats.maxDrawdownPc || 0);
let details = indicator.type.toString(); 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.period) details += ` (${indicator.period})`;
if (indicator.fastPeriods && indicator.slowPeriods) details += ` (${indicator.fastPeriods}/${indicator.slowPeriods})`; if (indicator.fastPeriods && indicator.slowPeriods) details += ` (${indicator.fastPeriods}/${indicator.slowPeriods})`;
if (indicator.multiplier) details += ` (${indicator.multiplier.toFixed(1)}x)`; if (indicator.multiplier) details += ` (${indicator.multiplier.toFixed(1)}x)`;
return details; return details;
}).join(', '); }).join(', ');
const riskRewardRatio = r.individual.takeProfit / r.individual.stopLoss; };
const plotData = backtests.length > 0 ? [
{
type: 'scatter3d' as const,
mode: 'markers' as const,
x: backtests.map(b => calculateFitnessScore(b)),
y: backtests.map(b => b.score || 0),
z: backtests.map(b => b.winRate || 0),
marker: {
size: 5,
color: backtests.map(b => getGeneration(b)),
colorscale: 'Viridis' as const,
opacity: 0.8,
},
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 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 ? '🟡' : '🔴'; const drawdownColor = maxDrawdownPc <= 10 ? '🟢' : maxDrawdownPc <= 25 ? '🟡' : '🔴';
return `Phase: ${r.phase || 'Unknown'}<br>` +
`Gen: ${r.generation}<br>` + return `Generation: ${generation}<br>` +
`Fitness: ${r.fitness.toFixed(2)}<br>` + `Fitness: ${fitness.toFixed(2)}<br>` +
`PnL: $${r.backtest?.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` + `PnL: $${b.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` +
`Win Rate: ${r.backtest?.winRate?.toFixed(1) || 'N/A'}%<br>` + `Win Rate: ${b.winRate?.toFixed(1) || 'N/A'}%<br>` +
`Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%<br>` + `Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%<br>` +
`SL: ${r.individual.stopLoss.toFixed(1)}%<br>` + `SL: ${b.config.moneyManagement?.stopLoss?.toFixed(1) || 'N/A'}%<br>` +
`TP: ${r.individual.takeProfit.toFixed(1)}%<br>` + `TP: ${b.config.moneyManagement?.takeProfit?.toFixed(1) || 'N/A'}%<br>` +
`R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}<br>` + `R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}<br>` +
`Leverage: ${r.individual.leverage}x<br>` + `Leverage: ${b.config.moneyManagement?.leverage || 1}x<br>` +
`Cooldown: ${r.individual.cooldownPeriod} candles<br>` + `Cooldown: ${b.config.cooldownPeriod || 0} candles<br>` +
`Max Loss Streak: ${r.individual.maxLossStreak}<br>` + `Max Loss Streak: ${b.config.maxLossStreak || 0}<br>` +
`Max Time: ${r.individual.maxPositionTimeHours}h<br>` + `Max Time: ${b.config.maxPositionTimeHours || 0}h<br>` +
`Indicators: ${indicatorDetails}<br>` + `Indicators: ${indicatorDetails}<br>` +
`Loopback: ${loopbackPeriod}`; `Loopback: ${b.config.scenario?.loopbackPeriod || 'Unknown'}`;
}), }),
hovertemplate: '%{text}<extra></extra>', hovertemplate: '%{text}<extra></extra>',
}, },

View File

@@ -3,49 +3,57 @@ import Plot from 'react-plotly.js'
import useTheme from '../../../hooks/useTheme' import useTheme from '../../../hooks/useTheme'
import {Backtest} from '../../../generated/ManagingApi' 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 { interface IndicatorsComparisonProps {
results: StrategyResult[] backtests: Backtest[]
} }
const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results }) => { const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ backtests }) => {
const theme = useTheme().themeProperty(); 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 ( return (
<div className="card bg-base-100 shadow-xl"> <div className="card bg-base-100 shadow-xl">
<div className="card-body"> <div className="card-body">
@@ -75,66 +83,67 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
bestNumPositions: number bestNumPositions: number
bestMaxDrawdown: number bestMaxDrawdown: number
avgDrawdown: number avgDrawdown: number
phase: string
generation: number generation: number
}>() }>()
validResults.forEach(result => { validBacktests.forEach(backtest => {
result.individual.indicators.forEach(indicator => { if (!backtest.config.scenario?.indicators) return;
const key = indicator.type
const existing = indicatorStats.get(key) backtest.config.scenario.indicators.forEach((indicator: any) => {
const sharpe = result.backtest?.statistics?.sharpeRatio ?? 0 const key = indicator.type?.toString() || 'Unknown';
const hodlPnl = result.backtest?.statistics?.hodlPnL ?? 0 const existing = indicatorStats.get(key);
const pnl = result.backtest?.statistics?.totalPnL ?? 0 const sharpe = backtest.statistics?.sharpeRatio ?? 0;
const pnlVsHodl = pnl - hodlPnl const pnl = backtest.statistics?.totalPnL ?? 0;
const numPositions = (result.backtest as Backtest).positions?.length ?? 0 // Calculate PnL vs HODL using the hodlPercentage from backtest
const avgDrawdown = Math.abs(result.backtest?.statistics?.maxDrawdownPc ?? 0) 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) { if (existing) {
existing.count++ existing.count++;
existing.totalPnl += pnl existing.totalPnl += pnl;
existing.totalWinRate += result.backtest?.winRate || 0 existing.totalWinRate += backtest.winRate || 0;
existing.totalScore += result.backtest?.score || 0 existing.totalScore += backtest.score || 0;
existing.totalFitness += result.fitness existing.totalFitness += fitness;
existing.totalDrawdown += avgDrawdown existing.totalDrawdown += avgDrawdown;
// Track best performance // Track best performance
if (result.fitness > existing.bestFitness) { if (fitness > existing.bestFitness) {
existing.bestFitness = result.fitness existing.bestFitness = fitness;
existing.bestPnl = pnl existing.bestPnl = pnl;
existing.bestWinRate = result.backtest?.winRate || 0 existing.bestWinRate = backtest.winRate || 0;
existing.bestScore = result.backtest?.score || 0 existing.bestScore = backtest.score || 0;
existing.bestSharpe = sharpe existing.bestSharpe = sharpe;
existing.bestHodlPnl = hodlPnl existing.bestHodlPnl = hodlPnl;
existing.bestPnlVsHodl = pnlVsHodl existing.bestPnlVsHodl = pnlVsHodl;
existing.bestNumPositions = numPositions existing.bestNumPositions = numPositions;
existing.bestMaxDrawdown = avgDrawdown existing.bestMaxDrawdown = avgDrawdown;
existing.avgDrawdown = existing.totalDrawdown / existing.count existing.avgDrawdown = existing.totalDrawdown / existing.count;
existing.phase = result.phase existing.generation = generation;
existing.generation = result.generation
} }
} else { } else {
indicatorStats.set(key, { indicatorStats.set(key, {
type: key, type: key,
count: 1, count: 1,
totalPnl: pnl, totalPnl: pnl,
totalWinRate: result.backtest?.winRate || 0, totalWinRate: backtest.winRate || 0,
totalScore: result.backtest?.score || 0, totalScore: backtest.score || 0,
totalFitness: result.fitness, totalFitness: fitness,
totalDrawdown: avgDrawdown, totalDrawdown: avgDrawdown,
bestFitness: result.fitness, bestFitness: fitness,
bestPnl: pnl, bestPnl: pnl,
bestWinRate: result.backtest?.winRate || 0, bestWinRate: backtest.winRate || 0,
bestScore: result.backtest?.score || 0, bestScore: backtest.score || 0,
bestSharpe: sharpe, bestSharpe: sharpe,
bestHodlPnl: hodlPnl, bestHodlPnl: hodlPnl,
bestPnlVsHodl: pnlVsHodl, bestPnlVsHodl: pnlVsHodl,
bestNumPositions: numPositions, bestNumPositions: numPositions,
bestMaxDrawdown: avgDrawdown, bestMaxDrawdown: avgDrawdown,
avgDrawdown: avgDrawdown, avgDrawdown: avgDrawdown,
phase: result.phase, generation: generation,
generation: result.generation,
}) })
} }
}) })
@@ -193,9 +202,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
'<b>Normalized:</b> %{y:.2f}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Count:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Best Gen:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<b>Phase:</b> %{customdata[3]}<br>' +
'<extra></extra>', '<extra></extra>',
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, type: 'bar' as const,
@@ -212,9 +220,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
'<b>Normalized:</b> %{y:.2f}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Count:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Best Gen:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<b>Phase:</b> %{customdata[3]}<br>' +
'<extra></extra>', '<extra></extra>',
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, type: 'bar' as const,
@@ -231,9 +238,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
'<b>Normalized:</b> %{y:.2f}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Count:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Best Gen:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<b>Phase:</b> %{customdata[3]}<br>' +
'<extra></extra>', '<extra></extra>',
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, type: 'bar' as const,
@@ -250,9 +256,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
'<b>Normalized:</b> %{y:.2f}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Count:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Best Gen:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<b>Phase:</b> %{customdata[3]}<br>' +
'<extra></extra>', '<extra></extra>',
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, type: 'bar' as const,
@@ -269,9 +274,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
'<b>Normalized:</b> %{y:.2f}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Count:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Best Gen:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<b>Phase:</b> %{customdata[3]}<br>' +
'<extra></extra>', '<extra></extra>',
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, type: 'bar' as const,
@@ -279,36 +283,35 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
y: normalizedIndicators.map(d => d.normPnlVsHodl), y: normalizedIndicators.map(d => d.normPnlVsHodl),
name: 'PnL vs HODL', name: 'PnL vs HODL',
marker: { marker: {
color: '#A259F7', color: '#FF8C42',
opacity: 0.8 opacity: 0.8
}, },
hovertemplate: hovertemplate:
'<b>%{x}</b><br>' + '<b>%{x}</b><br>' +
'<b>PnL vs HODL (raw):</b> %{customdata[0]:.2f}<br>' + '<b>PnL vs HODL (raw):</b> $%{customdata[0]:.2f}<br>' +
'<b>Normalized:</b> %{y:.2f}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Count:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Best Gen:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<b>Phase:</b> %{customdata[3]}<br>' +
'<extra></extra>', '<extra></extra>',
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, type: 'bar' as const,
x: normalizedIndicators.map(d => d.type), x: normalizedIndicators.map(d => d.type),
y: normalizedIndicators.map(d => d.normNumPositions), y: normalizedIndicators.map(d => d.normNumPositions),
name: 'Num Positions', name: 'Trade Count',
marker: { marker: {
color: '#FF8C00', color: '#9B59B6',
opacity: 0.8 opacity: 0.8
}, },
hovertemplate: hovertemplate:
'<b>%{x}</b><br>' + '<b>%{x}</b><br>' +
'<b>Num Positions (raw):</b> %{y}<br>' + '<b>Trade Count (raw):</b> %{customdata[0]}<br>' +
'<b>Count:</b> %{customdata[0]}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Best Gen:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Phase:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<extra></extra>', '<extra></extra>',
customdata: normalizedIndicators.map(d => [d.count, d.generation, d.phase]) customdata: normalizedIndicators.map(d => [d.bestNumPositions, d.count, d.generation])
}, },
{ {
type: 'bar' as const, type: 'bar' as const,
@@ -316,19 +319,18 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
y: normalizedIndicators.map(d => d.normAvgDrawdown), y: normalizedIndicators.map(d => d.normAvgDrawdown),
name: 'Avg Drawdown', name: 'Avg Drawdown',
marker: { marker: {
color: '#B22222', color: '#E74C3C',
opacity: 0.8 opacity: 0.8
}, },
hovertemplate: hovertemplate:
'<b>%{x}</b><br>' + '<b>%{x}</b><br>' +
'<b>Avg Drawdown (raw):</b> %{customdata[0]:.2f}%<br>' + '<b>Avg Drawdown (raw):</b> %{customdata[0]:.1f}%<br>' +
'<b>Normalized:</b> %{y:.2f}<br>' + '<b>Normalized:</b> %{y:.2f}<br>' +
'<b>Count:</b> %{customdata[1]}<br>' + '<b>Count:</b> %{customdata[1]}<br>' +
'<b>Best Gen:</b> %{customdata[2]}<br>' + '<b>Best Gen:</b> %{customdata[2]}<br>' +
'<b>Phase:</b> %{customdata[3]}<br>' +
'<extra></extra>', '<extra></extra>',
customdata: normalizedIndicators.map(d => [d.avgDrawdown, d.count, d.generation, d.phase]) customdata: normalizedIndicators.map(d => [d.avgDrawdown, d.count, d.generation])
}, }
] ]
return ( return (
@@ -390,7 +392,7 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
{/* Detailed indicator table */} {/* Detailed indicator table */}
<div className="mt-6"> <div className="mt-6">
<h4 className="font-semibold mb-4">Detailed Indicator Performance:</h4> <h4 className="text-lg font-semibold mb-4">Detailed Performance Summary</h4>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="table table-zebra w-full"> <table className="table table-zebra w-full">
<thead> <thead>
@@ -401,76 +403,31 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
<th>Best Win Rate</th> <th>Best Win Rate</th>
<th>Best Score</th> <th>Best Score</th>
<th>Best Fitness</th> <th>Best Fitness</th>
<th>Sharpe Ratio</th> <th>Best Sharpe</th>
<th>PnL vs HODL</th>
<th>Num Positions</th>
<th>Avg Drawdown</th>
<th>Best Gen</th> <th>Best Gen</th>
<th>Phase</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{normalizedIndicators.map((indicator, index) => ( {sortedIndicators.map((indicator, index) => (
<tr key={indicator.type}> <tr key={indicator.type}>
<td className="font-medium">{indicator.type}</td> <td className="font-medium">{indicator.type}</td>
<td>{indicator.count}</td> <td>{indicator.count}</td>
<td className={indicator.bestPnl >= 0 ? 'text-success' : 'text-error'}> <td className={indicator.bestPnl >= 0 ? 'text-success' : 'text-error'}>
${indicator.bestPnl.toFixed(2)} <span className="text-xs">({indicator.normPnl.toFixed(2)})</span> ${indicator.bestPnl.toFixed(2)}
</td>
<td>{indicator.bestWinRate.toFixed(1)}%</td>
<td>{indicator.bestScore.toFixed(2)}</td>
<td>{indicator.bestFitness.toFixed(3)}</td>
<td className={indicator.bestSharpe >= 0 ? 'text-success' : 'text-error'}>
{indicator.bestSharpe.toFixed(3)}
</td> </td>
<td>{indicator.bestWinRate.toFixed(1)}% <span className="text-xs">({indicator.normWinRate.toFixed(2)})</span></td>
<td>{indicator.bestScore.toFixed(2)} <span className="text-xs">({indicator.normScore.toFixed(2)})</span></td>
<td className="font-semibold">{indicator.bestFitness.toFixed(3)} <span className="text-xs">({indicator.normFitness.toFixed(2)})</span></td>
<td>{indicator.bestSharpe.toFixed(3)} <span className="text-xs">({indicator.normSharpe.toFixed(2)})</span></td>
<td>{indicator.bestPnlVsHodl.toFixed(2)} <span className="text-xs">({indicator.normPnlVsHodl.toFixed(2)})</span></td>
<td>{indicator.bestNumPositions} <span className="text-xs">({indicator.normNumPositions.toFixed(2)})</span></td>
<td>{indicator.avgDrawdown.toFixed(2)}% <span className="text-xs">({indicator.normAvgDrawdown.toFixed(2)})</span></td>
<td>{indicator.generation}</td> <td>{indicator.generation}</td>
<td className="badge badge-outline">{indicator.phase}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{/* Performance summary */}
<div className="mt-4 grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="stat bg-base-200 rounded-lg">
<div className="stat-title">Top Performer</div>
<div className="stat-value text-lg">{normalizedIndicators[0]?.type}</div>
<div className="stat-desc">Fitness: {normalizedIndicators[0]?.bestFitness.toFixed(3)} ({normalizedIndicators[0]?.normFitness.toFixed(2)})</div>
</div>
<div className="stat bg-base-200 rounded-lg">
<div className="stat-title">Best PnL</div>
<div className="stat-value text-lg">
{normalizedIndicators.reduce((max, curr) => curr.bestPnl > max.bestPnl ? curr : max).type}
</div>
<div className="stat-desc">
${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)})
</div>
</div>
<div className="stat bg-base-200 rounded-lg">
<div className="stat-title">Best Win Rate</div>
<div className="stat-value text-lg">
{normalizedIndicators.reduce((max, curr) => curr.bestWinRate > max.bestWinRate ? curr : max).type}
</div>
<div className="stat-desc">
{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)})
</div>
</div>
<div className="stat bg-base-200 rounded-lg">
<div className="stat-title">Most Tested</div>
<div className="stat-value text-lg">
{normalizedIndicators.reduce((max, curr) => curr.count > max.count ? curr : max).type}
</div>
<div className="stat-desc">
{normalizedIndicators.reduce((max, curr) => curr.count > max.count ? curr : max).count} tests
</div>
</div>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -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<ScoreVsGenerationProps> = ({ 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}<br>` +
`Score: ${score.toFixed(3)}<br>` +
`PnL: $${pnl.toFixed(2)}<br>` +
`Win Rate: ${winRate.toFixed(1)}%<br>` +
`Max Drawdown: ${maxDrawdown.toFixed(1)}%<br>` +
`Ticker: ${backtest.config?.ticker || 'N/A'}<br>` +
`Timeframe: ${backtest.config?.timeframe || 'N/A'}`
}),
hovertemplate: '%{text}<extra></extra>',
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 (
<div className="w-full h-96">
<Plot
data={[...chartData, ...trendLineData]}
layout={layout}
config={config}
style={{ width: '100%', height: '100%' }}
useResizeHandler={true}
/>
</div>
)
}
export default ScoreVsGeneration

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import Plot from 'react-plotly.js'; import Plot from 'react-plotly.js';
import {Backtest} from '../../../generated/ManagingApi';
interface TPvsSLvsPnL3DPlotProps { interface TPvsSLvsPnL3DPlotProps {
results: any[]; backtests: Backtest[];
theme: { secondary: string }; theme: { secondary: string };
} }
@@ -11,18 +12,39 @@ const LOOPBACK_CONFIG = {
multipleIndicators: { min: 5, max: 15 }, multipleIndicators: { min: 5, max: 15 },
}; };
const TPvsSLvsPnL3DPlot: React.FC<TPvsSLvsPnL3DPlotProps> = ({ results, theme }) => { const TPvsSLvsPnL3DPlot: React.FC<TPvsSLvsPnL3DPlotProps> = ({ backtests, theme }) => {
const plotDataTPvsSL = results.length > 0 ? [ // 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, type: 'scatter3d' as const,
mode: 'markers' as const, mode: 'markers' as const,
x: results.map(r => r.individual.takeProfit), x: backtests.map(b => b.config.moneyManagement?.takeProfit || 0),
y: results.map(r => r.individual.stopLoss), y: backtests.map(b => b.config.moneyManagement?.stopLoss || 0),
z: results.map(r => r.backtest?.statistics?.totalPnL || 0), z: backtests.map(b => b.statistics?.totalPnL || 0),
marker: { marker: {
size: 5, size: 5,
color: results.map(r => { color: backtests.map(b => {
const pnl = r.backtest?.statistics?.totalPnL || 0; const pnl = b.statistics?.totalPnL || 0;
return pnl > 0 ? pnl : 0; return pnl > 0 ? pnl : 0;
}), }),
colorscale: 'RdYlGn' as const, colorscale: 'RdYlGn' as const,
@@ -32,36 +54,27 @@ const TPvsSLvsPnL3DPlot: React.FC<TPvsSLvsPnL3DPlotProps> = ({ results, theme })
titleside: 'right', titleside: 'right',
}, },
}, },
text: results.map(r => { text: backtests.map(b => {
const loopbackPeriod = r.individual.indicators.length === 1 const generation = getGeneration(b);
? LOOPBACK_CONFIG.singleIndicator const indicatorDetails = getIndicatorDetails(b);
: '5-15'; const riskRewardRatio = (b.config.moneyManagement?.takeProfit || 0) / (b.config.moneyManagement?.stopLoss || 1);
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;
const riskRewardColor = riskRewardRatio >= 2 ? '🟢' : riskRewardRatio >= 1.5 ? '🟡' : '🔴'; 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 ? '🟡' : '🔴'; const drawdownColor = maxDrawdownPc <= 10 ? '🟢' : maxDrawdownPc <= 25 ? '🟡' : '🔴';
return `Phase: ${r.phase || 'Unknown'}<br>` +
`Gen: ${r.generation}<br>` + return `Generation: ${generation}<br>` +
`Fitness: ${r.fitness.toFixed(2)}<br>` + `PnL: $${b.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` +
`PnL: $${r.backtest?.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` + `Win Rate: ${b.winRate?.toFixed(1) || 'N/A'}%<br>` +
`Win Rate: ${r.backtest?.winRate?.toFixed(1) || 'N/A'}%<br>` +
`Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%<br>` + `Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%<br>` +
`SL: ${r.individual.stopLoss.toFixed(1)}%<br>` + `SL: ${b.config.moneyManagement?.stopLoss?.toFixed(1) || 'N/A'}%<br>` +
`TP: ${r.individual.takeProfit.toFixed(1)}%<br>` + `TP: ${b.config.moneyManagement?.takeProfit?.toFixed(1) || 'N/A'}%<br>` +
`R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}<br>` + `R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}<br>` +
`Leverage: ${r.individual.leverage}x<br>` + `Leverage: ${b.config.moneyManagement?.leverage || 1}x<br>` +
`Cooldown: ${r.individual.cooldownPeriod} candles<br>` + `Cooldown: ${b.config.cooldownPeriod || 0} candles<br>` +
`Max Loss Streak: ${r.individual.maxLossStreak}<br>` + `Max Loss Streak: ${b.config.maxLossStreak || 0}<br>` +
`Max Time: ${r.individual.maxPositionTimeHours}h<br>` + `Max Time: ${b.config.maxPositionTimeHours || 0}h<br>` +
`Indicators: ${indicatorDetails}<br>` + `Indicators: ${indicatorDetails}<br>` +
`Loopback: ${loopbackPeriod}`; `Loopback: ${b.config.scenario?.loopbackPeriod || 'Unknown'}`;
}), }),
hovertemplate: '%{text}<extra></extra>', hovertemplate: '%{text}<extra></extra>',
}, },

View File

@@ -3289,7 +3289,7 @@ export interface Backtest {
user: User; user: User;
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number; score: number;
requestId?: string | null; requestId?: string;
metadata?: any | null; metadata?: any | null;
} }

View File

@@ -236,7 +236,7 @@ export interface Backtest {
user: User; user: User;
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number; score: number;
requestId?: string | null; requestId?: string;
metadata?: any | null; metadata?: any | null;
} }

View File

@@ -2035,7 +2035,7 @@ const BacktestGenetic: React.FC = () => {
<div className="card bg-base-100 shadow-xl"> <div className="card bg-base-100 shadow-xl">
<div className="card-body"> <div className="card-body">
<h3 className="card-title">Fitness vs Score vs Win Rate</h3> <h3 className="card-title">Fitness vs Score vs Win Rate</h3>
<Fitness3DPlot results={results} theme={theme} /> <Fitness3DPlot backtests={results.map(r => r.backtest).filter(Boolean) as Backtest[]} theme={theme} />
</div> </div>
</div> </div>
@@ -2043,14 +2043,14 @@ const BacktestGenetic: React.FC = () => {
<div className="card bg-base-100 shadow-xl"> <div className="card bg-base-100 shadow-xl">
<div className="card-body"> <div className="card-body">
<h3 className="card-title">Take Profit vs Stop Loss vs PnL</h3> <h3 className="card-title">Take Profit vs Stop Loss vs PnL</h3>
<TPvsSLvsPnL3DPlot results={results} theme={theme} /> <TPvsSLvsPnL3DPlot backtests={results.map(r => r.backtest).filter(Boolean) as Backtest[]} theme={theme} />
</div> </div>
</div> </div>
</div> </div>
{/* Strategy Comparison Radar Chart */} {/* Strategy Comparison Radar Chart */}
<div className="mt-6"> <div className="mt-6">
<IndicatorsComparison results={results} /> <IndicatorsComparison backtests={results.map(r => r.backtest).filter(Boolean) as Backtest[]} />
</div> </div>
{/* Results Table */} {/* Results Table */}

View File

@@ -17,6 +17,11 @@ import {Toast} from '../../components/mollecules'
import Table from '../../components/mollecules/Table/Table' import Table from '../../components/mollecules/Table/Table'
import BacktestTable from '../../components/organism/Backtest/backtestTable' import BacktestTable from '../../components/organism/Backtest/backtestTable'
import Modal from '../../components/mollecules/Modal/Modal' 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 // Available Indicator Types
const ALL_INDICATORS = [ const ALL_INDICATORS = [
@@ -55,6 +60,7 @@ interface GeneticBundleFormData {
const BacktestGeneticBundle: React.FC = () => { const BacktestGeneticBundle: React.FC = () => {
const {apiUrl} = useApiUrlStore() const {apiUrl} = useApiUrlStore()
const backtestClient = new BacktestClient({}, apiUrl) const backtestClient = new BacktestClient({}, apiUrl)
const theme = useTheme().themeProperty()
// State // State
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@@ -552,11 +558,46 @@ const BacktestGeneticBundle: React.FC = () => {
<span className="loading loading-spinner loading-md"></span> <span className="loading loading-spinner loading-md"></span>
</div> </div>
) : backtests.length > 0 ? ( ) : backtests.length > 0 ? (
<>
{/* Score vs Generation Chart */}
<div className="mb-6">
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h3 className="card-title">Score vs Generation</h3>
<ScoreVsGeneration backtests={backtests} theme={theme} />
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Fitness vs Score vs Win Rate */}
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h3 className="card-title">Fitness vs Score vs Win Rate</h3>
<Fitness3DPlot backtests={backtests} theme={theme} />
</div>
</div>
{/* TP% vs SL% vs PnL */}
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h3 className="card-title">Take Profit vs Stop Loss vs PnL</h3>
<TPvsSLvsPnL3DPlot backtests={backtests} theme={theme} />
</div>
</div>
</div>
{/* Strategy Comparison Radar Chart */}
<div className="mb-6">
<IndicatorsComparison backtests={backtests} />
</div>
<BacktestTable <BacktestTable
list={backtests} list={backtests}
isFetching={false} isFetching={false}
displaySummary={false} displaySummary={false}
/> />
</>
) : ( ) : (
<div className="text-center text-gray-500 py-8"> <div className="text-center text-gray-500 py-8">
No backtest results found for this request. No backtest results found for this request.