/* 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} 활성
| 코드 |
전략 |
손익 30일 |
샤프 |
승률 |
매매 |
분포 |
{STRATEGIES.slice().sort((a, b) => b.pnl30d - a.pnl30d).map(st => (
| {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 });