goldenChat base source add
This commit is contained in:
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* BacktestResultModal / BacktestDashboard
|
||||
* ─────────────────────────────────────────────────────────────────────
|
||||
* BacktestDashboard — 카드 기반 대시보드 (재사용 가능)
|
||||
* BacktestResultModal — 드래그 팝업으로 Dashboard를 감싸는 래퍼
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import type { BacktestAnalysis, BacktestSignal, BacktestStats } from '../utils/backendApi';
|
||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||
|
||||
// ── 포맷 유틸 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const pct = (v: number, dec = 2) =>
|
||||
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%` : '–';
|
||||
const pctAbs = (v: number, dec = 1) =>
|
||||
isFinite(v) && v !== 0 ? `${(v * 100).toFixed(dec)}%` : '–';
|
||||
const num = (v: number, dec = 2) =>
|
||||
isFinite(v) && v !== 0 ? v.toFixed(dec) : '–';
|
||||
const wonFmt = (v: number) => {
|
||||
if (!isFinite(v) || v === 0) return '–';
|
||||
const abs = Math.abs(Math.round(v));
|
||||
const s = abs >= 100_000_000 ? `${(abs/100_000_000).toFixed(2)}억`
|
||||
: abs >= 10_000 ? `${(abs/10_000).toFixed(1)}만`
|
||||
: abs.toLocaleString();
|
||||
return (v >= 0 ? '+' : '-') + s + '원';
|
||||
};
|
||||
const fmtDate = (ts: number) => {
|
||||
const d = new Date(ts * 1000);
|
||||
return `${d.getFullYear()}.${String(d.getMonth()+1).padStart(2,'0')}.${String(d.getDate()).padStart(2,'0')}`;
|
||||
};
|
||||
const fmtDateStr = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
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')}`;
|
||||
};
|
||||
|
||||
const colorCls = (v: number) => v > 0 ? 'brd-pos' : v < 0 ? 'brd-neg' : '';
|
||||
const colorStyle = (v: number): React.CSSProperties => ({
|
||||
color: v > 0 ? 'var(--brd-pos)' : v < 0 ? 'var(--brd-neg)' : undefined,
|
||||
});
|
||||
|
||||
// ── Props ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface BacktestResultModalProps {
|
||||
analysis?: BacktestAnalysis | null;
|
||||
stats?: BacktestStats | null;
|
||||
signals?: BacktestSignal[];
|
||||
strategyName: string;
|
||||
symbol: string;
|
||||
timeframe: string;
|
||||
barCount: number;
|
||||
onClose: () => void;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export interface BacktestDashboardProps {
|
||||
analysis: BacktestAnalysis | null;
|
||||
signals: BacktestSignal[];
|
||||
strategyName: string;
|
||||
symbol: string;
|
||||
timeframe: string;
|
||||
barCount: number;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
// ── 섹션 제목 ─────────────────────────────────────────────────────────────
|
||||
|
||||
function SectionTitle({ icon, title, right }: { icon: string; title: string; right?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="brd-section-title">
|
||||
<span className="brd-section-icon">{icon}</span>
|
||||
<span className="brd-section-text">{title}</span>
|
||||
{right && <span className="brd-section-right">{right}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── KPI 카드 ──────────────────────────────────────────────────────────────
|
||||
|
||||
function KpiCard({ icon, label, value, sub, valueColor, accent }:
|
||||
{ icon: string; label: string; value: string; sub: string; valueColor?: string; accent?: string }) {
|
||||
return (
|
||||
<div className={`brd-kpi-card ${accent ? `brd-kpi-card--${accent}` : ''}`}>
|
||||
<div className="brd-kpi-top">
|
||||
<span className="brd-kpi-icon">{icon}</span>
|
||||
<span className="brd-kpi-label">{label}</span>
|
||||
</div>
|
||||
<div className={`brd-kpi-value ${valueColor ?? ''}`}>{value}</div>
|
||||
<div className="brd-kpi-sub">{sub}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 미니 지표 행 (테이블 스타일) ─────────────────────────────────────────
|
||||
|
||||
function MetricRow({ label, value, cls, note }: { label: string; value: string; cls?: string; note?: string }) {
|
||||
return (
|
||||
<div className="brd-metric-row">
|
||||
<span className="brd-metric-label">
|
||||
{label}
|
||||
{note && <span className="brd-metric-note">{note}</span>}
|
||||
</span>
|
||||
<span className={`brd-metric-value ${cls ?? ''}`}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 승률 도넛 (CSS 원) ────────────────────────────────────────────────────
|
||||
|
||||
function WinRateCircle({ winRate, winning, total }: { winRate: number; winning: number; total: number }) {
|
||||
const r = 28;
|
||||
const circ = 2 * Math.PI * r;
|
||||
const pct2 = Math.max(0, Math.min(1, winRate));
|
||||
const dash = pct2 * circ;
|
||||
return (
|
||||
<div className="brd-donut-wrap">
|
||||
<svg width="72" height="72" viewBox="0 0 72 72">
|
||||
<circle cx="36" cy="36" r={r} fill="none" stroke="var(--bg4)" strokeWidth="8"/>
|
||||
<circle
|
||||
cx="36" cy="36" r={r} fill="none"
|
||||
stroke={pct2 >= 0.5 ? 'var(--brd-pos)' : 'var(--brd-neg)'}
|
||||
strokeWidth="8"
|
||||
strokeDasharray={`${dash} ${circ}`}
|
||||
strokeLinecap="round"
|
||||
strokeDashoffset={circ * 0.25}
|
||||
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}
|
||||
/>
|
||||
<text x="36" y="33" textAnchor="middle" fontSize="11" fontWeight="800" fill="var(--text)">{pctAbs(winRate)}</text>
|
||||
<text x="36" y="45" textAnchor="middle" fontSize="8.5" fill="var(--text3)">승률</text>
|
||||
</svg>
|
||||
<div className="brd-donut-detail">
|
||||
<span className="brd-pos">▲ {winning}</span>
|
||||
<span className="brd-neg">▼ {total - winning}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 수평 비교 바 ─────────────────────────────────────────────────────────
|
||||
|
||||
function CompareBar({ labelA, valA, labelB, valB, colorA, colorB }:
|
||||
{ labelA: string; valA: number; labelB: string; valB: number; colorA: string; colorB: string }) {
|
||||
const max = Math.max(Math.abs(valA), Math.abs(valB), 0.01);
|
||||
const pctA2 = Math.min(100, Math.abs(valA) / max * 100);
|
||||
const pctB2 = Math.min(100, Math.abs(valB) / max * 100);
|
||||
return (
|
||||
<div className="brd-compare">
|
||||
<div className="brd-compare-row">
|
||||
<span className="brd-compare-label">{labelA}</span>
|
||||
<div className="brd-compare-track">
|
||||
<div className="brd-compare-bar" style={{ width: `${pctA2}%`, background: colorA }}/>
|
||||
</div>
|
||||
<span className="brd-compare-val" style={{ color: colorA }}>{pct(valA)}</span>
|
||||
</div>
|
||||
<div className="brd-compare-row">
|
||||
<span className="brd-compare-label">{labelB}</span>
|
||||
<div className="brd-compare-track">
|
||||
<div className="brd-compare-bar" style={{ width: `${pctB2}%`, background: colorB }}/>
|
||||
</div>
|
||||
<span className="brd-compare-val" style={{ color: colorB }}>{pct(valB)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 시그널 타입 ───────────────────────────────────────────────────────────
|
||||
|
||||
const SIG_LABEL: Record<string, string> = {
|
||||
BUY:'매수', SELL:'매도', SHORT_ENTRY:'공매도', SHORT_EXIT:'공매도청산', PARTIAL_SELL:'분할매도',
|
||||
};
|
||||
const SIG_COLOR: Record<string, string> = {
|
||||
BUY:'brd-pos', SELL:'brd-neg', SHORT_ENTRY:'brd-neg', SHORT_EXIT:'brd-pos', PARTIAL_SELL:'brd-warn',
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// BacktestDashboard — 메인 대시보드 컴포넌트 (재사용 가능)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function BacktestDashboard({
|
||||
analysis, signals, strategyName, symbol, timeframe, barCount, createdAt,
|
||||
}: BacktestDashboardProps) {
|
||||
const [sigExpanded, setSigExpanded] = useState(false);
|
||||
|
||||
const a = analysis;
|
||||
if (!a) {
|
||||
return (
|
||||
<div className="brd-empty-state">
|
||||
<div>📊</div>
|
||||
<p>결과 데이터가 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sigLimit = sigExpanded ? signals.length : 8;
|
||||
const periodStr = signals.length >= 2
|
||||
? `${fmtDate(signals[0].time)} ~ ${fmtDate(signals[signals.length-1].time)}`
|
||||
: `${barCount}봉`;
|
||||
|
||||
const sharpeLevel = a.sharpeRatio >= 2 ? '탁월' : a.sharpeRatio >= 1 ? '우수' : a.sharpeRatio >= 0 ? '보통' : '미흡';
|
||||
|
||||
return (
|
||||
<div className="brd-dashboard">
|
||||
|
||||
{/* ── 대시보드 헤더 ── */}
|
||||
<div className="brd-dash-header">
|
||||
<div className="brd-dash-header-left">
|
||||
<div className="brd-dash-strategy">{strategyName}</div>
|
||||
<div className="brd-dash-meta">
|
||||
<span className="brd-dash-badge">{symbol}</span>
|
||||
<span className="brd-dash-badge">{timeframe}</span>
|
||||
<span className="brd-dash-badge">{periodStr}</span>
|
||||
{createdAt && <span className="brd-dash-badge brd-dash-badge--time">실행 {fmtDateStr(createdAt)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="brd-dash-header-kpi">
|
||||
<div className="brd-dash-big-kpi">
|
||||
<div className={`brd-dash-big-val ${colorCls(a.totalReturnPct)}`}>{pct(a.totalReturnPct)}</div>
|
||||
<div className="brd-dash-big-label">총 수익률</div>
|
||||
</div>
|
||||
<div className="brd-dash-big-kpi">
|
||||
<div className={`brd-dash-big-val ${colorCls(a.finalEquity - a.initialCapital)}`}>
|
||||
{wonFmt(a.finalEquity)}
|
||||
</div>
|
||||
<div className="brd-dash-big-label">최종 자산</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Row 1: KPI 카드 6개 ── */}
|
||||
<div className="brd-row">
|
||||
<div className="brd-kpi-grid">
|
||||
<KpiCard icon="🎯" label="승률" value={pctAbs(a.winRate)} sub={`${a.numberOfWinning}승 ${a.numberOfLosing}패`} valueColor={colorCls(a.winRate - 0.5)} accent="blue"/>
|
||||
<KpiCard icon="📉" label="최대 낙폭" value={pct(a.maxDrawdownPct)} sub="기간 중 최대 손실" valueColor={a.maxDrawdownPct < 0 ? 'brd-neg' : ''} accent="red"/>
|
||||
<KpiCard icon="⚡" label="샤프 비율" value={num(a.sharpeRatio)} sub={sharpeLevel} valueColor={colorCls(a.sharpeRatio - 1)} accent="yellow"/>
|
||||
<KpiCard icon="🔄" label="소르티노" value={num(a.sortinoRatio)} sub="하방 변동성 기준" valueColor={colorCls(a.sortinoRatio - 1)} accent="purple"/>
|
||||
<KpiCard icon="⚖️" label="손익비" value={num(a.profitLossRatio)} sub="이익/손실 배수" valueColor={colorCls(a.profitLossRatio - 1)} accent="green"/>
|
||||
<KpiCard icon="📐" label="칼마 비율" value={num(a.calmarRatio)} sub="수익 / |MDD|" valueColor={colorCls(a.calmarRatio - 1)} accent="teal"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Row 2: 수익성 + 거래 통계 + 승률 도넛 ── */}
|
||||
<div className="brd-row brd-row--3col">
|
||||
|
||||
{/* 수익성 지표 카드 */}
|
||||
<div className="brd-card">
|
||||
<SectionTitle icon="📊" title="수익성 지표"/>
|
||||
<div className="brd-card-body">
|
||||
<MetricRow label="총 수익률" value={pct(a.totalReturnPct)} cls={colorCls(a.totalReturnPct)}/>
|
||||
<MetricRow label="총 손익 (금액)" value={wonFmt(a.totalProfitLoss)} cls={colorCls(a.totalProfitLoss)}/>
|
||||
<MetricRow label="총 이익" value={wonFmt(a.grossProfit)} cls="brd-pos"/>
|
||||
<MetricRow label="총 손실" value={wonFmt(a.grossLoss)} cls="brd-neg"/>
|
||||
<MetricRow label="평균 수익률/거래" value={pct(a.avgReturnPct)} cls={colorCls(a.avgReturnPct)}/>
|
||||
<MetricRow label="최대 상승폭" value={pct(a.maxRunupPct)} cls="brd-pos"/>
|
||||
<MetricRow label="손익분기 거래" value={`${a.numberOfBreakEven}회`}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 리스크 지표 카드 */}
|
||||
<div className="brd-card">
|
||||
<SectionTitle icon="🛡" title="리스크 지표"/>
|
||||
<div className="brd-card-body">
|
||||
<MetricRow label="최대 낙폭 (MDD)" value={pct(a.maxDrawdownPct)} cls={colorCls(a.maxDrawdownPct)} note="고점 대비"/>
|
||||
<MetricRow label="샤프 비율" value={num(a.sharpeRatio)} cls={colorCls(a.sharpeRatio - 1)} note="≥1 우수"/>
|
||||
<MetricRow label="소르티노 비율" value={num(a.sortinoRatio)} cls={colorCls(a.sortinoRatio - 1)} note="하방 기준"/>
|
||||
<MetricRow label="칼마 비율" value={num(a.calmarRatio)} cls={colorCls(a.calmarRatio - 1)}/>
|
||||
<MetricRow label="VaR 95%" value={pct(a.valueAtRisk95)} cls="brd-neg" note="최대 손실 예상"/>
|
||||
<MetricRow label="기대 손실 (CVaR)" value={pct(a.expectedShortfall)} cls="brd-neg" note="꼬리 리스크"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 승률 + 자본 카드 */}
|
||||
<div className="brd-card">
|
||||
<SectionTitle icon="🔢" title="거래 통계"/>
|
||||
<div className="brd-card-body brd-card-body--center">
|
||||
<WinRateCircle winRate={a.winRate} winning={a.numberOfWinning} total={a.numberOfPositions}/>
|
||||
<div className="brd-trade-stats">
|
||||
<MetricRow label="총 거래" value={`${a.numberOfPositions}회`}/>
|
||||
<MetricRow label="수익 거래" value={`${a.numberOfWinning}회`} cls="brd-pos"/>
|
||||
<MetricRow label="손실 거래" value={`${a.numberOfLosing}회`} cls="brd-neg"/>
|
||||
<MetricRow label="초기 자본" value={`${Math.round(a.initialCapital/10000).toLocaleString()}만원`}/>
|
||||
<MetricRow label="최종 자산" value={`${Math.round(a.finalEquity/10000).toLocaleString()}만원`} cls={colorCls(a.finalEquity - a.initialCapital)}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Row 3: 벤치마크 비교 + 자본 분석 ── */}
|
||||
<div className="brd-row brd-row--2col">
|
||||
|
||||
{/* 벤치마크 비교 */}
|
||||
<div className="brd-card">
|
||||
<SectionTitle icon="📐" title="벤치마크 비교" right={
|
||||
<span className={`brd-badge-inline ${colorCls(a.vsBuyAndHold - 1)}`}>
|
||||
{isFinite(a.vsBuyAndHold) && a.vsBuyAndHold !== 0
|
||||
? `${a.vsBuyAndHold >= 1 ? '전략 우위 ' : ''}${a.vsBuyAndHold.toFixed(2)}×`
|
||||
: '–'
|
||||
}
|
||||
</span>
|
||||
}/>
|
||||
<div className="brd-card-body">
|
||||
<CompareBar
|
||||
labelA="전략 수익률" valA={a.totalReturnPct}
|
||||
labelB="바이앤홀드" valB={a.buyAndHoldReturnPct}
|
||||
colorA={a.totalReturnPct >= 0 ? 'var(--brd-pos)' : 'var(--brd-neg)'}
|
||||
colorB="var(--text3)"
|
||||
/>
|
||||
<div className="brd-bench-summary">
|
||||
<div className="brd-bench-item">
|
||||
<span className="brd-bench-label">전략</span>
|
||||
<span className={`brd-bench-val ${colorCls(a.totalReturnPct)}`}>{pct(a.totalReturnPct)}</span>
|
||||
</div>
|
||||
<div className="brd-bench-divider"/>
|
||||
<div className="brd-bench-item">
|
||||
<span className="brd-bench-label">바이앤홀드</span>
|
||||
<span className={`brd-bench-val ${colorCls(a.buyAndHoldReturnPct)}`}>{pct(a.buyAndHoldReturnPct)}</span>
|
||||
</div>
|
||||
<div className="brd-bench-divider"/>
|
||||
<div className="brd-bench-item">
|
||||
<span className="brd-bench-label">초과 수익</span>
|
||||
<span className={`brd-bench-val ${colorCls(a.totalReturnPct - a.buyAndHoldReturnPct)}`}>
|
||||
{pct(a.totalReturnPct - a.buyAndHoldReturnPct)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수익/손실 상세 */}
|
||||
<div className="brd-card">
|
||||
<SectionTitle icon="💰" title="손익 분석"/>
|
||||
<div className="brd-card-body">
|
||||
<div className="brd-pnl-bar-section">
|
||||
<div className="brd-pnl-row">
|
||||
<span className="brd-pnl-label brd-pos">총 이익</span>
|
||||
<div className="brd-pnl-track">
|
||||
<div className="brd-pnl-bar brd-pnl-bar--pos" style={{
|
||||
width: `${a.grossProfit > 0 && Math.abs(a.grossLoss) > 0
|
||||
? Math.min(100, a.grossProfit / (a.grossProfit + Math.abs(a.grossLoss)) * 100)
|
||||
: a.grossProfit > 0 ? 100 : 0}%`
|
||||
}}/>
|
||||
</div>
|
||||
<span className="brd-pnl-val brd-pos">{wonFmt(a.grossProfit)}</span>
|
||||
</div>
|
||||
<div className="brd-pnl-row">
|
||||
<span className="brd-pnl-label brd-neg">총 손실</span>
|
||||
<div className="brd-pnl-track">
|
||||
<div className="brd-pnl-bar brd-pnl-bar--neg" style={{
|
||||
width: `${Math.abs(a.grossLoss) > 0 && a.grossProfit > 0
|
||||
? Math.min(100, Math.abs(a.grossLoss) / (a.grossProfit + Math.abs(a.grossLoss)) * 100)
|
||||
: Math.abs(a.grossLoss) > 0 ? 100 : 0}%`
|
||||
}}/>
|
||||
</div>
|
||||
<span className="brd-pnl-val brd-neg">{wonFmt(a.grossLoss)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="brd-pnl-net">
|
||||
<span className="brd-pnl-net-label">순 손익</span>
|
||||
<span className={`brd-pnl-net-val ${colorCls(a.totalProfitLoss)}`}>{wonFmt(a.totalProfitLoss)}</span>
|
||||
</div>
|
||||
<MetricRow label="손익비" value={num(a.profitLossRatio)} cls={colorCls(a.profitLossRatio - 1)}/>
|
||||
<MetricRow label="평균 수익률/거래" value={pct(a.avgReturnPct)} cls={colorCls(a.avgReturnPct)}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Row 4: 거래 시그널 목록 ── */}
|
||||
<div className="brd-row">
|
||||
<div className="brd-card brd-card--full">
|
||||
<SectionTitle icon="📋" title="거래 시그널" right={
|
||||
<span className="brd-section-count">{signals.length}건</span>
|
||||
}/>
|
||||
{signals.length === 0 ? (
|
||||
<div className="brd-empty-msg">시그널이 없습니다.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="brd-sig-scroll">
|
||||
<table className="brd-sig-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width:36}}>#</th>
|
||||
<th>날짜</th>
|
||||
<th>구분</th>
|
||||
<th style={{textAlign:'right'}}>가격</th>
|
||||
<th style={{textAlign:'center',width:50}}>봉#</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signals.slice(0, sigLimit).map((s, i) => (
|
||||
<tr key={i} className={i % 2 === 0 ? 'brd-sig-even' : ''}>
|
||||
<td style={{textAlign:'center',color:'var(--text3)'}}>{i+1}</td>
|
||||
<td style={{fontVariantNumeric:'tabular-nums'}}>{fmtDate(s.time)}</td>
|
||||
<td>
|
||||
<span className={`brd-sig-badge ${SIG_COLOR[s.type] ?? ''}`}>
|
||||
{SIG_LABEL[s.type] ?? s.type}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{textAlign:'right',fontVariantNumeric:'tabular-nums',fontWeight:600}}>
|
||||
{Math.round(s.price).toLocaleString()}
|
||||
</td>
|
||||
<td style={{textAlign:'center',color:'var(--text3)'}}>{s.barIndex}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{signals.length > 8 && (
|
||||
<button className="brd-expand-btn" onClick={() => setSigExpanded(v => !v)}>
|
||||
{sigExpanded ? '▲ 접기' : `▼ 전체 보기 (${signals.length}건)`}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// BacktestResultModal — 팝업 래퍼
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function BacktestResultModal({
|
||||
analysis, stats, signals = [], strategyName, symbol, timeframe, barCount, onClose, embedded = false,
|
||||
}: BacktestResultModalProps) {
|
||||
|
||||
const {
|
||||
panelRef: modalRef,
|
||||
dragging,
|
||||
onHeaderPointerDown, headerTouchStyle,
|
||||
panelStyle,
|
||||
headerCursor,
|
||||
} = useDraggablePanel({ centerOnMount: !embedded });
|
||||
|
||||
const a = analysis ?? (stats ? buildAnalysisFromStats(stats) : null);
|
||||
if (!a && !stats) return null;
|
||||
|
||||
const inner = (
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`brm-modal ${embedded ? 'brm-modal--embedded' : ''}`}
|
||||
style={embedded ? undefined : {
|
||||
...panelStyle,
|
||||
zIndex: 9200,
|
||||
cursor: dragging ? 'grabbing' : undefined,
|
||||
}}
|
||||
onMouseDown={embedded ? undefined : e => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className="gc-popup-header brm-header"
|
||||
onPointerDown={embedded ? undefined : onHeaderPointerDown}
|
||||
style={embedded ? undefined : { cursor: headerCursor, ...headerTouchStyle }}
|
||||
>
|
||||
<div className="brm-header-left">
|
||||
<span className="brm-header-title">백테스팅 결과</span>
|
||||
</div>
|
||||
{!embedded && (
|
||||
<button className="brm-close" onClick={onClose} title="닫기">✕</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="brm-body">
|
||||
<BacktestDashboard
|
||||
analysis={a}
|
||||
signals={signals}
|
||||
strategyName={strategyName}
|
||||
symbol={symbol}
|
||||
timeframe={timeframe}
|
||||
barCount={barCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (embedded) return inner;
|
||||
|
||||
return (
|
||||
<div className="brm-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildAnalysisFromStats(s: BacktestStats): BacktestAnalysis {
|
||||
return {
|
||||
initialCapital: 10_000_000,
|
||||
finalEquity: s.finalEquity ?? 0,
|
||||
totalReturnPct: s.totalReturn ?? 0,
|
||||
totalProfitLoss: (s.finalEquity ?? 0) - 10_000_000,
|
||||
grossProfit: 0, grossLoss: 0,
|
||||
avgReturnPct: s.avgReturn ?? 0,
|
||||
profitLossRatio: 0,
|
||||
numberOfPositions: s.totalTrades ?? 0,
|
||||
numberOfWinning: s.winTrades ?? 0,
|
||||
numberOfLosing: (s.totalTrades ?? 0) - (s.winTrades ?? 0),
|
||||
numberOfBreakEven: 0,
|
||||
winRate: s.winRate ?? 0,
|
||||
maxDrawdownPct: s.maxDrawdown ?? 0,
|
||||
maxRunupPct: 0, sharpeRatio: 0, sortinoRatio: 0, calmarRatio: 0,
|
||||
valueAtRisk95: 0, expectedShortfall: 0,
|
||||
buyAndHoldReturnPct: 0, vsBuyAndHold: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export default BacktestResultModal;
|
||||
Reference in New Issue
Block a user