백테스팅 적용
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { BacktestAnalysis } from '../../utils/backendApi';
|
||||
import BacktestSparkline from './BacktestSparkline';
|
||||
import type { EquityPoint } from '../../utils/backtestEquity';
|
||||
import { krw, pct, pctAbs } from '../../utils/backtestUiUtils';
|
||||
|
||||
interface Props {
|
||||
analysis: BacktestAnalysis | null;
|
||||
equityCurve: EquityPoint[];
|
||||
totalReturnOverride?: number;
|
||||
winRateOverride?: number;
|
||||
tradeCountOverride?: number;
|
||||
winCountOverride?: number;
|
||||
mddOverride?: number;
|
||||
sharpeOverride?: number;
|
||||
profitFactorOverride?: number;
|
||||
netProfitOverride?: number;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
const num = (v: number, dec = 2) => (isFinite(v) ? v.toFixed(dec) : '–');
|
||||
|
||||
const fmtWon = (v: number) =>
|
||||
isFinite(v) ? `₩${Math.round(v).toLocaleString('ko-KR')}` : '–';
|
||||
|
||||
function MetricCell({
|
||||
label,
|
||||
value,
|
||||
tone,
|
||||
}: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
tone?: 'up' | 'down' | 'gold' | 'neutral';
|
||||
}) {
|
||||
return (
|
||||
<div className="btd-analysis-metric">
|
||||
<span className="btd-analysis-metric-label">{label}</span>
|
||||
<span className={`btd-analysis-metric-value${tone ? ` btd-analysis-metric-value--${tone}` : ''}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BacktestKpiPanel({
|
||||
analysis,
|
||||
equityCurve,
|
||||
totalReturnOverride,
|
||||
winRateOverride,
|
||||
tradeCountOverride,
|
||||
winCountOverride,
|
||||
mddOverride,
|
||||
sharpeOverride,
|
||||
profitFactorOverride,
|
||||
netProfitOverride,
|
||||
emptyText = '분석 결과를 선택하세요.',
|
||||
}: Props) {
|
||||
const positive = useMemo(() => {
|
||||
const ret = totalReturnOverride ?? analysis?.totalReturnPct ?? 0;
|
||||
return ret >= 0;
|
||||
}, [analysis, totalReturnOverride]);
|
||||
|
||||
if (!analysis && totalReturnOverride == null) {
|
||||
return <div className="btd-analysis-empty">{emptyText}</div>;
|
||||
}
|
||||
|
||||
const totalReturn = totalReturnOverride ?? analysis?.totalReturnPct ?? 0;
|
||||
const winRate = winRateOverride ?? analysis?.winRate ?? 0;
|
||||
const tradeCount = tradeCountOverride ?? analysis?.numberOfPositions ?? 0;
|
||||
const winCount = winCountOverride ?? analysis?.numberOfWinning ?? Math.round(tradeCount * winRate);
|
||||
const lossCount = analysis?.numberOfLosing ?? Math.max(0, tradeCount - winCount);
|
||||
const breakEven = analysis?.numberOfBreakEven ?? Math.max(0, tradeCount - winCount - lossCount);
|
||||
const mdd = mddOverride ?? analysis?.maxDrawdownPct ?? 0;
|
||||
const sharpe = sharpeOverride ?? analysis?.sharpeRatio ?? 0;
|
||||
const profitFactor = profitFactorOverride ?? (
|
||||
analysis && analysis.grossLoss !== 0
|
||||
? Math.abs(analysis.grossProfit / analysis.grossLoss)
|
||||
: analysis?.profitLossRatio ?? 0
|
||||
);
|
||||
const netProfit = netProfitOverride ?? analysis?.totalProfitLoss ?? 0;
|
||||
const initialCap = analysis?.initialCapital ?? 0;
|
||||
const finalEquity = analysis?.finalEquity ?? initialCap + netProfit;
|
||||
const grossProfit = analysis?.grossProfit ?? 0;
|
||||
const grossLoss = analysis?.grossLoss ?? 0;
|
||||
const avgWin = winCount > 0 ? grossProfit / winCount : 0;
|
||||
const avgLoss = lossCount > 0 ? grossLoss / lossCount : 0;
|
||||
const buyHold = analysis?.buyAndHoldReturnPct ?? 0;
|
||||
const vsHold = analysis?.vsBuyAndHold ?? 0;
|
||||
const maxRunup = analysis?.maxRunupPct ?? 0;
|
||||
const sortino = analysis?.sortinoRatio ?? 0;
|
||||
const calmar = analysis?.calmarRatio ?? 0;
|
||||
const var95 = analysis?.valueAtRisk95 ?? 0;
|
||||
const es = analysis?.expectedShortfall ?? 0;
|
||||
const plRatio = analysis?.profitLossRatio ?? profitFactor;
|
||||
const recovery = mdd !== 0 ? totalReturn / Math.abs(mdd) : 0;
|
||||
|
||||
return (
|
||||
<div className="btd-analysis-stack">
|
||||
<section className="btd-analysis-card btd-analysis-card--profit">
|
||||
<header className="btd-analysis-card-head">
|
||||
<h3 className="btd-analysis-card-title">수익성</h3>
|
||||
<span className="btd-analysis-card-tag">Profitability</span>
|
||||
</header>
|
||||
<div className="btd-analysis-card-body">
|
||||
<div className="btd-analysis-hero">
|
||||
<div className="btd-analysis-hero-main">
|
||||
<span className="btd-analysis-hero-label">총 수익률</span>
|
||||
<strong className={`btd-analysis-hero-value ${totalReturn >= 0 ? 'up' : 'down'}`}>
|
||||
{pct(totalReturn)}
|
||||
</strong>
|
||||
</div>
|
||||
<BacktestSparkline curve={equityCurve} positive={positive} width={80} height={30} />
|
||||
</div>
|
||||
<div className="btd-analysis-grid">
|
||||
<MetricCell label="초기 자본" value={fmtWon(initialCap)} />
|
||||
<MetricCell label="최종 자산" value={fmtWon(finalEquity)} tone={finalEquity >= initialCap ? 'up' : 'down'} />
|
||||
<MetricCell label="순이익" value={krw(netProfit)} tone={netProfit >= 0 ? 'up' : 'down'} />
|
||||
<MetricCell label="평균 수익률" value={pct(analysis?.avgReturnPct ?? 0)} tone={(analysis?.avgReturnPct ?? 0) >= 0 ? 'up' : 'down'} />
|
||||
<MetricCell label="총 이익 (Gross)" value={krw(grossProfit)} tone="up" />
|
||||
<MetricCell label="총 손실 (Gross)" value={krw(-Math.abs(grossLoss))} tone="down" />
|
||||
<MetricCell label="Buy & Hold" value={pct(buyHold)} tone={buyHold >= 0 ? 'up' : 'down'} />
|
||||
<MetricCell label="vs Buy & Hold" value={num(vsHold, 2)} tone={vsHold >= 1 ? 'up' : 'down'} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="btd-analysis-card btd-analysis-card--risk">
|
||||
<header className="btd-analysis-card-head">
|
||||
<h3 className="btd-analysis-card-title">리스크 관리</h3>
|
||||
<span className="btd-analysis-card-tag">Risk</span>
|
||||
</header>
|
||||
<div className="btd-analysis-card-body">
|
||||
<div className="btd-analysis-hero btd-analysis-hero--compact">
|
||||
<div className="btd-analysis-hero-main">
|
||||
<span className="btd-analysis-hero-label">MDD (최대낙폭)</span>
|
||||
<strong className="btd-analysis-hero-value down">{pct(mdd)}</strong>
|
||||
</div>
|
||||
<div className="btd-analysis-hero-side">
|
||||
<span className="btd-analysis-side-label">Max Runup</span>
|
||||
<strong className="btd-analysis-side-value up">{pct(maxRunup)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="btd-analysis-grid">
|
||||
<MetricCell label="Sharpe Ratio" value={num(sharpe)} tone="gold" />
|
||||
<MetricCell label="Sortino Ratio" value={num(sortino)} tone="gold" />
|
||||
<MetricCell label="Calmar Ratio" value={num(calmar)} tone="gold" />
|
||||
<MetricCell label="Recovery Factor" value={num(recovery)} tone={recovery >= 1 ? 'up' : 'neutral'} />
|
||||
<MetricCell label="VaR (95%)" value={pct(var95)} tone="down" />
|
||||
<MetricCell label="Expected Shortfall" value={pct(es)} tone="down" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="btd-analysis-card btd-analysis-card--stats">
|
||||
<header className="btd-analysis-card-head">
|
||||
<h3 className="btd-analysis-card-title">거래 통계</h3>
|
||||
<span className="btd-analysis-card-tag">Trades</span>
|
||||
</header>
|
||||
<div className="btd-analysis-card-body">
|
||||
<div className="btd-analysis-hero btd-analysis-hero--compact">
|
||||
<div className="btd-analysis-hero-main">
|
||||
<span className="btd-analysis-hero-label">승률</span>
|
||||
<strong className={`btd-analysis-hero-value ${winRate >= 0.5 ? 'up' : 'down'}`}>
|
||||
{pctAbs(winRate, 1)}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="btd-analysis-hero-side">
|
||||
<span className="btd-analysis-side-label">총 거래</span>
|
||||
<strong className="btd-analysis-side-value">{tradeCount}회</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="btd-analysis-grid">
|
||||
<MetricCell label="승리" value={`${winCount}회`} tone="up" />
|
||||
<MetricCell label="패배" value={`${lossCount}회`} tone="down" />
|
||||
<MetricCell label="무승부" value={`${breakEven}회`} tone="neutral" />
|
||||
<MetricCell label="체결 비율" value={tradeCount > 0 ? pctAbs(winCount / tradeCount, 0) : '0%'} tone="gold" />
|
||||
<MetricCell label="평균 수익" value={krw(avgWin)} tone="up" />
|
||||
<MetricCell label="평균 손실" value={krw(-Math.abs(avgLoss))} tone="down" />
|
||||
<MetricCell label="Profit Factor" value={num(profitFactor)} tone={profitFactor >= 1 ? 'up' : 'down'} />
|
||||
<MetricCell label="손익비 (P/L)" value={num(plRatio)} tone="gold" />
|
||||
</div>
|
||||
<p className="btd-analysis-footnote">
|
||||
총 {tradeCount}회 · 승 {winCount} / 패 {lossCount}
|
||||
{breakEven > 0 ? ` / 무 ${breakEven}` : ''}
|
||||
· PF {num(profitFactor)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user