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}
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)}
)}
);
}