import { useCallback, useEffect, useRef, useState } from 'react'; import { motion } from 'motion/react'; import { CandlestickSeries, LineSeries, createChart, createSeriesMarkers, } from 'lightweight-charts'; const CHART_THEME = { layout: { background: { color: '#0a0a0a' }, textColor: '#737373', fontFamily: 'Inter, system-ui, sans-serif', fontSize: 11 }, grid: { vertLines: { color: '#1a1a1a' }, horzLines: { color: '#1a1a1a' } }, crosshair: { vertLine: { color: 'rgba(250, 250, 250, 0.15)', labelBackgroundColor: '#262626' }, horzLine: { color: 'rgba(250, 250, 250, 0.15)', labelBackgroundColor: '#262626' }, }, rightPriceScale: { borderColor: '#262626', textColor: '#737373' }, timeScale: { borderColor: '#262626', timeVisible: true, secondsVisible: false }, }; const TIMEFRAMES = [ { label: '1m', value: 1 }, { label: '3m', value: 3 }, { label: '5m', value: 5 }, { label: '15m', value: 15 }, { label: '30m', value: 30 }, { label: '1H', value: 60 }, ]; const RR_OPTIONS = [1, 1.5, 2, 2.5, 3]; const SESSIONS = ['london', 'new_york', 'asian', 'london_close', 'london_ny_overlap', 'all']; const DAYS = [ { label: 'Mon', value: 0 }, { label: 'Tue', value: 1 }, { label: 'Wed', value: 2 }, { label: 'Thu', value: 3 }, { label: 'Fri', value: 4 }, ]; const STARTING_BALANCE = 10000; const PRESETS_KEY = 'nq_backtest_presets'; const RESULT_HISTORY_KEY = 'nq_backtest_recent_results'; const RESULT_HISTORY_LIMIT = 10; const DEFAULT_PRESET_NAME = 'Manual'; function formatMoney(v) { return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function calculateProfitFactor(trades) { const winners = trades.filter((trade) => trade.pnl > 0).reduce((sum, trade) => sum + trade.pnl, 0); const losers = Math.abs(trades.filter((trade) => trade.pnl < 0).reduce((sum, trade) => sum + trade.pnl, 0)); if (losers <= 0) return winners > 0 ? 999 : 0; return Number((winners / losers).toFixed(2)); } function calculateMaxDrawdown(trades, startingBalance = STARTING_BALANCE) { if (!trades.length) return 0; let equity = startingBalance; let peak = startingBalance; let maxDrawdown = 0; trades .slice() .sort((a, b) => new Date(a.exit_time) - new Date(b.exit_time)) .forEach((trade) => { equity += trade.pnl; if (equity > peak) peak = equity; const drawdown = ((peak - equity) / peak) * 100; if (drawdown > maxDrawdown) maxDrawdown = drawdown; }); return Number(maxDrawdown.toFixed(2)); } function loadPresets() { try { return JSON.parse(localStorage.getItem(PRESETS_KEY) || '{}'); } catch { return {}; } } function savePresets(presets) { localStorage.setItem(PRESETS_KEY, JSON.stringify(presets)); } function loadRecentResults() { try { const parsed = JSON.parse(localStorage.getItem(RESULT_HISTORY_KEY) || '[]'); return Array.isArray(parsed) ? parsed.slice(0, RESULT_HISTORY_LIMIT) : []; } catch { return []; } } function saveRecentResults(results) { localStorage.setItem(RESULT_HISTORY_KEY, JSON.stringify(results.slice(0, RESULT_HISTORY_LIMIT))); } function NumberInput({ label, value, onChange, min, max, step = 1 }) { return (
onChange(Number(e.target.value))} className="w-full border border-[#262626] bg-black text-[#fafafa] font-mono text-[13px] px-3 py-2 outline-none focus:border-[#404040] transition-colors" />
); } function ToggleInput({ label, value, onChange, color = '#10b981' }) { return ( ); } function SectionHeader({ title, subtitle }) { return (

{title}

{subtitle &&

{subtitle}

}
); } export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange, onBacktestComplete }) { const chartContainerRef = useRef(null); const equityChartRef = useRef(null); const chartRef = useRef(null); const equityChartObjRef = useRef(null); const candleSeriesRef = useRef(null); const equitySeriesRef = useRef(null); const markersRef = useRef(null); const progressIntervalRef = useRef(null); const progressResetTimeoutRef = useRef(null); const [chartsReady, setChartsReady] = useState(false); const [loading, setLoading] = useState(false); const [results, setResults] = useState(null); const [autoRun, setAutoRun] = useState(false); const [progressPct, setProgressPct] = useState(0); const [mcLoading, setMcLoading] = useState(false); const [mcErrorMessage, setMcErrorMessage] = useState(''); const [mcResult, setMcResult] = useState(null); const [mcRuns, setMcRuns] = useState(500); const [mcVariationPct, setMcVariationPct] = useState(15); const [mcPriceNoisePct, setMcPriceNoisePct] = useState(0); const [mcSlippage, setMcSlippage] = useState(0); const [mcSpread, setMcSpread] = useState(0); const [mcRuinDrawdownPct, setMcRuinDrawdownPct] = useState(20); const [mcShuffleTrades, setMcShuffleTrades] = useState(true); const [timeframe, setTimeframe] = useState(1); const [riskReward, setRiskReward] = useState(2.5); const [lookback, setLookback] = useState(7); const [atrMult, setAtrMult] = useState(2.5); const [session, setSession] = useState('london'); const [useFvg, setUseFvg] = useState(true); const [useOb, setUseOb] = useState(true); const [useLiquiditySweep, setUseLiquiditySweep] = useState(true); const [obMaxAge, setObMaxAge] = useState(50); const [proximityPct, setProximityPct] = useState(0.5); const [sweepLookback, setSweepLookback] = useState(5); const [minGapSize, setMinGapSize] = useState(0.0); const [impulseMultiplier, setImpulseMultiplier] = useState(0.0); const [requireUnmitigatedFvg, setRequireUnmitigatedFvg] = useState(true); const [requireBosConfluence, setRequireBosConfluence] = useState(false); const [minObSize, setMinObSize] = useState(0.0); const [requireFvgObConfluence, setRequireFvgObConfluence] = useState(false); const [asianSweepOnly, setAsianSweepOnly] = useState(false); const [useBreakEven, setUseBreakEven] = useState(false); const [beTriggerRr, setBeTriggerRr] = useState(1.0); const [usePartialTp, setUsePartialTp] = useState(false); const [partialTpRr, setPartialTpRr] = useState(1.0); const [partialTpPercent, setPartialTpPercent] = useState(50); const [dayFilter, setDayFilter] = useState([0, 1, 2, 3, 4]); const [maxDailyLoss, setMaxDailyLoss] = useState(0.0); const [maxConsecutiveLosses, setMaxConsecutiveLosses] = useState(0); // Presets const [presets, setPresets] = useState(loadPresets); const [presetName, setPresetName] = useState(''); const [activePresetName, setActivePresetName] = useState(DEFAULT_PRESET_NAME); const [showPresets, setShowPresets] = useState(false); const [recentResults, setRecentResults] = useState(loadRecentResults); const getSettings = () => ({ timeframe, riskReward, lookback, atrMult, session, useFvg, useOb, useLiquiditySweep, obMaxAge, proximityPct, sweepLookback, minGapSize, impulseMultiplier, requireUnmitigatedFvg, requireBosConfluence, minObSize, requireFvgObConfluence, asianSweepOnly, dayFilter, useBreakEven, beTriggerRr, usePartialTp, partialTpRr, partialTpPercent, maxDailyLoss, maxConsecutiveLosses, }); const applySettings = (s) => { if (s.timeframe !== undefined) setTimeframe(s.timeframe); if (s.riskReward !== undefined) setRiskReward(s.riskReward); if (s.lookback !== undefined) setLookback(s.lookback); if (s.atrMult !== undefined) setAtrMult(s.atrMult); if (s.session !== undefined) setSession(s.session); if (s.useFvg !== undefined) setUseFvg(s.useFvg); if (s.useOb !== undefined) setUseOb(s.useOb); if (s.useLiquiditySweep !== undefined) setUseLiquiditySweep(s.useLiquiditySweep); if (s.obMaxAge !== undefined) setObMaxAge(s.obMaxAge); if (s.proximityPct !== undefined) setProximityPct(s.proximityPct); if (s.sweepLookback !== undefined) setSweepLookback(s.sweepLookback); if (s.minGapSize !== undefined) setMinGapSize(s.minGapSize); if (s.impulseMultiplier !== undefined) setImpulseMultiplier(s.impulseMultiplier); if (s.requireUnmitigatedFvg !== undefined) setRequireUnmitigatedFvg(s.requireUnmitigatedFvg); if (s.requireBosConfluence !== undefined) setRequireBosConfluence(s.requireBosConfluence); if (s.minObSize !== undefined) setMinObSize(s.minObSize); if (s.requireFvgObConfluence !== undefined) setRequireFvgObConfluence(s.requireFvgObConfluence); if (s.asianSweepOnly !== undefined) setAsianSweepOnly(s.asianSweepOnly); if (s.useBreakEven !== undefined) setUseBreakEven(s.useBreakEven); if (s.beTriggerRr !== undefined) setBeTriggerRr(s.beTriggerRr); if (s.usePartialTp !== undefined) setUsePartialTp(s.usePartialTp); if (s.partialTpRr !== undefined) setPartialTpRr(s.partialTpRr); if (s.partialTpPercent !== undefined) setPartialTpPercent(s.partialTpPercent); if (s.dayFilter !== undefined) setDayFilter(s.dayFilter); if (s.maxDailyLoss !== undefined) setMaxDailyLoss(s.maxDailyLoss); if (s.maxConsecutiveLosses !== undefined) setMaxConsecutiveLosses(s.maxConsecutiveLosses); }; const handleSavePreset = () => { const name = presetName.trim(); if (!name) return; const updated = { ...presets, [name]: getSettings() }; setPresets(updated); savePresets(updated); setActivePresetName(name); setPresetName(''); }; const handleLoadPreset = (name) => { const preset = presets[name]; if (preset) { applySettings(preset); setActivePresetName(name); } setShowPresets(false); }; const handleDeletePreset = (name) => { const updated = { ...presets }; delete updated[name]; setPresets(updated); savePresets(updated); if (activePresetName === name) { setActivePresetName(DEFAULT_PRESET_NAME); } }; const exportRunParameters = (run) => { const exportPayload = { exportedAt: new Date().toISOString(), runId: run.id, presetName: run.presetName || DEFAULT_PRESET_NAME, parameters: run.settings || { timeframe: run.timeframe, riskReward: run.riskReward, }, queryParameters: run.queryParameters || null, summary: { dataset: run.dataset, timeframe: run.timeframe, riskReward: run.riskReward, totalPnl: run.totalPnl, winRate: run.winRate, totalTrades: run.totalTrades, }, }; const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); const safePreset = (run.presetName || DEFAULT_PRESET_NAME).replace(/[^a-z0-9_-]/gi, '_'); anchor.href = url; anchor.download = `backtest-params-${run.dataset || 'dataset'}-${safePreset}-${run.id}.json`; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); }; const toggleDay = (day) => { setDayFilter((prev) => { if (prev.includes(day)) { const next = prev.filter((d) => d !== day); return next.length ? next : prev; } return [...prev, day].sort(); }); }; useEffect(() => { if (!chartContainerRef.current) return; const chart = createChart(chartContainerRef.current, { width: chartContainerRef.current.clientWidth, height: 420, ...CHART_THEME, }); const candleSeries = chart.addSeries(CandlestickSeries, { upColor: '#10b981', downColor: '#ef4444', borderVisible: false, wickUpColor: '#10b981', wickDownColor: '#ef4444', }); chartRef.current = chart; candleSeriesRef.current = candleSeries; if (equityChartRef.current) { const eqChart = createChart(equityChartRef.current, { width: equityChartRef.current.clientWidth, height: 160, ...CHART_THEME, layout: { ...CHART_THEME.layout, fontSize: 10 }, }); equityChartObjRef.current = eqChart; equitySeriesRef.current = eqChart.addSeries(LineSeries, { color: '#10b981', lineWidth: 2, priceLineVisible: false, lastValueVisible: true, }); } setChartsReady(true); const handleResize = () => { if (chartContainerRef.current) chart.applyOptions({ width: chartContainerRef.current.clientWidth }); if (equityChartRef.current && equityChartObjRef.current) equityChartObjRef.current.applyOptions({ width: equityChartRef.current.clientWidth }); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); chart.remove(); equityChartObjRef.current?.remove(); }; }, []); const runBacktest = useCallback(async () => { if (!chartsReady) return; if (progressIntervalRef.current) clearInterval(progressIntervalRef.current); if (progressResetTimeoutRef.current) clearTimeout(progressResetTimeoutRef.current); setProgressPct(8); progressIntervalRef.current = setInterval(() => { setProgressPct((prev) => (prev < 92 ? prev + 3 : prev)); }, 140); setLoading(true); try { const params = new URLSearchParams({ timeframe: timeframe.toString(), rr: riskReward.toString(), lookback: lookback.toString(), atr_mult: atrMult.toString(), session, sweep: useLiquiditySweep.toString(), sweep_lookback: sweepLookback.toString(), ob_age: obMaxAge.toString(), dataset: selectedDataset, use_fvg: useFvg.toString(), use_ob: useOb.toString(), proximity_pct: proximityPct.toString(), min_gap_size: minGapSize.toString(), impulse_multiplier: impulseMultiplier.toString(), require_unmitigated_fvg: requireUnmitigatedFvg.toString(), require_bos_confluence: requireBosConfluence.toString(), min_ob_size: minObSize.toString(), require_fvg_ob_confluence: requireFvgObConfluence.toString(), asian_sweep_only: asianSweepOnly.toString(), use_break_even: useBreakEven.toString(), be_trigger_rr: beTriggerRr.toString(), use_partial_tp: usePartialTp.toString(), partial_tp_rr: partialTpRr.toString(), partial_tp_percent: partialTpPercent.toString(), day_filter: dayFilter.join(','), max_daily_loss: maxDailyLoss.toString(), max_consecutive_losses: maxConsecutiveLosses.toString(), }); const [candleRes, backtestRes] = await Promise.all([ fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}&dataset=${encodeURIComponent(selectedDataset)}`), fetch(`http://localhost:8000/api/backtest?${params}`), ]); const candleData = await candleRes.json(); const backtestData = await backtestRes.json(); const candles = candleData.candles.map((c) => ({ time: Math.floor(new Date(c.time).getTime() / 1000), open: c.open, high: c.high, low: c.low, close: c.close, })); candleSeriesRef.current?.setData(candles); const markers = []; if (backtestData.trades) { backtestData.trades.forEach((trade) => { const isWin = trade.pnl > 0; markers.push({ time: Math.floor(new Date(trade.enter_time).getTime() / 1000), position: trade.direction === 'long' ? 'belowBar' : 'aboveBar', color: isWin ? '#10b981' : '#ef4444', shape: trade.direction === 'long' ? 'arrowUp' : 'arrowDown', text: trade.direction === 'long' ? 'BUY' : 'SELL', }); markers.push({ time: Math.floor(new Date(trade.exit_time).getTime() / 1000), position: 'inBar', color: isWin ? '#10b981' : '#ef4444', shape: 'circle', text: isWin ? `+${trade.pnl.toFixed(2)}` : trade.pnl.toFixed(2), }); }); } markers.sort((a, b) => a.time - b.time); markersRef.current?.setMarkers([]); markersRef.current = createSeriesMarkers(candleSeriesRef.current, markers); chartRef.current?.timeScale().fitContent(); if (backtestData.trades?.length) { let equity = STARTING_BALANCE; const sortedTrades = backtestData.trades.slice().sort((a, b) => new Date(a.exit_time) - new Date(b.exit_time)); const eqData = sortedTrades.map((t) => { equity += t.pnl; return { time: Math.floor(new Date(t.exit_time).getTime() / 1000), value: Number(equity.toFixed(2)), }; }); equitySeriesRef.current?.setData(eqData); equityChartObjRef.current?.timeScale().fitContent(); } else { equitySeriesRef.current?.setData([]); } setResults(backtestData); onBacktestComplete?.(backtestData); if (backtestData?.stats) { const snapshotSettings = { ...getSettings(), dayFilter: [...dayFilter], }; const snapshot = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, runAt: new Date().toISOString(), dataset: selectedDataset, presetName: activePresetName, timeframe, riskReward, settings: snapshotSettings, queryParameters: Object.fromEntries(params.entries()), totalPnl: Number(backtestData.stats.total_pnl ?? 0), winRate: Number(backtestData.stats.win_rate ?? 0), totalTrades: Number(backtestData.stats.total_trades ?? 0), }; setRecentResults((prev) => { const next = [snapshot, ...prev].slice(0, RESULT_HISTORY_LIMIT); saveRecentResults(next); return next; }); } } catch (err) { console.error('Backtest failed:', err); } finally { if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); progressIntervalRef.current = null; } setProgressPct(100); setLoading(false); progressResetTimeoutRef.current = setTimeout(() => { setProgressPct(0); }, 500); } }, [ chartsReady, timeframe, riskReward, lookback, atrMult, session, useFvg, useOb, useLiquiditySweep, sweepLookback, obMaxAge, selectedDataset, proximityPct, minGapSize, impulseMultiplier, requireUnmitigatedFvg, requireBosConfluence, minObSize, requireFvgObConfluence, asianSweepOnly, dayFilter, useBreakEven, beTriggerRr, usePartialTp, partialTpRr, partialTpPercent, maxDailyLoss, maxConsecutiveLosses, onBacktestComplete, activePresetName, ]); const runMonteCarlo = useCallback(async () => { if (!results?.trades?.length) { setMcErrorMessage('Run a backtest first so Monte Carlo has trades to simulate.'); return; } const trades = results.trades; const totalTradesLocal = trades.length; const totalPnlLocal = trades.reduce((sum, trade) => sum + Number(trade.pnl ?? 0), 0); const winRateLocal = totalTradesLocal ? (trades.filter((trade) => trade.pnl > 0).length / totalTradesLocal) * 100 : 0; setMcLoading(true); setMcErrorMessage(''); setMcResult(null); try { const response = await fetch('http://localhost:8000/api/backtest/monte-carlo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ trade_r_multiples: results.trades.map((trade) => Number(trade.r_multiple ?? 0)), runs: mcRuns, starting_balance: STARTING_BALANCE, risk_per_trade_pct: 1, sampling_method: mcShuffleTrades ? 'shuffle' : 'bootstrap', missed_trade_pct: 0, pnl_variation_pct: mcVariationPct, price_noise_pct: mcPriceNoisePct, slippage_per_trade: mcSlippage, spread_per_trade: mcSpread, ruin_drawdown_pct: mcRuinDrawdownPct, base_trade_count: totalTradesLocal, base_net_pnl: totalPnlLocal, base_win_rate: winRateLocal, base_profit_factor: calculateProfitFactor(results.trades), base_max_drawdown_pct: calculateMaxDrawdown(results.trades, STARTING_BALANCE), }), }); const data = await response.json(); if (!response.ok) { throw new Error(data?.detail ?? 'Monte Carlo run failed'); } setMcResult(data); } catch (err) { setMcErrorMessage(err instanceof Error ? err.message : 'Monte Carlo run failed'); } finally { setMcLoading(false); } }, [ results, mcRuns, mcVariationPct, mcPriceNoisePct, mcSlippage, mcSpread, mcRuinDrawdownPct, mcShuffleTrades, ]); useEffect(() => { if (chartsReady && autoRun) runBacktest(); }, [runBacktest, chartsReady, autoRun]); useEffect(() => { let f2 = 0; const f1 = requestAnimationFrame(() => { f2 = requestAnimationFrame(() => { if (chartRef.current && chartContainerRef.current) { chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth }); chartRef.current.timeScale().fitContent(); } if (equityChartObjRef.current && equityChartRef.current) { equityChartObjRef.current.applyOptions({ width: equityChartRef.current.clientWidth }); equityChartObjRef.current.timeScale().fitContent(); } }); }); return () => { cancelAnimationFrame(f1); cancelAnimationFrame(f2); }; }, []); useEffect(() => () => { if (progressIntervalRef.current) clearInterval(progressIntervalRef.current); if (progressResetTimeoutRef.current) clearTimeout(progressResetTimeoutRef.current); }, []); const stats = results?.stats; const totalTrades = stats?.total_trades ?? 0; const winRate = stats?.win_rate ?? 0; const totalPnl = stats?.total_pnl ?? 0; const partialTpTrades = stats?.partial_tp_trades ?? 0; const partialTpRate = stats?.partial_tp_rate ?? 0; const partialTpRealized = stats?.partial_tp_realized_total ?? 0; const presetNames = Object.keys(presets); const mcRunsData = mcResult?.sample_runs ?? mcResult?.distribution ?? []; const itemVariants = { hidden: { opacity: 0, y: 18 }, visible: { opacity: 1, y: 0, transition: { duration: 0.52, ease: [0.22, 1, 0.36, 1] } }, }; return (
{/* Header row */}

Backtesting Engine

Adjust parameters and run strategy

{loading &&
}
{loading ? 'Running backtest...' : 'Backtest progress'} {Math.round(progressPct)}%
{/* Presets panel */} {showPresets && (

Presets

{/* Save */}
setPresetName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSavePreset()} className="flex-1 border border-[#262626] bg-[#0a0a0a] text-[#fafafa] font-mono text-[13px] px-3 py-2 outline-none focus:border-[#404040] transition-colors" />
{/* List */} {presetNames.length === 0 ? (

No saved presets yet.

) : (
{presetNames.map((name) => (
{name}
))}
)}
)} {/* Core Strategy */}
Timeframe
{TIMEFRAMES.map((tf) => ( ))}
Risk : Reward
{RR_OPTIONS.map((rr) => ( ))}
Session
{SESSIONS.map((s) => ( ))}
Dataset
{DAYS.map((d) => ( ))}
{/* Chart */}

