/** * The Quant Command Center — 10 Dimensions * TRADING_REPORT_UI_DESIGN.pdf 설계 기반 */ import React, { useMemo, useState } from 'react'; import type { Theme } from '../../types'; import type { AnalysisReportTypeId } from '../../utils/analysisReportTypes'; import { getReportType } from '../../utils/analysisReportTypes'; import type { AnalysisReportContext } from '../../utils/analysisReportContext'; import type { BacktestResultRecord } from '../../utils/backendApi'; import { estimateCagr, buildMonthlyMatrix, buildYtdByYear, buildReturnDistribution, buildHoldingPoints, buildAllocationSlices, buildVolatilitySeries, drawdownSeries, } from '../../utils/analysisReportMetrics'; import { repairUtf8Mojibake } from '../../utils/textEncoding'; /* ── 포맷 유틸 ── */ const pct = (v: number, dec = 2) => `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%`; const pctAbs= (v: number, dec = 1) => `${(v * 100).toFixed(dec)}%`; const num = (v: number, dec = 2) => (isFinite(v) ? v.toFixed(dec) : '—'); const wonFmt= (v: number) => { const abs = Math.abs(v); const sign = v < 0 ? '-' : v > 0 ? '+' : ''; if (abs >= 1e8) return `${sign}${(abs / 1e8).toFixed(1)}억`; if (abs >= 1e4) return `${sign}${(abs / 1e4).toFixed(0)}만`; return `${sign}${abs.toLocaleString('ko-KR')}원`; }; const colorCls = (v: number) => (v > 0 ? 'up' : v < 0 ? 'down' : ''); interface Props { typeId: AnalysisReportTypeId; ctx: AnalysisReportContext | null; theme: Theme; compareBacktest?: BacktestResultRecord | null; /** 현재 사용 안 함 — AnalysisReportPage 호환용 */ allBacktests?: BacktestResultRecord[]; compareLive?: unknown; } /* ── 공통 유틸 ── */ function fmtDateTime(ts: number) { const d = new Date(ts * 1000); return `${d.getFullYear().toString().slice(2)}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } function fmtDuration(sec: number) { if (sec < 3600) return `${Math.round(sec / 60)}m`; const h = Math.floor(sec / 3600); const m = Math.round((sec % 3600) / 60); return m > 0 ? `${h}h ${m}m` : `${h}h`; } /* ── SVG 스파크 라인 ── */ function SparkLine({ values, w = 400, h = 100, positive = true, fill = true, }: { values: number[]; w?: number; h?: number; positive?: boolean; fill?: boolean; }) { const path = useMemo(() => { if (values.length < 2) return { line: '', area: '' }; const lo = Math.min(...values); const hi = Math.max(...values); const range = hi - lo || 1; const pts = values.map((v, i) => { const x = (i / (values.length - 1)) * w; const y = h - 8 - ((v - lo) / range) * (h - 16); return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; }); const line = pts.join(' '); const area = `${line} L${w},${h} L0,${h} Z`; return { line, area }; }, [values, w, h]); const stroke = positive ? '#00e5ff' : '#ff1744'; const fillId = `sf${positive ? 'c' : 'r'}`; return ( {fill && } ); } /* ── 도넛 차트 ── */ function DonutChart({ slices, centerLabel, centerValue, }: { slices: { pct: number; color: string }[]; centerLabel: string; centerValue: string; }) { const r = 70; const c = 2 * Math.PI * r; let acc = 0; const total = slices.reduce((s, x) => s + x.pct, 0) || 1; return ( {slices.map((sl, i) => { const dash = (sl.pct / total) * c; const offset = c * 0.25 - acc; acc += dash; return ( ); })} {centerLabel} {centerValue} ); } /* ── VaR 반원 게이지 ── */ function VarGauge({ varPct }: { varPct: number }) { const abs = Math.abs(varPct) * 100; const degree = Math.min(180, abs * 9); // 0~20% → 0~180도 const rad = ((degree - 180) * Math.PI) / 180; const x = 90 + 70 * Math.cos(rad); const y = 90 + 70 * Math.sin(rad); return ( Low Risk High Risk VaR: {abs.toFixed(2)}% ); } /* ── 원형 게이지 (Efficiency) ── */ function CircularGauge({ value, label, max = 100 }: { value: number; label: string; max?: number }) { const r = 60; const c = 2 * Math.PI * r; const pct2 = Math.min(1, Math.max(0, value / max)); const dash = pct2 * c; const color = value >= max * 0.9 ? '#ffd700' : value >= max * 0.5 ? '#00e676' : '#ff1744'; return ( {value >= 99 ? '99+' : value.toFixed(value >= 10 ? 2 : 2)} {label} ); } /* ── 산포도 (Timing) ── */ function ScatterPlot({ points }: { points: { x: number; y: number; label?: string }[] }) { if (points.length === 0) { return
보유시간 데이터 없음
; } const maxX = Math.max(...points.map(p => p.x), 120); const maxY = Math.max(...points.map(p => p.y), 10); const W = 400; const H = 220; const pad = { l: 36, r: 12, t: 12, b: 28 }; const xScale = (v: number) => pad.l + (v / maxX) * (W - pad.l - pad.r); const yScale = (v: number) => H - pad.b - (v / maxY) * (H - pad.t - pad.b); const xTicks = [0, 15, 30, 45, 60, 80, 100, 120].filter(v => v <= maxX); const yTicks = [0, 50, 100, 150, 200].filter(v => v <= maxY); return ( {/* 그리드 */} {xTicks.map(v => ( {v} ))} {yTicks.map(v => ( {v}% ))} {/* 축 */} {/* 점 */} {points.map((p, i) => ( ))} {/* 축 레이블 */} Holding Duration (Minutes/Hours) P&L (%) ); } /* ── 뷰 쉘 ── */ function ViewShell({ ctx, typeId, children, }: { ctx: AnalysisReportContext | null; typeId: AnalysisReportTypeId; children: React.ReactNode; }) { const def = getReportType(typeId); if (!ctx) { return (
📊

좌측 목록에서 백테스팅 또는 실시간 투자 실행을 선택하세요.

); } return (
{ctx.symbol} {ctx.timeframe} {ctx.sourceTab === 'backtest' ? '백테스팅' : '실시간'} {repairUtf8Mojibake(ctx.strategyName)}
{children}
); } /* ══════════════════════════════════════════════════════════ 01 Standard — Assets & P&L + 3 Hero KPIs + Growth Chart ══════════════════════════════════════════════════════════ */ function StandardView({ ctx }: { ctx: AnalysisReportContext }) { const a = ctx.analysis; const cagr = a ? estimateCagr(a.totalReturnPct, ctx.signals) : 0; const equityVals = ctx.equityCurve.map(p => p.equity); const up = (a?.totalReturnPct ?? 0) >= 0; return (
{/* 좌측: Assets & P&L Summary */}
Assets & P&L Summary
Total Trades {a?.numberOfPositions ?? '—'}
Win Rate {pctAbs(a?.winRate ?? 0)}
Profit Factor {num(a?.profitLossRatio ?? 0)}
Average P&L {pct(a?.avgReturnPct ?? 0)}
Gross Profit {wonFmt(a?.grossProfit ?? 0)}
Gross Loss {wonFmt(a?.grossLoss ?? 0)}
Net P&L {wonFmt(a?.totalProfitLoss ?? 0)}
{/* 우측: 3 Hero KPIs + Growth Chart */}
{/* Total ROI */}
Total ROI
{pct(a?.totalReturnPct ?? 0)}
(CAGR: {pct(cagr)})
= 2 ? equityVals : [0, 1]} h={40} positive={up} />
{/* MDD */}
Max Drawdown (MDD)
{pct(a?.maxDrawdownPct ?? 0)}
{/* Sharpe */}
Sharpe Ratio
{num(a?.sharpeRatio ?? 0, 2)}
= 2 ? equityVals : [0, 1]} h={36} fill={false} />
{/* Assets Growth Chart */}
Assets Growth Chart
= 2 ? equityVals : [0, 1]} h={200} positive={up} />
); } /* ══════════════════════════════════════════════════════════ 02 Risk — Drawdown Depth + Volatility + VaR Gauge ══════════════════════════════════════════════════════════ */ function RiskView({ ctx }: { ctx: AnalysisReportContext }) { const a = ctx.analysis; const dd = useMemo(() => drawdownSeries(ctx.equityCurve), [ctx.equityCurve]); const vol = useMemo(() => buildVolatilitySeries(ctx.equityCurve), [ctx.equityCurve]); const ddVals = dd.map(d => d.ddPct * 100); const volVals = vol.map(v => v.vol); const mdd = a?.maxDrawdownPct ?? 0; return (
Stress-Testing Capital Preservation
  • Capital Drawdown (Underwater Curve): Peak-to-trough declines evaluated across the backtest period. Current MDD: {pct(mdd)}.
  • Volatility Metrics: Annualized standard deviation of returns and Ulcer Index (depth and duration of drawdowns).
  • Value at Risk (VaR - 95%): Maximum expected loss over a specific timeframe at a 95% confidence interval.
Drawdown Depth Current MDD: {pct(mdd)}
= 2 ? ddVals : [0, -0.01]} h={160} positive={false} />
Volatility Index
= 2 ? volVals : [0.5, 0.5, 0.5]} h={100} positive={true} fill={false} />
Low Volatility Channel
Value at Risk (VaR – 95%)
); } /* ══════════════════════════════════════════════════════════ 03 Efficiency — Profit Factor Gauge + Win Rate + Avg Win/Loss ══════════════════════════════════════════════════════════ */ function EfficiencyView({ ctx }: { ctx: AnalysisReportContext }) { const a = ctx.analysis; const pf = a?.profitLossRatio ?? 0; const wr = (a?.winRate ?? 0) * 100; const avgWin = a ? (a.grossProfit / (a.numberOfWinning || 1)) / (a.initialCapital || 1) * 100 : 0; const avgLoss = a ? Math.abs(a.grossLoss / (a.numberOfLosing || 1)) / (a.initialCapital || 1) * 100 : 0; const maxBar = Math.max(avgWin, avgLoss, 0.1); return (
Execution Quality and Profit Extraction

Profit Factor [{pf >= 99 ? '99.00' : pf.toFixed(2)}]: Gross Profit / Gross Loss. {pf >= 10 ? 'An extreme metric indicating near-perfect trade filtering.' : 'Measures overall profitability efficiency.'}

Win Rate [{pctAbs(a?.winRate ?? 0)}]: Profitable Trades / Total Trades ({a?.numberOfWinning ?? 0} consecutive wins).

Average Profit vs. Average Loss: Avg P&L stands at {pct(a?.avgReturnPct ?? 0)} {(a?.numberOfLosing ?? 0) === 0 ? 'with zero losing trades executed.' : '.'}

Win Rate {wr.toFixed(1)}%
+{avgWin.toFixed(2)}%
Avg Win
{avgLoss > 0 ? `-${avgLoss.toFixed(2)}%` : '0.00%'}
Avg Loss
); } /* ══════════════════════════════════════════════════════════ 04 Temporal — Seasonality Monthly Matrix ══════════════════════════════════════════════════════════ */ const MON_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; function TemporalView({ ctx }: { ctx: AnalysisReportContext }) { const monthly = useMemo(() => buildMonthlyMatrix(ctx.trades), [ctx.trades]); const ytdMap = useMemo(() => buildYtdByYear(monthly), [monthly]); const years = useMemo(() => [...new Set(monthly.map(r => r.year))].sort(), [monthly]); const map = useMemo(() => new Map(monthly.map(r => [`${r.year}-${r.month}`, r.returnPct])), [monthly]); if (years.length === 0) { return
📅

거래 데이터가 부족합니다.

; } return (

Seasonality and Time-Based Consistency

Monthly & Yearly Performance Matrix: Verifying that the algorithm generates consistent alpha regardless of specific macro-market phases.

)} {years.map(year => { const ytd = ytdMap.get(year) ?? 0; return ( {MON_LABELS.map((_, mi) => { const v = map.get(`${year}-${mi}`); const intensity = v != null ? Math.min(1, Math.abs(v) * 8) : 0; const bg = v == null ? 'transparent' : v > 0 ? `rgba(0,230,118,${0.15 + intensity * 0.65})` : v < 0 ? `rgba(255,23,68,${0.15 + intensity * 0.65})` : 'rgba(60,70,90,0.3)'; return ( ); })} ); })}
{MON_LABELS.map(m => {m}YTD
{year} {v != null ? pct(v, 2) : 0.00%} {pct(ytd, 2)}
); } /* ══════════════════════════════════════════════════════════ 05 Logs — Granular Trade Log (Entry+Exit Price, Duration) ══════════════════════════════════════════════════════════ */ interface TradeLogFull { id: number; dateTime: string; symbol: string; side: string; entryPrice: number; exitPrice: number; qty: number; pnlPct: number; durationSec: number; } function buildFullLogRows(ctx: AnalysisReportContext): TradeLogFull[] { if (ctx.sourceTab === 'live' && ctx.paperTrades.length > 0) { return ctx.paperTrades.map((t, i) => { const ts = t.createdAt ? Math.floor(new Date(t.createdAt).getTime() / 1000) : 0; return { id: t.id ?? i, dateTime: ts ? fmtDateTime(ts) : '—', symbol: t.symbol, side: t.side, entryPrice: t.side === 'SELL' ? t.price : t.price * 0.99, exitPrice: t.price, qty: t.quantity, pnlPct: t.realizedPnlDelta != null ? t.realizedPnlDelta / (t.price * t.quantity || 1) * 100 : 0, durationSec: 0, }; }); } const buys = new Map(); const rows: TradeLogFull[] = []; const sorted = [...ctx.trades].sort((a, b) => a.time - b.time); for (const t of sorted) { if (t.side === 'buy') { buys.set(t.symbol, { time: t.time, price: t.price, qty: t.quantity }); } else { const buy = buys.get(t.symbol); const entry = buy?.price ?? t.price; const dur = buy ? t.time - buy.time : 0; rows.push({ id: t.id, dateTime: fmtDateTime(t.time), symbol: t.symbol, side: 'BUY/SELL', entryPrice: entry, exitPrice: t.price, qty: t.quantity, pnlPct: (t.pnlPct ?? 0) * 100, durationSec: dur, }); buys.delete(t.symbol); } } return rows; } function LogsView({ ctx }: { ctx: AnalysisReportContext }) { const rows = useMemo(() => buildFullLogRows(ctx), [ctx]); return (

Granular Transparency for Every Execution

Data derived from the GoldenChart execution feed (100ms latency WebSocket).

{rows.length === 0 ? ( ) : rows.map(r => ( 0 ? 'qcc-row--up' : r.pnlPct < 0 ? 'qcc-row--down' : ''}> ))}
Date & Time Symbol Side Entry Price Exit Price Qty P&L (%) Duration
거래 내역 없음
{r.dateTime} {r.symbol.replace('KRW-', 'KRW-')} {r.side} {r.entryPrice.toLocaleString('ko-KR')} {r.exitPrice.toLocaleString('ko-KR')} {r.qty.toFixed(3)} = 0 ? 'up' : 'down'}>{r.pnlPct >= 0 ? '+' : ''}{r.pnlPct.toFixed(1)}% {r.durationSec > 0 ? fmtDuration(r.durationSec) : '—'}
); } /* ══════════════════════════════════════════════════════════ 06 Allocation — Donut + Legend Table ══════════════════════════════════════════════════════════ */ function AllocationView({ ctx }: { ctx: AnalysisReportContext }) { const a = ctx.analysis; const slices = useMemo( () => buildAllocationSlices(ctx.trades, a?.initialCapital ?? 10_000_000), [ctx.trades, a], ); const totalCapital = a?.initialCapital ?? 10_000_000; return (

Capital Distribution
and Asset Weights

Dynamic Capital Allocation: Visualizing the distribution of the initial simulated capital across different cryptocurrency pairs during the backtest.

Exposure Analysis: Validating risk distribution across multiple assets in accordance with the CAPITAL_PCT engine settings.

{slices.map(sl => (
{sl.symbol.replace('KRW-', 'KRW-')} {(sl.pct * 100).toFixed(2)}% ₩{Math.round(sl.capitalKrw).toLocaleString('ko-KR')}
))}
({ pct: sl.pct * 100, color: sl.color }))} centerLabel="Total Capital Deployed" centerValue={`₩${Math.round(totalCapital / 1e8).toLocaleString()}억`} />
); } /* ══════════════════════════════════════════════════════════ 07 Stats — Return Distribution Histogram ══════════════════════════════════════════════════════════ */ function StatsView({ ctx }: { ctx: AnalysisReportContext }) { const buckets = useMemo(() => buildReturnDistribution(ctx.trades), [ctx.trades]); const maxCount = Math.max(...buckets.map(b => b.count), 1); const W = 500; const H = 240; const pad = { l: 36, r: 20, t: 20, b: 32 }; const bw = buckets.length > 0 ? (W - pad.l - pad.r) / buckets.length - 2 : 0; return (
The Shape of Returns: Statistical Distribution
  • Right Tail: Outlier massive wins — extreme positive P&L executions.
  • Center Mass: The bulk of standard, high-probability trades falling in the +1% to +6% range.
  • Left Tail: Frequency and severity of losing trades.
{buckets.filter(b => b.count > 0).slice(0, 5).map(b => (
{b.bucket} {b.count}
))}
{buckets.length === 0 ? (
거래 분포 데이터 없음
) : ( {/* 그리드 */} {[0, 25, 50, 75, 100].map(p => { const y = H - pad.b - (p / 100) * (H - pad.t - pad.b); return ; })} {/* 막대 */} {buckets.map((b, i) => { const barH = (b.count / maxCount) * (H - pad.t - pad.b); const x = pad.l + i * ((W - pad.l - pad.r) / buckets.length); const isExtreme = b.from >= 100; const fill = isExtreme ? '#ffd700' : b.from >= 0 ? '#00e676' : '#ff1744'; return ( {b.count > 0 && ( {b.count} )} {isExtreme && b.count > 0 && ( +{b.from}% )} {b.bucket.replace('%', '')} ); })} {/* Y축 */} {[0, 25, 50, 75, 100].map(p => { const cnt = Math.round((p / 100) * maxCount); const y = H - pad.b - (p / 100) * (H - pad.t - pad.b); return {cnt}; })} {/* 종 곡선 (정규분포 근사) */} {buckets.length >= 3 && (() => { const pts = buckets.map((_, i) => { const x = pad.l + (i + 0.5) * ((W - pad.l - pad.r) / buckets.length); const normI = (i - buckets.length / 2) / (buckets.length / 4); const bell = Math.exp(-0.5 * normI * normI) * 0.9; const y = H - pad.b - bell * (H - pad.t - pad.b); return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; }); return ; })()} Return % Buckets Frequency )}
); } /* ══════════════════════════════════════════════════════════ 08 AI Feedback — LLM Terminal Output ══════════════════════════════════════════════════════════ */ function AiFeedbackView({ ctx }: { ctx: AnalysisReportContext }) { const a = ctx.analysis; const mdd = a?.maxDrawdownPct ?? 0; const sharpe = a?.sharpeRatio ?? 0; const pf = a?.profitLossRatio ?? 0; const wr = a?.winRate ?? 0; const lines: { key: string; label: string; text: string }[] = [ { key: 'market', label: 'Market Context', text: sharpe > 1 ? `Strategy performed optimally during the backtest period. Sharpe ratio of ${sharpe.toFixed(2)} confirms risk-adjusted alpha generation with ${pctAbs(wr)} win rate.` : `Strategy shows moderate performance. Win rate of ${pctAbs(wr)} with Sharpe ${sharpe.toFixed(2)} suggests potential for parameter optimization.`, }, { key: 'indicator', label: 'Indicator Interpretation', text: pf > 2 ? `Profit Factor of ${pf.toFixed(2)} indicates highly efficient trade filtering. Gross profit significantly exceeds gross loss, validating the entry/exit logic.` : `Profit Factor of ${pf.toFixed(2)} is ${pf >= 1 ? 'profitable but moderate' : 'below breakeven'}. Review entry conditions to improve signal quality.`, }, { key: 'weakness', label: 'Weakness Detection', text: Math.abs(mdd) > 0.1 ? `MDD of ${pct(mdd)} suggests significant drawdown exposure. Consider adding an ATR-based trailing stop or reducing position sizing during volatile periods.` : `Maximum drawdown remains controlled at ${pct(mdd)}. Current risk management parameters appear well-calibrated for the tested market conditions.`, }, ]; return (

Qualitative Insights via Local LLM (qwen3:4b)

Powered by LangGraph strategy_tool and backtest_tool orchestration.

{lines.map(l => (
> {l.label}: {l.text}
))}
); } /* ══════════════════════════════════════════════════════════ 09 Comparative — Strategy vs. Buy & Hold ══════════════════════════════════════════════════════════ */ function ComparativeView({ ctx }: { ctx: AnalysisReportContext }) { const a = ctx.analysis; const stratVals = ctx.equityCurve.map(p => p.equity); const initial = a?.initialCapital ?? 10_000_000; // Buy & Hold 시리즈 재구성 (첫 신호 진입가→마지막 신호 가격) const benchVals = useMemo(() => { const signals = ctx.signals; if (signals.length < 2) return stratVals.map(() => initial); const p0 = signals[0].price; return ctx.equityCurve.map((_, i) => { const s = signals[Math.min(i, signals.length - 1)]; return initial * (s.price / p0); }); }, [ctx.signals, ctx.equityCurve, initial]); const W = 600; const H = 300; const pad = { l: 44, r: 80, t: 16, b: 28 }; const allVals = [...stratVals, ...benchVals]; const lo = Math.min(...allVals); const hi = Math.max(...allVals); const range = hi - lo || 1; const xS = (i: number) => pad.l + (i / (stratVals.length - 1)) * (W - pad.l - pad.r); const yS = (v: number) => H - pad.b - ((v - lo) / range) * (H - pad.t - pad.b); const stratPath = stratVals.map((v, i) => `${i === 0 ? 'M' : 'L'}${xS(i).toFixed(1)},${yS(v).toFixed(1)}`).join(' '); const benchPath = benchVals.map((v, i) => `${i === 0 ? 'M' : 'L'}${xS(i).toFixed(1)},${yS(v).toFixed(1)}`).join(' '); const benchPct = a?.buyAndHoldReturnPct ?? (benchVals.length > 1 ? (benchVals[benchVals.length - 1] - initial) / initial : 0); const stratRetFinal = a?.totalReturnPct ?? 0; return (

Strategy vs. Benchmark: Alpha Generation

Relative Performance Analytics: Proving the value of active, algorithmically traded signals over passive market exposure.

{/* 그리드 */} {[0, 25, 50, 75, 100].map(p => { const v = lo + (p / 100) * range; const y = yS(v); const lbl = `${(((v - initial) / initial) * 100).toFixed(0)}%`; return ( {lbl} ); })} {/* Bench area */} {/* Strategy */} {/* 우측 레이블 */} Return +{(stratRetFinal * 100).toFixed(0)}% Max drawdown {pct(a?.maxDrawdownPct ?? 0)} Sharpe {num(a?.sharpeRatio ?? 0)} Benchmark +{(benchPct * 100).toFixed(0)}%
━ GoldenChart Strategy ┅ Buy & Hold Benchmark
); } /* ══════════════════════════════════════════════════════════ 10 Timing — Holding Periods & Entry Precision Scatter ══════════════════════════════════════════════════════════ */ function TimingView({ ctx }: { ctx: AnalysisReportContext }) { const pts = useMemo( () => buildHoldingPoints(ctx.signals, ctx.symbol), [ctx.signals, ctx.symbol], ); return (
Holding Periods & Entry Precision

Time-Efficiency: Analyzing how long capital is locked up to generate returns, evaluating{' '} AVERAGE_HOLDING_PERIOD against the execution type.

Cluster Analysis: High concentration of short-duration trades signifies highly efficient, surgical market entries.

{pts.length > 0 && (
Avg Duration {fmtDuration(pts.reduce((s, p) => s + p.durationMin * 60, 0) / pts.length)}
Total Trades {pts.length}
Avg P&L +{(pts.reduce((s, p) => s + p.pnlPct, 0) / pts.length).toFixed(2)}%
)}
({ x: Math.min(p.durationMin, 200), y: Math.abs(p.pnlPct) }))} />
); } /* ══════════════════════════════════════════════════════════ 메인 라우터 ══════════════════════════════════════════════════════════ */ export default function AnalysisReportViews({ typeId, ctx, theme, compareBacktest }: Props) { const body = useMemo(() => { if (!ctx) return null; switch (typeId) { case 'standard': return ; case 'risk': return ; case 'efficiency': return ; case 'temporal': return ; case 'logs': return ; case 'allocation': return ; case 'stats': return ; case 'ai-feedback': return ; case 'comparative': return ; case 'timing': return ; default: return ; } }, [typeId, ctx, theme]); return ( {body} ); }