import React, { useEffect, useMemo, useState } from 'react'; import { runBacktestApi } from '../api'; import type { BacktestIntraCandlePolicy, BacktestRequestPayload, BacktestResult, BacktestTimeframe, BacktestTriggerTimeframe } from '../types'; import { parseSymbolsInput, toDateInputValue } from '../utils'; import { BacktestResultsDashboard } from './BacktestResultsDashboard'; import { Button, Checkbox, Input, Select } from '../../components/ui/Primitives'; type SourceType = 'csv' | 'json' | 'replay' | 'kraken'; export interface BacktestProfile { id: string; name: string; symbols: string; strategy_config: any; allocated_capital?: number; } interface CompareRow { profileId: string; profileName: string; status: 'success' | 'error'; durationMs: number; result?: BacktestResult; error?: string; } interface BacktestComparePanelProps { profiles: BacktestProfile[]; } const money = (value: number): string => Number(value || 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }); const pct = (value: number): string => `${Number(value || 0).toFixed(2)}%`; export const BacktestComparePanel: React.FC = ({ profiles }) => { const now = Date.now(); const defaultFrom = toDateInputValue(now - (30 * 24 * 60 * 60 * 1000)); const defaultTo = toDateInputValue(now); const [selectedProfileIds, setSelectedProfileIds] = useState([]); const [timeframe, setTimeframe] = useState('15m'); const [fromDate, setFromDate] = useState(defaultFrom); const [toDate, setToDate] = useState(defaultTo); const [sourceType, setSourceType] = useState('kraken'); const [sourcePayload, setSourcePayload] = useState({ exchange: 'kraken' }); const [sourceName, setSourceName] = useState('Kraken historical API'); const [slippageBps, setSlippageBps] = useState(5); const [feeBps, setFeeBps] = useState(10); const [partialFillPct, setPartialFillPct] = useState(1); const [fillOnNextBar, setFillOnNextBar] = useState(true); const [intraCandlePolicy, setIntraCandlePolicy] = useState('ohlc_path'); const [triggerTimeframe, setTriggerTimeframe] = useState('1m'); const [forceCloseAtWindowEnd, setForceCloseAtWindowEnd] = useState(false); const [krakenLookbackCandles, setKrakenLookbackCandles] = useState(2000); const [useNormalizedCapital, setUseNormalizedCapital] = useState(true); const [normalizedCapitalUsd, setNormalizedCapitalUsd] = useState(1000); const [running, setRunning] = useState(false); const [runningIndex, setRunningIndex] = useState(0); const [rows, setRows] = useState([]); const [selectedResultProfileId, setSelectedResultProfileId] = useState(''); const [error, setError] = useState(null); useEffect(() => { const existing = new Set(profiles.map((profile) => profile.id)); setSelectedProfileIds((prev) => { const filtered = prev.filter((id) => existing.has(id)); if (filtered.length) return filtered; return profiles.slice(0, Math.min(3, profiles.length)).map((profile) => profile.id); }); }, [profiles]); const profileMap = useMemo(() => { const map = new Map(); for (const profile of profiles) { map.set(profile.id, profile); } return map; }, [profiles]); const selectedProfiles = useMemo(() => ( selectedProfileIds .map((id) => profileMap.get(id)) .filter((value): value is BacktestProfile => Boolean(value)) .sort((a, b) => a.name.localeCompare(b.name)) ), [selectedProfileIds, profileMap]); const successfulRows = useMemo(() => rows.filter((row) => row.status === 'success' && row.result), [rows]); const rankedRows = useMemo(() => { const success = successfulRows .slice() .sort((a, b) => (b.result?.summary.netPnlUsd || 0) - (a.result?.summary.netPnlUsd || 0)); const errored = rows .filter((row) => row.status === 'error') .slice() .sort((a, b) => a.profileName.localeCompare(b.profileName)); return [...success, ...errored]; }, [successfulRows, rows]); const selectedResultRow = useMemo( () => rows.find((row) => row.profileId === selectedResultProfileId && row.result), [rows, selectedResultProfileId] ); const readFile = async (file: File): Promise => { const text = await file.text(); setSourceName(file.name); if (sourceType === 'csv') { setSourcePayload(text); return; } try { const parsed = JSON.parse(text); setSourcePayload(parsed); } catch { throw new Error('Failed to parse JSON file.'); } }; const buildPayload = (profile: BacktestProfile): BacktestRequestPayload => { const parsedSymbols = parseSymbolsInput(profile.symbols || ''); const initialCapital = useNormalizedCapital ? Number(normalizedCapitalUsd || 0) : Number(profile.allocated_capital || 1000); return { mode: 'backtest', profileId: profile.id, strategyConfig: profile.strategy_config, symbols: parsedSymbols, timeframe, dateRange: { from: new Date(`${fromDate}T00:00:00.000Z`).toISOString(), to: new Date(`${toDate}T23:59:59.999Z`).toISOString() }, dataSource: { type: sourceType, payload: sourceType === 'kraken' ? { exchange: 'kraken', lookbackCandles: Number(krakenLookbackCandles || 2000) } : (sourceType === 'csv' ? String(sourcePayload || '') : sourcePayload) }, execution: { initialCapitalUsd: initialCapital, orderType: 'market', slippageBps: Number(slippageBps), feeBps: Number(feeBps), partialFillPct: Number(partialFillPct), fillOnNextBar, intraCandlePolicy, triggerTimeframe, enforceWarmup: true, allowNegativeCash: false, forceCloseAtWindowEnd } }; }; const runCompare = async () => { setError(null); if (selectedProfiles.length < 2) { setError('Select at least two strategy profiles to compare.'); return; } if (!fromDate || !toDate) { setError('Please provide both from/to dates.'); return; } if (new Date(`${toDate}T23:59:59.999Z`).getTime() < new Date(`${fromDate}T00:00:00.000Z`).getTime()) { setError('To date must be greater than or equal to From date.'); return; } if (sourceType !== 'kraken' && !sourcePayload) { setError('Please upload historical data before running comparison.'); return; } if (useNormalizedCapital && Number(normalizedCapitalUsd || 0) <= 0) { setError('Normalized capital must be greater than zero.'); return; } setRunning(true); setRunningIndex(0); setRows([]); setSelectedResultProfileId(''); const nextRows: CompareRow[] = []; for (let index = 0; index < selectedProfiles.length; index += 1) { const profile = selectedProfiles[index]; setRunningIndex(index + 1); const startedAt = Date.now(); try { const payload = buildPayload(profile); if (!payload.symbols.length) { throw new Error('Profile has no symbols configured.'); } const result = await runBacktestApi(payload); nextRows.push({ profileId: profile.id, profileName: profile.name, status: 'success', durationMs: Date.now() - startedAt, result }); } catch (runError: any) { nextRows.push({ profileId: profile.id, profileName: profile.name, status: 'error', durationMs: Date.now() - startedAt, error: runError?.message || 'Backtest compare run failed' }); } setRows([...nextRows]); } const firstSuccess = nextRows.find((row) => row.status === 'success'); if (firstSuccess) { setSelectedResultProfileId(firstSuccess.profileId); } setRunning(false); setRunningIndex(0); }; const toggleProfile = (profileId: string, checked: boolean) => { setSelectedProfileIds((prev) => { if (checked) { if (prev.includes(profileId)) return prev; return [...prev, profileId]; } return prev.filter((id) => id !== profileId); }); }; return (

Compare Strategies

Run one deterministic replay window across multiple profiles and rank outcomes.

Selected: {selectedProfiles.length}
Profiles
{profiles.map((profile) => ( ))}
{sourceType === 'kraken' ? ( ) : ( )}
Source: {sourceName || 'none'}
{error && (
{error}
)}
{rows.length > 0 && (

Comparison Results

Success: {successfulRows.length} / {rows.length}
{rankedRows.map((row, index) => { const summary = row.result?.summary; const active = row.profileId === selectedResultProfileId; return ( ); })}
Rank Strategy Net PnL Win Rate Max DD Sharpe Trades Duration Details
{row.status === 'success' ? `#${index + 1}` : '-'} {row.profileName} = 0 ? 'text-emerald-400' : 'text-rose-400'}`}> {summary ? money(summary.netPnlUsd) : '-'} {summary ? pct(summary.winRate) : '-'} {summary ? pct(summary.maxDrawdownPct) : '-'} {summary ? summary.sharpe.toFixed(3) : '-'} {summary ? summary.totalTrades : '-'} {(row.durationMs / 1000).toFixed(1)}s {row.status === 'success' && row.result ? ( ) : ( {row.error || 'Run failed'} )}
)} {selectedResultRow?.result && (

Detailed Result: {selectedResultRow.profileName}

)}
); };