Backtest Chart

Trade entries and exits

{/* Equity */}

Equity Curve

Balance progression

{/* Results */} {stats && (

Results

Backtest Summary

CSV: {selectedDataset}

Preset: {activePresetName}

Last {RESULT_HISTORY_LIMIT} Runs

{recentResults.length === 0 ? (

No previous runs saved yet.

) : (
{recentResults.map((run) => (
{new Date(run.runAt).toLocaleString()} {run.dataset || 'Unknown CSV'} {run.timeframe ?? '-'}m {run.presetName || DEFAULT_PRESET_NAME} = 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}>${formatMoney(Number(run.totalPnl ?? 0))} {Number(run.winRate ?? 0).toFixed(1)}% {Number(run.totalTrades ?? 0)} trades
))}
)}

Monte Carlo

Stress test the current trade set

{mcErrorMessage && (
{mcErrorMessage}
)} {mcResult && (

Avg PnL

{Number(mcResult.summary?.avg_pnl ?? 0).toFixed(2)}

Profitable %

{Number(mcResult.summary?.profitable_run_pct ?? 0).toFixed(2)}%

Worst DD %

{Number(mcResult.summary?.worst_max_drawdown_pct ?? 0).toFixed(2)}%

Avg WR

{Number(mcResult.summary?.avg_win_rate ?? 0).toFixed(2)}%

Avg PF

{Number(mcResult.summary?.avg_profit_factor ?? 0).toFixed(2)}

Ruin %

{Number(mcResult.summary?.probability_of_ruin ?? 0).toFixed(2)}%

Run Net PnL Max DD % Win Rate PF Ruin
{mcRunsData.map((run) => (
{run.run} = 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}>{Number(run.net_pnl).toFixed(2)} {Number(run.max_drawdown_pct).toFixed(2)} {Number(run.win_rate).toFixed(2)}% {Number(run.profit_factor).toFixed(2)} {run.ruin ? 'Yes' : 'No'}
))}
)}

Net P/L

= 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}`}>${formatMoney(totalPnl)}

Win Rate

50 ? 'text-[#10b981]' : 'text-[#ef4444]'}`}>{winRate.toFixed(1)}%

Total Trades

{totalTrades}

Winners

{stats.winners}

Losers

{stats.losers}

Avg Win

${formatMoney(stats.avg_win)}

Avg Loss

${formatMoney(Math.abs(stats.avg_loss))}

Partials

{partialTpTrades}

Partial Rate

{partialTpRate.toFixed(1)}%

Partial P/L

= 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}`}>${formatMoney(partialTpRealized)}

)}
); }