백테스팅 적용
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 운영 현황 대시보드 — 멀티스레드 파이프라인·JVM·트래픽·모의/실거래
|
||||
* GoldenChart Command Center — Master Plan [01] Standard + [04] System/Tech
|
||||
*/
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { Theme } from '../types';
|
||||
import {
|
||||
loadDashboardSummary,
|
||||
@@ -9,15 +9,18 @@ import {
|
||||
type ProcessMonitorDto,
|
||||
type SystemMonitorDto,
|
||||
} from '../utils/backendApi';
|
||||
import DashboardGauge from './dashboard/DashboardGauge';
|
||||
import DashboardSegmentBar from './dashboard/DashboardSegmentBar';
|
||||
import DashboardSparkChart from './dashboard/DashboardSparkChart';
|
||||
import DashboardIndicatorBar from './dashboard/DashboardIndicatorBar';
|
||||
import '../styles/strategyEditorTheme.css';
|
||||
import '../styles/dashboardCommand.css';
|
||||
|
||||
interface Props {
|
||||
theme: Theme;
|
||||
onGoChart?: (market: string) => void;
|
||||
}
|
||||
|
||||
const fmtKrw = (n: number) =>
|
||||
n >= 1e8 ? `${(n / 1e8).toFixed(2)}억` : n >= 1e4 ? `${(n / 1e4).toFixed(0)}만` : n.toLocaleString('ko-KR');
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
healthy: '정상',
|
||||
degraded: '주의',
|
||||
@@ -25,21 +28,10 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
disabled: '비활성',
|
||||
};
|
||||
|
||||
function statusClass(s: string): string {
|
||||
if (s === 'healthy') return 'dash-proc--ok';
|
||||
if (s === 'degraded') return 'dash-proc--warn';
|
||||
if (s === 'down') return 'dash-proc--down';
|
||||
return 'dash-proc--off';
|
||||
}
|
||||
const fmtKrw = (n: number) =>
|
||||
n >= 1e8 ? `${(n / 1e8).toFixed(2)}억` : n >= 1e4 ? `${(n / 1e4).toFixed(0)}만` : n.toLocaleString('ko-KR');
|
||||
|
||||
function formatMetricValue(v: unknown): string {
|
||||
if (typeof v === 'boolean') return v ? 'Y' : 'N';
|
||||
if (typeof v === 'number') {
|
||||
if (Number.isInteger(v)) return v.toLocaleString('ko-KR');
|
||||
return v.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
|
||||
}
|
||||
return String(v ?? '—');
|
||||
}
|
||||
const fmtPct = (v: number) => `${v >= 0 ? '+' : ''}${(v * 100).toFixed(1)}%`;
|
||||
|
||||
function formatAgo(ms: number): string {
|
||||
if (!ms) return '—';
|
||||
@@ -49,38 +41,65 @@ function formatAgo(ms: number): string {
|
||||
return `${Math.floor(sec / 3600)}시간 전`;
|
||||
}
|
||||
|
||||
function statusClass(s: string): string {
|
||||
if (s === 'healthy') return 'gc-proc--ok';
|
||||
if (s === 'degraded') return 'gc-proc--warn';
|
||||
if (s === 'down') return 'gc-proc--down';
|
||||
return 'gc-proc--off';
|
||||
}
|
||||
|
||||
function healthLevel(mon: SystemMonitorDto): 'green' | 'yellow' | 'red' {
|
||||
if (!mon.pipelineEnabled) return 'red';
|
||||
if (mon.jvm.heapPct > 85 || mon.queue.dropped > 0 || mon.staleMarkets.length > 5) return 'yellow';
|
||||
if (mon.processes.some(p => p.status === 'down')) return 'red';
|
||||
if (mon.processes.some(p => p.status === 'degraded')) return 'yellow';
|
||||
return 'green';
|
||||
}
|
||||
|
||||
const ProcessCard: React.FC<{ proc: ProcessMonitorDto }> = ({ proc }) => (
|
||||
<article className={`dash-proc ${statusClass(proc.status)}`}>
|
||||
<header className="dash-proc-head">
|
||||
<span className="dash-proc-layer">{proc.layer}</span>
|
||||
<span className={`dash-proc-badge ${statusClass(proc.status)}`}>
|
||||
{STATUS_LABEL[proc.status] ?? proc.status}
|
||||
</span>
|
||||
<article className={`gc-proc ${statusClass(proc.status)}`}>
|
||||
<header className="gc-proc-head">
|
||||
<span className="gc-proc-layer">{proc.layer}</span>
|
||||
<span className={`gc-proc-badge ${statusClass(proc.status)}`}>{STATUS_LABEL[proc.status] ?? proc.status}</span>
|
||||
</header>
|
||||
<h3 className="dash-proc-name">{proc.name}</h3>
|
||||
<ul className="dash-proc-metrics">
|
||||
{Object.entries(proc.metrics).slice(0, 6).map(([k, v]) => (
|
||||
<li key={k}>
|
||||
<span>{k}</span>
|
||||
<strong>{formatMetricValue(v)}</strong>
|
||||
</li>
|
||||
<h3 className="gc-proc-name">{proc.name}</h3>
|
||||
<ul className="gc-proc-metrics">
|
||||
{Object.entries(proc.metrics).slice(0, 4).map(([k, v]) => (
|
||||
<li key={k}><span>{k}</span><strong>{String(v ?? '—')}</strong></li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
);
|
||||
|
||||
const HISTORY_LEN = 48;
|
||||
|
||||
const DashboardPage: React.FC<Props> = ({ theme, onGoChart }) => {
|
||||
const [data, setData] = useState<DashboardSummaryDto | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const tickHistory = useRef<number[]>([]);
|
||||
const wsHistory = useRef<number[]>([]);
|
||||
const [, bump] = useState(0);
|
||||
|
||||
const pushHistory = useCallback((ref: React.MutableRefObject<number[]>, v: number) => {
|
||||
ref.current = [...ref.current.slice(-(HISTORY_LEN - 1)), v];
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setLoading(true);
|
||||
loadDashboardSummary()
|
||||
.then(d => { setData(d); setError(null); })
|
||||
.then(d => {
|
||||
if (d?.systemMonitor) {
|
||||
pushHistory(tickHistory, d.systemMonitor.traffic.ticksProcessedPerSec ?? 0);
|
||||
pushHistory(wsHistory, d.systemMonitor.traffic.wsMessagesPerSec ?? 0);
|
||||
bump(n => n + 1);
|
||||
}
|
||||
setData(d);
|
||||
setError(null);
|
||||
})
|
||||
.catch(e => setError((e as Error).message))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
}, [pushHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
@@ -88,202 +107,226 @@ const DashboardPage: React.FC<Props> = ({ theme, onGoChart }) => {
|
||||
return () => window.clearInterval(id);
|
||||
}, [refresh]);
|
||||
|
||||
const mon = data?.systemMonitor;
|
||||
const app = data?.app as Record<string, unknown> | undefined;
|
||||
const mon: SystemMonitorDto | undefined = data?.systemMonitor;
|
||||
const health = mon ? healthLevel(mon) : 'yellow';
|
||||
|
||||
const latencyMs = useMemo(() => {
|
||||
if (!mon) return 0;
|
||||
const tps = mon.traffic.ticksProcessedPerSec ?? 0;
|
||||
return tps > 0 ? Math.max(12, Math.round(1000 / Math.max(tps, 1))) : 45;
|
||||
}, [mon]);
|
||||
|
||||
const stompConnections = data?.monitoredMarkets ?? 0;
|
||||
|
||||
const ta4jPct = mon && mon.memory.maxBarsPerSeries > 0
|
||||
? Math.min(100, (mon.memory.totalBars / (mon.memory.series * mon.memory.maxBarsPerSeries || 1)) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className={`dashboard-page theme-${theme}`}>
|
||||
<header className="dashboard-header">
|
||||
<h1>운영 대시보드</h1>
|
||||
<div className="dashboard-header-actions">
|
||||
<div className={`gc-dash-page se-page se-page--${theme}`}>
|
||||
<header className="gc-dash-header">
|
||||
<div className="gc-dash-header-left">
|
||||
<span className="gc-dash-badge">[01] Standard Dashboard</span>
|
||||
<h1 className="gc-dash-title">GoldenChart Command Center</h1>
|
||||
<p className="gc-dash-sub">실시간 투자 분석 · 파이프라인 모니터링 · 시스템 헬스체크</p>
|
||||
</div>
|
||||
<div className="gc-dash-header-right">
|
||||
<span className="gc-dash-stat">Global Latency: <strong>{latencyMs}ms</strong></span>
|
||||
<span className="gc-dash-stat">Active Markets: <strong>{stompConnections}</strong></span>
|
||||
{mon && (
|
||||
<span className="dashboard-live">
|
||||
파이프라인 {mon.pipelineEnabled ? 'ON' : 'OFF'}
|
||||
{mon.sampledAtMs ? ` · ${formatAgo(mon.sampledAtMs)} 갱신` : ''}
|
||||
<span className="gc-dash-stat">
|
||||
Pipeline <strong className={mon.pipelineEnabled ? 'up' : 'down'}>{mon.pipelineEnabled ? 'ON' : 'OFF'}</strong>
|
||||
{mon.sampledAtMs ? ` · ${formatAgo(mon.sampledAtMs)}` : ''}
|
||||
</span>
|
||||
)}
|
||||
<button type="button" className="stg-btn" onClick={refresh} disabled={loading}>
|
||||
{loading ? '갱신 중…' : '새로고침'}
|
||||
<button type="button" className="gc-dash-btn" onClick={refresh} disabled={loading}>
|
||||
{loading ? '갱신…' : '새로고침'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <p className="dashboard-error">{error}</p>}
|
||||
{error && <p className="gc-dash-error">{error}</p>}
|
||||
|
||||
{data && mon && (
|
||||
<div className="dashboard-grid">
|
||||
{/* JVM · 트래픽 */}
|
||||
<section className="dashboard-card dashboard-card--wide dash-monitor-top">
|
||||
<h2>JVM · 트래픽</h2>
|
||||
<div className="dash-jvm-row">
|
||||
<div className="dash-jvm-heap">
|
||||
<div className="dash-jvm-label">
|
||||
Heap {mon.jvm.heapUsedMb} / {mon.jvm.heapMaxMb} MB ({mon.jvm.heapPct}%)
|
||||
<div className="gc-dash-body">
|
||||
{/* ── LEFT: Portfolio + Indicators ── */}
|
||||
<aside className="gc-dash-col gc-dash-col--left">
|
||||
<section className="gc-dash-card gc-dash-card--glow">
|
||||
<h2 className="gc-dash-card-title">포트폴리오</h2>
|
||||
<div className="gc-dash-kpi-hero">
|
||||
<div className="gc-dash-kpi-block">
|
||||
<span className="gc-dash-kpi-label">모의 총자산</span>
|
||||
<strong className="gc-dash-kpi-val">{fmtKrw(data.paper.totalAsset)}</strong>
|
||||
<span className={`gc-dash-kpi-sub ${data.paper.totalReturnPct >= 0 ? 'up' : 'down'}`}>
|
||||
{fmtPct(data.paper.totalReturnPct)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="dash-jvm-bar">
|
||||
<div
|
||||
className={`dash-jvm-fill${mon.jvm.heapPct > 85 ? ' dash-jvm-fill--warn' : ''}`}
|
||||
style={{ width: `${Math.min(100, mon.jvm.heapPct)}%` }}
|
||||
/>
|
||||
<div className="gc-dash-kpi-block">
|
||||
<span className="gc-dash-kpi-label">실거래 KRW</span>
|
||||
<strong className="gc-dash-kpi-val">{fmtKrw(data.live.krwBalance)}</strong>
|
||||
<span className="gc-dash-kpi-sub">{data.live.configured ? '연동됨' : '미설정'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="dashboard-kv dash-traffic-kv">
|
||||
<li><span>WS 메시지/s</span><strong>{mon.traffic.wsMessagesPerSec ?? 0}</strong></li>
|
||||
<li><span>틱 적재/s</span><strong>{mon.traffic.ticksEnqueuedPerSec ?? 0}</strong></li>
|
||||
<li><span>틱 처리/s</span><strong>{mon.traffic.ticksProcessedPerSec ?? 0}</strong></li>
|
||||
<li><span>주문 실행/s</span><strong>{mon.traffic.ordersExecutedPerSec ?? 0}</strong></li>
|
||||
<li><span>스레드</span><strong>{mon.jvm.threadCount}</strong></li>
|
||||
<li><span>Non-Heap</span><strong>{mon.jvm.nonHeapUsedMb} MB</strong></li>
|
||||
<ul className="gc-dash-kv">
|
||||
<li><span>모의 현금</span><strong>{fmtKrw(data.paper.cashBalance)}</strong></li>
|
||||
<li><span>보유 종목</span><strong>{data.paper.positions?.length ?? 0}</strong></li>
|
||||
<li><span>관심종목</span><strong>{data.watchlistCount}</strong></li>
|
||||
<li><span>전략 감시</span><strong>{data.monitoredMarkets}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Ta4j 메모리 */}
|
||||
<section className="dashboard-card">
|
||||
<h2>인메모리 캔들 (Ta4j)</h2>
|
||||
<ul className="dashboard-kv">
|
||||
<li><span>종목</span><strong>{mon.memory.markets}</strong></li>
|
||||
<li><span>시리즈</span><strong>{mon.memory.series}</strong></li>
|
||||
<li><span>총 봉 수</span><strong>{mon.memory.totalBars.toLocaleString('ko-KR')}</strong></li>
|
||||
<li><span>시리즈 상한</span><strong>{mon.memory.maxBarsPerSeries}</strong></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* 이벤트 큐 */}
|
||||
<section className="dashboard-card">
|
||||
<h2>이벤트 큐</h2>
|
||||
<ul className="dashboard-kv">
|
||||
<li><span>대기</span><strong>{mon.queue.pending}</strong></li>
|
||||
<li><span>용량</span><strong>{mon.queue.capacity?.toLocaleString('ko-KR') ?? '—'}</strong></li>
|
||||
<li><span>적재율</span><strong>{Math.round((mon.queue.fillRatio ?? 0) * 1000) / 10}%</strong></li>
|
||||
<li><span>드롭</span><strong className={mon.queue.dropped > 0 ? 'warn' : ''}>{mon.queue.dropped}</strong></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* 멀티스레드 프로세스 */}
|
||||
<section className="dashboard-card dashboard-card--wide">
|
||||
<h2>멀티스레드 프로세스</h2>
|
||||
<div className="dash-proc-grid">
|
||||
{mon.processes.map(p => (
|
||||
<ProcessCard key={p.id} proc={p} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 스레드 목록 */}
|
||||
<section className="dashboard-card dashboard-card--wide">
|
||||
<h2>관련 스레드</h2>
|
||||
{mon.threads.length === 0 ? (
|
||||
<p className="dashboard-muted">표시할 스레드 없음</p>
|
||||
) : (
|
||||
<table className="dashboard-table dashboard-table--compact">
|
||||
<thead>
|
||||
<tr><th>이름</th><th>ID</th><th>상태</th><th>데몬</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mon.threads.map(t => (
|
||||
<tr key={`${t.id}-${t.name}`}>
|
||||
<td><code>{t.name}</code></td>
|
||||
<td>{t.id}</td>
|
||||
<td>{t.state}</td>
|
||||
<td>{t.daemon ? 'Y' : 'N'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 틱 정체 종목 */}
|
||||
{mon.staleMarkets.length > 0 && (
|
||||
<section className="dashboard-card dashboard-card--wide dash-stale">
|
||||
<h2>틱 정체 종목 ({mon.staleMarkets.length})</h2>
|
||||
<table className="dashboard-table dashboard-table--compact">
|
||||
<thead>
|
||||
<tr><th>종목</th><th>마지막 틱 경과</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mon.staleMarkets.slice(0, 12).map(s => (
|
||||
<tr key={s.market}>
|
||||
<td>
|
||||
<button type="button" className="dashboard-link" onClick={() => onGoChart?.(s.market)}>
|
||||
{s.market.replace('KRW-', '')}
|
||||
</button>
|
||||
</td>
|
||||
<td>{s.lastTickAgeSec}초</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="dashboard-card">
|
||||
<h2>매매 모드</h2>
|
||||
<ul className="dashboard-kv">
|
||||
<li><span>운영 모드</span><strong>{String(app?.tradingMode ?? '—')}</strong></li>
|
||||
<li><span>실거래 자동</span><strong>{app?.liveAutoTradeEnabled ? 'ON' : 'OFF'}</strong></li>
|
||||
<li><span>모의 자동</span><strong>{app?.paperAutoTradeEnabled ? 'ON' : 'OFF'}</strong></li>
|
||||
<li><span>전략 체크</span><strong>{app?.liveStrategyCheck ? 'ON' : 'OFF'}</strong></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section className="gc-dash-card">
|
||||
<h2 className="gc-dash-card-title">시스템 상태</h2>
|
||||
<div className={`gc-traffic-light gc-traffic-light--${health}`} aria-label="시스템 상태">
|
||||
<span className="gc-traffic-bulb gc-traffic-bulb--r" />
|
||||
<span className="gc-traffic-bulb gc-traffic-bulb--y" />
|
||||
<span className="gc-traffic-bulb gc-traffic-bulb--g" />
|
||||
</div>
|
||||
<p className="gc-traffic-caption">
|
||||
{health === 'green' ? '정상 운영' : health === 'yellow' ? '주의 필요' : '점검 필요'}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-card">
|
||||
<h2>모니터링</h2>
|
||||
<ul className="dashboard-kv">
|
||||
<li><span>관심종목</span><strong>{data.watchlistCount}</strong></li>
|
||||
<li><span>전략 감시</span><strong>{data.monitoredMarkets}</strong></li>
|
||||
<li><span>FCM</span><strong>
|
||||
{data.fcm.pushEnabled ? (data.fcm.available ? '활성' : '설정됨·서버 미연결') : 'OFF'}
|
||||
</strong></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section className="gc-dash-card">
|
||||
<h2 className="gc-dash-card-title">리소스 인디케이터</h2>
|
||||
<div className="gc-ind-grid">
|
||||
<DashboardIndicatorBar label="JVM Heap" pct={mon.jvm.heapPct} />
|
||||
<DashboardIndicatorBar label="Event Queue" pct={(mon.queue.fillRatio ?? 0) * 100} />
|
||||
<DashboardIndicatorBar label="Ta4j Cache" pct={ta4jPct} />
|
||||
<DashboardIndicatorBar label="WS Load" pct={Math.min(100, (mon.traffic.wsMessagesPerSec ?? 0) * 2)} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-card">
|
||||
<h2>모의투자</h2>
|
||||
<ul className="dashboard-kv">
|
||||
<li><span>현금</span><strong>{fmtKrw(data.paper.cashBalance)} KRW</strong></li>
|
||||
<li><span>총자산</span><strong>{fmtKrw(data.paper.totalAsset)} KRW</strong></li>
|
||||
<li><span>보유 종목</span><strong>{data.paper.positions?.length ?? 0}</strong></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section className="gc-dash-card gc-dash-card--compact">
|
||||
<h2 className="gc-dash-card-title">매매 모드</h2>
|
||||
<ul className="gc-dash-kv">
|
||||
<li><span>운영 모드</span><strong>{String(app?.tradingMode ?? '—')}</strong></li>
|
||||
<li><span>실거래 자동</span><strong>{app?.liveAutoTradeEnabled ? 'ON' : 'OFF'}</strong></li>
|
||||
<li><span>모의 자동</span><strong>{app?.paperAutoTradeEnabled ? 'ON' : 'OFF'}</strong></li>
|
||||
<li><span>FCM</span><strong>{data.fcm.pushEnabled ? (data.fcm.available ? '활성' : '미연결') : 'OFF'}</strong></li>
|
||||
</ul>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section className="dashboard-card">
|
||||
<h2>실거래</h2>
|
||||
<ul className="dashboard-kv">
|
||||
<li><span>KRW</span><strong>{fmtKrw(data.live.krwBalance)}</strong></li>
|
||||
<li><span>보유</span><strong>{data.live.positions?.length ?? 0} 종목</strong></li>
|
||||
<li><span>연동</span><strong>{data.live.configured ? 'OK' : '미설정'}</strong></li>
|
||||
</ul>
|
||||
</section>
|
||||
{/* ── CENTER: Infrastructure + Chart ── */}
|
||||
<main className="gc-dash-col gc-dash-col--center">
|
||||
<section className="gc-dash-card gc-dash-card--wide">
|
||||
<h2 className="gc-dash-card-title">[04] 데이터 파이프라인 · Ta4j 엔진</h2>
|
||||
<div className="gc-infra-grid">
|
||||
<DashboardSegmentBar
|
||||
label="Ta4jStorage"
|
||||
valueLabel={`${mon.memory.markets}종목 · ${mon.memory.totalBars.toLocaleString()}봉`}
|
||||
pct={ta4jPct}
|
||||
tone="green"
|
||||
/>
|
||||
<DashboardSegmentBar
|
||||
label="Event Queue"
|
||||
valueLabel={`${mon.queue.pending} / ${mon.queue.capacity?.toLocaleString() ?? '—'}`}
|
||||
pct={(mon.queue.fillRatio ?? 0) * 100}
|
||||
tone={mon.queue.fillRatio > 0.7 ? 'amber' : 'cyan'}
|
||||
/>
|
||||
<DashboardSegmentBar
|
||||
label="Order Worker"
|
||||
valueLabel={`${mon.orders.executed} executed · ${mon.orders.failed} fail`}
|
||||
pct={Math.min(100, mon.orders.ratePerSecond * 10)}
|
||||
tone="cyan"
|
||||
/>
|
||||
<DashboardSegmentBar
|
||||
label="Tick Worker"
|
||||
valueLabel={`${mon.workers.processed.toLocaleString()} processed`}
|
||||
pct={mon.workers.active ? 85 : 20}
|
||||
tone={mon.workers.active ? 'green' : 'red'}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-card dashboard-card--wide">
|
||||
<h2>최근 시그널</h2>
|
||||
{data.recentSignals.length === 0 ? (
|
||||
<p className="dashboard-muted">최근 시그널 없음</p>
|
||||
) : (
|
||||
<table className="dashboard-table">
|
||||
<thead>
|
||||
<tr><th>종목</th><th>방향</th><th>가격</th><th>분봉</th><th>시각</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.recentSignals.map(s => (
|
||||
<tr key={s.id}>
|
||||
<td>
|
||||
<button type="button" className="dashboard-link" onClick={() => onGoChart?.(s.market)}>
|
||||
{s.market.replace('KRW-', '')}
|
||||
</button>
|
||||
</td>
|
||||
<td className={s.signalType === 'BUY' ? 'buy' : 'sell'}>{s.signalType}</td>
|
||||
<td>{s.price?.toLocaleString('ko-KR')}</td>
|
||||
<td>{s.candleType}</td>
|
||||
<td>{s.createdAt?.slice(0, 19) ?? '—'}</td>
|
||||
</tr>
|
||||
<section className="gc-dash-card gc-dash-card--chart">
|
||||
<DashboardSparkChart series={tickHistory.current} label="Tick 처리량 (실시간)" height={160} />
|
||||
</section>
|
||||
|
||||
<section className="gc-dash-card gc-dash-card--wide">
|
||||
<h2 className="gc-dash-card-title">멀티스레드 프로세스</h2>
|
||||
<div className="gc-proc-grid">
|
||||
{mon.processes.map(p => <ProcessCard key={p.id} proc={p} />)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{mon.staleMarkets.length > 0 && (
|
||||
<section className="gc-dash-card gc-dash-card--warn">
|
||||
<h2 className="gc-dash-card-title">틱 정체 종목 ({mon.staleMarkets.length})</h2>
|
||||
<div className="gc-stale-chips">
|
||||
{mon.staleMarkets.slice(0, 8).map(s => (
|
||||
<button key={s.market} type="button" className="gc-stale-chip" onClick={() => onGoChart?.(s.market)}>
|
||||
{s.market.replace('KRW-', '')} · {s.lastTickAgeSec}s
|
||||
</button>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* ── RIGHT: Gauges + Signals ── */}
|
||||
<aside className="gc-dash-col gc-dash-col--right">
|
||||
<section className="gc-dash-card gc-dash-card--glow">
|
||||
<h2 className="gc-dash-card-title">성과 · 리스크</h2>
|
||||
<div>
|
||||
<DashboardGauge
|
||||
label="JVM Heap"
|
||||
value={`${mon.jvm.heapPct}%`}
|
||||
pct={mon.jvm.heapPct}
|
||||
tone={mon.jvm.heapPct > 85 ? 'red' : 'green'}
|
||||
/>
|
||||
<DashboardGauge
|
||||
label="Queue Fill"
|
||||
value={`${Math.round((mon.queue.fillRatio ?? 0) * 100)}%`}
|
||||
pct={(mon.queue.fillRatio ?? 0) * 100}
|
||||
tone={(mon.queue.fillRatio ?? 0) > 0.7 ? 'amber' : 'cyan'}
|
||||
/>
|
||||
<DashboardGauge
|
||||
label="모의 수익률"
|
||||
value={fmtPct(data.paper.totalReturnPct)}
|
||||
pct={Math.min(100, Math.abs(data.paper.totalReturnPct) * 200)}
|
||||
max={100}
|
||||
tone={data.paper.totalReturnPct >= 0 ? 'green' : 'red'}
|
||||
/>
|
||||
<DashboardGauge
|
||||
label="WS msg/s"
|
||||
value={String(mon.traffic.wsMessagesPerSec ?? 0)}
|
||||
pct={Math.min(100, (mon.traffic.wsMessagesPerSec ?? 0) * 3)}
|
||||
tone="cyan"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="gc-dash-card gc-dash-card--chart-sm">
|
||||
<DashboardSparkChart series={wsHistory.current} label="WebSocket 수신" height={80} />
|
||||
</section>
|
||||
|
||||
<section className="gc-dash-card gc-dash-card--flex">
|
||||
<h2 className="gc-dash-card-title">최근 시그널 · 체결</h2>
|
||||
{data.recentSignals.length === 0 ? (
|
||||
<p className="gc-dash-muted">최근 시그널 없음</p>
|
||||
) : (
|
||||
<ul className="gc-signal-list">
|
||||
{data.recentSignals.slice(0, 12).map(s => {
|
||||
const buy = s.signalType === 'BUY';
|
||||
return (
|
||||
<li key={s.id} className={`gc-signal-item gc-signal-item--${buy ? 'buy' : 'sell'}`}>
|
||||
<span className="gc-signal-arrow">{buy ? '▲' : '▼'}</span>
|
||||
<button type="button" className="gc-signal-main" onClick={() => onGoChart?.(s.market)}>
|
||||
<strong>{buy ? 'BUY' : 'SELL'}</strong>
|
||||
<span>{s.market.replace('KRW-', '')}</span>
|
||||
<span className="gc-signal-price">@ {s.price?.toLocaleString('ko-KR')}</span>
|
||||
</button>
|
||||
<span className="gc-signal-time">{s.createdAt?.slice(11, 16) ?? ''}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user