백테스팅 적용

This commit is contained in:
Macbook
2026-05-25 20:23:54 +09:00
parent 02d14e4b2b
commit cae1c3a624
33 changed files with 3399 additions and 385 deletions
@@ -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>
);
}