import { useEffect, useMemo, useState, useRef } from 'react' import { motion } from 'framer-motion' import { createChart, ColorType, CrosshairMode, CandlestickSeries, createSeriesMarkers, type IChartApi, type ISeriesApi, type CandlestickData, type UTCTimestamp, type SeriesMarker, } from 'lightweight-charts' import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceDot, } from 'recharts' import { Clock, AlertTriangle, RefreshCw, CandlestickChart as CandlestickIcon, } from 'lucide-react' import { api } from '../../lib/api' import { t, type Language } from '../../i18n/translations' import type { BacktestEquityPoint, BacktestTradeEvent, BacktestKlinesResponse, } from '../../types' // ============ Equity Chart (Recharts) ============ interface EquityChartProps { equity: BacktestEquityPoint[] trades: BacktestTradeEvent[] } export function EquityChart({ equity, trades }: EquityChartProps) { const chartData = useMemo(() => { return equity.map((point) => ({ time: new Date(point.ts).toLocaleString(), ts: point.ts, equity: point.equity, pnl_pct: point.pnl_pct, })) }, [equity]) const tradeMarkers = useMemo(() => { if (!trades.length || !equity.length) return [] return trades .filter((t) => t.action.includes('open') || t.action.includes('close')) .map((trade) => { const closest = equity.reduce((prev, curr) => Math.abs(curr.ts - trade.ts) < Math.abs(prev.ts - trade.ts) ? curr : prev ) return { ts: closest.ts, equity: closest.equity, action: trade.action, symbol: trade.symbol, isOpen: trade.action.includes('open'), } }) .slice(-30) }, [trades, equity]) return (
[`$${value.toFixed(2)}`, 'Equity']} /> {tradeMarkers.map((marker, idx) => ( d.ts === marker.ts)} y={marker.equity} r={4} fill={marker.isOpen ? '#0ECB81' : '#F6465D'} stroke={marker.isOpen ? '#0ECB81' : '#F6465D'} /> ))}
) } // ============ Candlestick Chart with Trade Markers ============ interface CandlestickChartProps { runId: string trades: BacktestTradeEvent[] language: Language } export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) { const chartContainerRef = useRef(null) const chartRef = useRef(null) const candleSeriesRef = useRef | null>(null) const symbols = useMemo(() => { const symbolSet = new Set(trades.map((t) => t.symbol)) return Array.from(symbolSet).sort() }, [trades]) const [selectedSymbol, setSelectedSymbol] = useState(symbols[0] || '') const [selectedTimeframe, setSelectedTimeframe] = useState('15m') const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const CHART_TIMEFRAMES = ['1m', '3m', '5m', '15m', '30m', '1h', '4h', '1d'] useEffect(() => { if (symbols.length > 0 && !symbols.includes(selectedSymbol)) { setSelectedSymbol(symbols[0]) } }, [symbols, selectedSymbol]) const symbolTrades = useMemo(() => { return trades.filter((t) => t.symbol === selectedSymbol) }, [trades, selectedSymbol]) useEffect(() => { if (!chartContainerRef.current || !selectedSymbol || !runId) return const container = chartContainerRef.current const chart = createChart(container, { layout: { background: { type: ColorType.Solid, color: '#0B0E11' }, textColor: '#848E9C', }, grid: { vertLines: { color: 'rgba(43, 49, 57, 0.5)' }, horzLines: { color: 'rgba(43, 49, 57, 0.5)' }, }, crosshair: { mode: CrosshairMode.Normal, }, rightPriceScale: { borderColor: '#2B3139', }, timeScale: { borderColor: '#2B3139', timeVisible: true, secondsVisible: false, }, width: container.clientWidth, height: 400, }) chartRef.current = chart const candleSeries = chart.addSeries(CandlestickSeries, { upColor: '#0ECB81', downColor: '#F6465D', borderUpColor: '#0ECB81', borderDownColor: '#F6465D', wickUpColor: '#0ECB81', wickDownColor: '#F6465D', }) candleSeriesRef.current = candleSeries setIsLoading(true) setError(null) api .getBacktestKlines(runId, selectedSymbol, selectedTimeframe) .then((data: BacktestKlinesResponse) => { const klineData: CandlestickData[] = data.klines.map((k) => ({ time: k.time as UTCTimestamp, open: k.open, high: k.high, low: k.low, close: k.close, })) candleSeries.setData(klineData) const markers: SeriesMarker[] = symbolTrades .map((trade) => { const tradeTime = Math.floor(trade.ts / 1000) const closestKline = data.klines.reduce((prev, curr) => Math.abs(curr.time - tradeTime) < Math.abs(prev.time - tradeTime) ? curr : prev ) const isOpen = trade.action.includes('open') const isLong = trade.side === 'long' || trade.action.includes('long') const pnl = trade.realized_pnl let text = '' let color = '#0ECB81' if (isOpen) { if (isLong) { text = `▲ Long @${trade.price.toFixed(2)}` color = '#0ECB81' } else { text = `▼ Short @${trade.price.toFixed(2)}` color = '#F6465D' } } else { const pnlStr = pnl >= 0 ? `+$${pnl.toFixed(2)}` : `-$${Math.abs(pnl).toFixed(2)}` text = `✕ ${pnlStr}` color = pnl >= 0 ? '#0ECB81' : '#F6465D' } return { time: closestKline.time as UTCTimestamp, position: isOpen ? (isLong ? 'belowBar' as const : 'aboveBar' as const) : (isLong ? 'aboveBar' as const : 'belowBar' as const), color, shape: 'circle' as const, size: 2, text, } }) .sort((a, b) => (a.time as number) - (b.time as number)) createSeriesMarkers(candleSeries, markers) chart.timeScale().fitContent() setIsLoading(false) }) .catch((err) => { setError(err.message || 'Failed to load klines') setIsLoading(false) }) const handleResize = () => { if (chartContainerRef.current) { chart.applyOptions({ width: chartContainerRef.current.clientWidth }) } } window.addEventListener('resize', handleResize) return () => { window.removeEventListener('resize', handleResize) chart.remove() chartRef.current = null candleSeriesRef.current = null } }, [runId, selectedSymbol, selectedTimeframe, symbolTrades]) if (symbols.length === 0) { return (
{t('backtestChart.noTrades', language)}
) } return (
{t('backtestChart.symbol', language)}
{t('backtestChart.interval', language)}
{CHART_TIMEFRAMES.map((tf) => ( ))}
({symbolTrades.length} {t('backtestChart.trades', language)})
{isLoading && (
{t('backtestChart.loadingKline', language)}
)} {error && (
{error}
)}
{t('backtestChart.openProfit', language)}
{t('backtestChart.lossClose', language)}
| ▲ Long · ▼ Short · ✕ {t('backtestChart.close', language)}
) } // ============ Chart Tab Content ============ interface BacktestChartTabProps { equity: BacktestEquityPoint[] | undefined trades: BacktestTradeEvent[] | undefined selectedRunId: string language: Language tr: (key: string) => string } export function BacktestChartTab({ equity, trades, selectedRunId, language, tr, }: BacktestChartTabProps) { return (

{t('backtestChart.equityCurve', language)}

{equity && equity.length > 0 ? ( ) : (
{tr('charts.equityEmpty')}
)}
{selectedRunId && trades && trades.length > 0 && (

{t('backtestChart.candlestickTradeMarkers', language)}

)}
) }