/* global React, TICKERS, STRATEGIES, ORDERS_SEED, AUDIT_SEED, genEquity, krw, krwSigned, pct, fmtPx, sigQty, LineChart, Sparkline, StratBar */ const { useState, useEffect, useMemo } = React; // =============== KPI STRIP =============== function KpiStrip({ s }) { const items = [ { k: "오늘 손익", v: krwSigned(Math.round(s.todayPnl)), sub: s.liveView ? '실현 (장중 mark 제외)' : pct(s.todayPnlPct), tone: s.todayPnl >= 0 ? 'up' : 'down', }, { k: s.liveView ? "총 평가손익" : "누적 손익", v: krwSigned(Math.round(s.cumPnl)), sub: pct(s.cumPnlPct), tone: s.cumPnl >= 0 ? 'up' : 'down', }, { k: s.liveView ? "총 보유자산" : "총 자본", v: krw(Math.round(s.totalEquity)), sub: s.liveView ? `원화 ${krw(Math.round(s.krwBalance))} + 평가 ${krw(Math.round(s.totalValueKrw))}` : `원화 + ${s.slotsUsed}종목`, tone: 'accent', }, { k: "포지션", v: `${s.slotsUsed}/${s.slotsMax}`, sub: `슬롯 ${Math.round((s.slotsUsed / s.slotsMax) * 100)}% 사용`, tone: '', }, { k: "일일 손실 소진", v: `${Math.round(s.lossUsedPct)}%`, sub: `한도 ${krw(s.dailyLossLimit)}`, tone: s.lossUsedPct > 60 ? 'down' : '', }, { k: "승률 · 30일", v: `${(s.winRate30d || 0).toFixed(1)}%`, sub: `샤프 ${(s.sharpe30d || 0).toFixed(2)}`, tone: (s.winRate30d || 0) >= 50 ? 'up' : '', }, ]; const sparkValues = s.equity.map(e => e.v); return (
{items.map((k, i) => (
{k.k}
{k.v}
{k.sub}
{i === 1 && (
)}
))}
); } // =============== EQUITY CELL =============== function EquityCell({ s }) { const [range, setRange] = useState('30D'); const baseline = (s.risk && s.risk.paper_initial_krw) || (s.totalEquity - s.cumPnl) || 0; const points = useMemo(() => { const days = range === '7D' ? 7 : range === '14D' ? 14 : range === '30D' ? 30 : 90; const slice = range === 'ALL' ? s.equity : s.equity.slice(-days); return slice.map((e, i) => ({ x: i, y: e.v, label: e.d })); }, [s.equity, range]); const latest = points[points.length - 1]?.y || 0; const first = points[0]?.y || 0; const change = latest - first; const changePct = first > 0 ? (change / first) * 100 : 0; const modeLabel = (s.mode === 'live' ? '실거래' : '페이퍼') + ' · 원화'; return (

자본 곡선

{modeLabel}
{[['7D','7일'], ['14D','14일'], ['30D','30일'], ['ALL','전체']].map(([r, lbl]) => ( ))}
자본 기준선 {krw(Math.round(baseline))}
{krw(Math.round(latest))}
= 0 ? 'var(--up)' : 'var(--down)' }}> {krwSigned(Math.round(change))} ({pct(changePct)})
{points.length > 0 && ( ({ x: p.x, y: baseline, label: p.label })) }, ]} formatY={(y) => (y / 1_000_000).toFixed(1) + 'M'} /> )}
); } // =============== POSITIONS CELL =============== function PositionsCell({ s }) { return (

보유 포지션

{s.slotsUsed}/{s.slotsMax} 슬롯 · 실시간 평가
{s.tickers.map(t => { const isOpen = t.qty > 0; const upnl = isOpen ? (t.price - t.entry) * t.qty : 0; const upnlPct = isOpen ? ((t.price - t.entry) / t.entry) * 100 : 0; const stop = t.entry * 0.98; return ( ); })}
종목 전략 수량 진입가 현재가 손익 수익률 평가액 손절가
{t.sym} {STRATEGIES.find(st => st.id === t.strat)?.code || '—'} {isOpen ? sigQty(t.qty) : } {isOpen ? fmtPx(t.entry) : } {fmtPx(Math.round(t.price))}{t.price_stale ? ' ·' : ''} = 0 ? 'up' : 'down') : 'muted'}> {isOpen && !t.price_stale ? krwSigned(Math.round(upnl)) : '—'} = 0 ? 'up' : 'down') : 'muted'}> {isOpen && !t.price_stale ? pct(upnlPct) : '—'} {isOpen ? krw(Math.round(t.qty * t.price)) : } {isOpen ? fmtPx(Math.round(stop)) : '—'}
); } // =============== STRATEGY PERFORMANCE CELL =============== function StrategyPerfCell({ s }) { const maxAbs = Math.max(...STRATEGIES.map(x => Math.abs(x.pnl30d))); return (

전략 성과 · 30일

walk-forward
{STRATEGIES.find(x => x.id === s.activeStrat)?.code} 활성
{STRATEGIES.slice().sort((a, b) => b.pnl30d - a.pnl30d).map(st => ( ))}
코드 전략 손익 30일 샤프 승률 매매 분포
{st.code} {st.name} = 0 ? 'up' : 'down'}>{pct(st.pnl30d, 1)} {st.sharpe.toFixed(2)} {st.winRate}% {st.trades} = 0 ? 'var(--up)' : 'var(--down)'} />
); } // =============== ORDER STREAM CELL =============== function OrderStreamCell({ s }) { const [filter, setFilter] = useState('ALL'); const filtered = useMemo(() => { if (filter === 'ALL') return s.orders; return s.orders.filter(o => o.side === filter); }, [s.orders, filter]); return (

주문 스트림

최근 24시간 · 체결 {s.orders.length}건
{[['ALL','전체'], ['BUY','매수'], ['SELL','매도']].map(([f, lbl]) => ( ))}
{filtered.map((o, i) => ( ))}
시간 방향 종목 전략 가격 수량 금액 상태
{o.ts} {o.side === 'BUY' ? '매수' : '매도'} {o.sym} {o.strat} {fmtPx(o.px)} {sigQty(o.qty)} {krw(o.krw)} {o.status}
); } // =============== DASHBOARD =============== function Dashboard({ s }) { return ( <>
); } Object.assign(window, { Dashboard });