백테스팅 적용

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
+89
View File
@@ -0,0 +1,89 @@
import type { PaperSummaryDto, PaperTradeDto } from './backendApi';
import { computePaperMetrics } from './paperMetrics';
export interface LiveExecutionItem {
id: string;
symbol: string;
strategyLabel: string;
sourceLabel: string;
trades: PaperTradeDto[];
createdAt: string;
totalReturnPct: number;
winRatePct: number;
tradeCount: number;
mddPct: number;
sharpeRatio: number;
profitFactor: number;
}
function dayKey(iso: string | null | undefined): string {
if (!iso) return 'unknown';
return iso.slice(0, 10);
}
function sourceLabel(source: string): string {
if (source === 'STRATEGY') return '자동';
return '수동';
}
/** 가상투자·모의투자 체결 이력을 실행 단위 목록으로 그룹 */
export function buildLiveExecutionItems(
trades: PaperTradeDto[],
summary: PaperSummaryDto | null,
strategyNames: Record<number, string> = {},
): LiveExecutionItem[] {
if (!trades.length) return [];
const groups = new Map<string, PaperTradeDto[]>();
for (const t of trades) {
const key = `${t.symbol}|${t.strategyId ?? 0}|${dayKey(t.createdAt)}|${t.source}`;
const list = groups.get(key) ?? [];
list.push(t);
groups.set(key, list);
}
const initial = summary?.initialCapital ?? 10_000_000;
return [...groups.entries()]
.map(([key, groupTrades]) => {
const sorted = [...groupTrades].sort((a, b) =>
(a.createdAt ?? '').localeCompare(b.createdAt ?? ''),
);
const first = sorted[0];
const last = sorted[sorted.length - 1];
const metrics = computePaperMetrics(sorted, summary);
const buyVol = sorted.filter(t => t.side === 'BUY').reduce((s, t) => s + t.netAmount, 0);
const sellVol = sorted.filter(t => t.side === 'SELL').reduce((s, t) => s + t.netAmount, 0);
const pnl = sellVol - buyVol;
const totalReturnPct = initial > 0 ? pnl / initial : 0;
return {
id: key,
symbol: first.symbol,
strategyLabel: first.strategyId != null
? (strategyNames[first.strategyId] ?? `전략 #${first.strategyId}`)
: '수동 매매',
sourceLabel: sourceLabel(first.source),
trades: sorted,
createdAt: last.createdAt ?? first.createdAt ?? new Date().toISOString(),
totalReturnPct,
winRatePct: metrics.winRatePct / 100,
tradeCount: sorted.length,
mddPct: metrics.mddPct / 100,
sharpeRatio: metrics.sharpeRatio,
profitFactor: metrics.profitFactor,
};
})
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
export function paperTradesToSignals(trades: PaperTradeDto[]) {
return trades
.filter(t => t.createdAt)
.map(t => ({
time: Math.floor(new Date(t.createdAt!).getTime() / 1000),
type: t.side as 'BUY' | 'SELL',
price: t.price,
barIndex: 0,
}));
}