Add chart for genetic bundle
This commit is contained in:
@@ -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<Fitness3DPlotProps> = ({ results, theme }) => {
|
||||
const Fitness3DPlot: React.FC<Fitness3DPlotProps> = ({ backtests, theme }) => {
|
||||
const LOOPBACK_CONFIG = {
|
||||
singleIndicator: 1,
|
||||
multipleIndicators: { min: 5, max: 15 },
|
||||
};
|
||||
|
||||
const plotData = results.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),
|
||||
marker: {
|
||||
size: 5,
|
||||
color: results.map(r => r.generation),
|
||||
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();
|
||||
// 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 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 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'}<br>` +
|
||||
`Gen: ${r.generation}<br>` +
|
||||
`Fitness: ${r.fitness.toFixed(2)}<br>` +
|
||||
`PnL: $${r.backtest?.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` +
|
||||
`Win Rate: ${r.backtest?.winRate?.toFixed(1) || 'N/A'}%<br>` +
|
||||
|
||||
return `Generation: ${generation}<br>` +
|
||||
`Fitness: ${fitness.toFixed(2)}<br>` +
|
||||
`PnL: $${b.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` +
|
||||
`Win Rate: ${b.winRate?.toFixed(1) || 'N/A'}%<br>` +
|
||||
`Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%<br>` +
|
||||
`SL: ${r.individual.stopLoss.toFixed(1)}%<br>` +
|
||||
`TP: ${r.individual.takeProfit.toFixed(1)}%<br>` +
|
||||
`SL: ${b.config.moneyManagement?.stopLoss?.toFixed(1) || 'N/A'}%<br>` +
|
||||
`TP: ${b.config.moneyManagement?.takeProfit?.toFixed(1) || 'N/A'}%<br>` +
|
||||
`R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}<br>` +
|
||||
`Leverage: ${r.individual.leverage}x<br>` +
|
||||
`Cooldown: ${r.individual.cooldownPeriod} candles<br>` +
|
||||
`Max Loss Streak: ${r.individual.maxLossStreak}<br>` +
|
||||
`Max Time: ${r.individual.maxPositionTimeHours}h<br>` +
|
||||
`Leverage: ${b.config.moneyManagement?.leverage || 1}x<br>` +
|
||||
`Cooldown: ${b.config.cooldownPeriod || 0} candles<br>` +
|
||||
`Max Loss Streak: ${b.config.maxLossStreak || 0}<br>` +
|
||||
`Max Time: ${b.config.maxPositionTimeHours || 0}h<br>` +
|
||||
`Indicators: ${indicatorDetails}<br>` +
|
||||
`Loopback: ${loopbackPeriod}`;
|
||||
`Loopback: ${b.config.scenario?.loopbackPeriod || 'Unknown'}`;
|
||||
}),
|
||||
hovertemplate: '%{text}<extra></extra>',
|
||||
},
|
||||
|
||||
@@ -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<IndicatorsComparisonProps> = ({ results }) => {
|
||||
const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ 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 (
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
@@ -75,66 +83,67 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ 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<IndicatorsComparisonProps> = ({ results })
|
||||
'<b>Normalized:</b> %{y:.2f}<br>' +
|
||||
'<b>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[3]}<br>' +
|
||||
'<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,
|
||||
@@ -212,9 +220,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
'<b>Normalized:</b> %{y:.2f}<br>' +
|
||||
'<b>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[3]}<br>' +
|
||||
'<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,
|
||||
@@ -231,9 +238,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
'<b>Normalized:</b> %{y:.2f}<br>' +
|
||||
'<b>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[3]}<br>' +
|
||||
'<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,
|
||||
@@ -250,9 +256,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
'<b>Normalized:</b> %{y:.2f}<br>' +
|
||||
'<b>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[3]}<br>' +
|
||||
'<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,
|
||||
@@ -269,9 +274,8 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
'<b>Normalized:</b> %{y:.2f}<br>' +
|
||||
'<b>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[3]}<br>' +
|
||||
'<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,
|
||||
@@ -279,36 +283,35 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
y: normalizedIndicators.map(d => d.normPnlVsHodl),
|
||||
name: 'PnL vs HODL',
|
||||
marker: {
|
||||
color: '#A259F7',
|
||||
color: '#FF8C42',
|
||||
opacity: 0.8
|
||||
},
|
||||
hovertemplate:
|
||||
'<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>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[3]}<br>' +
|
||||
'<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,
|
||||
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:
|
||||
'<b>%{x}</b><br>' +
|
||||
'<b>Num Positions (raw):</b> %{y}<br>' +
|
||||
'<b>Count:</b> %{customdata[0]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[1]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[2]}<br>' +
|
||||
'<b>Trade Count (raw):</b> %{customdata[0]}<br>' +
|
||||
'<b>Normalized:</b> %{y:.2f}<br>' +
|
||||
'<b>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<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,
|
||||
@@ -316,19 +319,18 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
y: normalizedIndicators.map(d => d.normAvgDrawdown),
|
||||
name: 'Avg Drawdown',
|
||||
marker: {
|
||||
color: '#B22222',
|
||||
color: '#E74C3C',
|
||||
opacity: 0.8
|
||||
},
|
||||
hovertemplate:
|
||||
'<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>Count:</b> %{customdata[1]}<br>' +
|
||||
'<b>Best Gen:</b> %{customdata[2]}<br>' +
|
||||
'<b>Phase:</b> %{customdata[3]}<br>' +
|
||||
'<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 (
|
||||
@@ -390,7 +392,7 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
|
||||
{/* Detailed indicator table */}
|
||||
<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">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
@@ -401,76 +403,31 @@ const IndicatorsComparison: React.FC<IndicatorsComparisonProps> = ({ results })
|
||||
<th>Best Win Rate</th>
|
||||
<th>Best Score</th>
|
||||
<th>Best Fitness</th>
|
||||
<th>Sharpe Ratio</th>
|
||||
<th>PnL vs HODL</th>
|
||||
<th>Num Positions</th>
|
||||
<th>Avg Drawdown</th>
|
||||
<th>Best Sharpe</th>
|
||||
<th>Best Gen</th>
|
||||
<th>Phase</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{normalizedIndicators.map((indicator, index) => (
|
||||
{sortedIndicators.map((indicator, index) => (
|
||||
<tr key={indicator.type}>
|
||||
<td className="font-medium">{indicator.type}</td>
|
||||
<td>{indicator.count}</td>
|
||||
<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>{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 className="badge badge-outline">{indicator.phase}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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<TPvsSLvsPnL3DPlotProps> = ({ results, theme }) => {
|
||||
const plotDataTPvsSL = results.length > 0 ? [
|
||||
const TPvsSLvsPnL3DPlot: React.FC<TPvsSLvsPnL3DPlotProps> = ({ 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<TPvsSLvsPnL3DPlotProps> = ({ 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'}<br>` +
|
||||
`Gen: ${r.generation}<br>` +
|
||||
`Fitness: ${r.fitness.toFixed(2)}<br>` +
|
||||
`PnL: $${r.backtest?.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` +
|
||||
`Win Rate: ${r.backtest?.winRate?.toFixed(1) || 'N/A'}%<br>` +
|
||||
|
||||
return `Generation: ${generation}<br>` +
|
||||
`PnL: $${b.statistics?.totalPnL?.toFixed(2) || 'N/A'}<br>` +
|
||||
`Win Rate: ${b.winRate?.toFixed(1) || 'N/A'}%<br>` +
|
||||
`Max Drawdown: ${drawdownColor} ${maxDrawdownPc.toFixed(1)}%<br>` +
|
||||
`SL: ${r.individual.stopLoss.toFixed(1)}%<br>` +
|
||||
`TP: ${r.individual.takeProfit.toFixed(1)}%<br>` +
|
||||
`SL: ${b.config.moneyManagement?.stopLoss?.toFixed(1) || 'N/A'}%<br>` +
|
||||
`TP: ${b.config.moneyManagement?.takeProfit?.toFixed(1) || 'N/A'}%<br>` +
|
||||
`R/R: ${riskRewardColor} ${riskRewardRatio.toFixed(2)}<br>` +
|
||||
`Leverage: ${r.individual.leverage}x<br>` +
|
||||
`Cooldown: ${r.individual.cooldownPeriod} candles<br>` +
|
||||
`Max Loss Streak: ${r.individual.maxLossStreak}<br>` +
|
||||
`Max Time: ${r.individual.maxPositionTimeHours}h<br>` +
|
||||
`Leverage: ${b.config.moneyManagement?.leverage || 1}x<br>` +
|
||||
`Cooldown: ${b.config.cooldownPeriod || 0} candles<br>` +
|
||||
`Max Loss Streak: ${b.config.maxLossStreak || 0}<br>` +
|
||||
`Max Time: ${b.config.maxPositionTimeHours || 0}h<br>` +
|
||||
`Indicators: ${indicatorDetails}<br>` +
|
||||
`Loopback: ${loopbackPeriod}`;
|
||||
`Loopback: ${b.config.scenario?.loopbackPeriod || 'Unknown'}`;
|
||||
}),
|
||||
hovertemplate: '%{text}<extra></extra>',
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2035,7 +2035,7 @@ const BacktestGenetic: React.FC = () => {
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
<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>
|
||||
|
||||
@@ -2043,14 +2043,14 @@ const BacktestGenetic: React.FC = () => {
|
||||
<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 results={results} theme={theme} />
|
||||
<TPvsSLvsPnL3DPlot backtests={results.map(r => r.backtest).filter(Boolean) as Backtest[]} theme={theme} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Strategy Comparison Radar Chart */}
|
||||
<div className="mt-6">
|
||||
<IndicatorsComparison results={results} />
|
||||
<IndicatorsComparison backtests={results.map(r => r.backtest).filter(Boolean) as Backtest[]} />
|
||||
</div>
|
||||
|
||||
{/* Results Table */}
|
||||
|
||||
@@ -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 = () => {
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
</div>
|
||||
) : 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
|
||||
list={backtests}
|
||||
isFetching={false}
|
||||
displaySummary={false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
No backtest results found for this request.
|
||||
|
||||
Reference in New Issue
Block a user