전략분봉 저장 에러 문제 수정

This commit is contained in:
Macbook
2026-05-23 21:08:46 +09:00
parent eaf067db1c
commit 70ac67afe7
18 changed files with 2445 additions and 498 deletions
@@ -0,0 +1,165 @@
/**
* BacktestResultDashboard — 첨부 이미지 스타일 백테스팅 결과 대시보드
*/
import React, { useMemo } from 'react';
import type { BacktestAnalysis, BacktestSignal } from '../../utils/backendApi';
import { buildEquityFromSignals, type TradeHistoryRow } from '../../utils/backtestEquity';
import BacktestAssetChart from './BacktestAssetChart';
export interface BacktestResultDashboardProps {
analysis: BacktestAnalysis | null;
signals: BacktestSignal[];
strategyName: string;
symbol: string;
timeframe: string;
barCount: number;
createdAt?: string;
}
const pct = (v: number, dec = 1) =>
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%` : '0.0%';
const pctAbs = (v: number, dec = 0) =>
isFinite(v) ? `${(v * 100).toFixed(dec)}%` : '';
const num = (v: number, dec = 2) =>
isFinite(v) ? v.toFixed(dec) : '';
function fmtDateTime(ts: number): string {
const d = new Date(ts * 1000);
return `${d.getFullYear()}-${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 SectionTitle({ title, sub }: { title: string; sub?: string }) {
return (
<div className="btd-section-head">
<span className="btd-section-bar" />
<div>
<h2 className="btd-section-title">{title}</h2>
{sub && <p className="btd-section-sub">{sub}</p>}
</div>
</div>
);
}
export function BacktestResultDashboard({
analysis,
signals,
strategyName: _strategyName,
symbol,
timeframe: _timeframe,
barCount: _barCount,
createdAt: _createdAt,
}: BacktestResultDashboardProps) {
const a = analysis;
const { curve, markers, trades } = useMemo(() => {
if (!a) return { curve: [], markers: [], trades: [] };
return buildEquityFromSignals(signals, a.initialCapital, symbol.startsWith('KRW-') ? symbol : `KRW-${symbol.replace(/^KRW-/, '')}`);
}, [a, signals, symbol]);
if (!a) {
return (
<div className="btd-empty">
<span>📊</span>
<p> .</p>
</div>
);
}
const profitFactor = a.profitLossRatio > 0 ? a.profitLossRatio : (
a.grossLoss !== 0 ? Math.abs(a.grossProfit / a.grossLoss) : 0
);
return (
<div className="btd-dashboard">
<div className="btd-section btd-section--kpi">
<SectionTitle title="핵심 성과 지표" sub="Core KPIs" />
<div className="btd-kpi-grid">
<div className="btd-kpi-card">
<div className="btd-kpi-label"> (ROI)</div>
<div className={`btd-kpi-value ${a.totalReturnPct >= 0 ? 'up' : 'down'}`}>
{pct(a.totalReturnPct)}
</div>
<p className="btd-kpi-desc"> </p>
</div>
<div className="btd-kpi-card">
<div className="btd-kpi-label"> (MDD)</div>
<div className={`btd-kpi-value ${a.maxDrawdownPct <= 0 ? 'down' : ''}`}>
{pct(a.maxDrawdownPct)}
</div>
<p className="btd-kpi-desc"> </p>
</div>
<div className="btd-kpi-card btd-kpi-card--sharpe">
<div className="btd-kpi-label"> (Sharpe Ratio)</div>
<div className="btd-kpi-row">
<span className={`btd-kpi-value ${a.sharpeRatio >= 1 ? 'up' : a.sharpeRatio >= 0 ? 'neutral' : 'down'}`}>{num(a.sharpeRatio)}</span>
<span className="btd-kpi-icon" aria-hidden></span>
</div>
<p className="btd-kpi-desc"> </p>
</div>
<div className="btd-kpi-card btd-kpi-card--dual">
<div className="btd-kpi-label"> Profit Factor</div>
<div className="btd-kpi-dual">
<div className="btd-kpi-value up">{pctAbs(a.winRate)}</div>
<div className="btd-kpi-divider" />
<div className="btd-kpi-value gold">{num(profitFactor)}</div>
</div>
</div>
</div>
</div>
<div className="btd-section btd-section--chart">
<SectionTitle title="자산 곡선 및 차트 시각화" sub="Asset Curve" />
<div className="btd-chart-card">
<BacktestAssetChart curve={curve} markers={markers} />
</div>
</div>
<div className="btd-section btd-section--table">
<SectionTitle title="상세 거래 내역" sub="Trade History Table" />
<div className="btd-table-card">
<div className="btd-table-scroll">
<table className="btd-table">
<thead>
<tr>
<th> </th>
<th></th>
<th></th>
<th></th>
<th></th>
<th> (%)</th>
</tr>
</thead>
<tbody>
{trades.length === 0 ? (
<tr>
<td colSpan={6} className="btd-table-empty"> .</td>
</tr>
) : trades.map((t: TradeHistoryRow) => (
<tr key={t.id}>
<td className="btd-td-time">{fmtDateTime(t.time)}</td>
<td>{t.symbol.startsWith('KRW-') ? t.symbol : `KRW-${t.symbol}`}</td>
<td className={t.side === 'buy' ? 'buy' : 'sell'}>
{t.side === 'buy' ? '매수' : '매도'}
</td>
<td>{Math.round(t.price).toLocaleString()}</td>
<td>{t.quantity.toFixed(4)}</td>
<td className={t.pnlPct != null ? (t.pnlPct >= 0 ? 'up' : 'down') : ''}>
{t.pnlPct != null ? pct(t.pnlPct) : ''}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
export default BacktestResultDashboard;