119 lines
3.6 KiB
TypeScript
119 lines
3.6 KiB
TypeScript
import type { PaperSummaryDto, PaperTradeDto } from './backendApi';
|
|
import { buildAnalysisFromPaperTrades, computePaperMetrics } from './paperMetrics';
|
|
|
|
export interface LiveExecutionItem {
|
|
id: string;
|
|
symbol: string;
|
|
strategyLabel: string;
|
|
sourceLabel: string;
|
|
/** 체결 시점 평가 시간봉 */
|
|
timeframe: string;
|
|
executionType?: string;
|
|
strategyId?: number | null;
|
|
trades: PaperTradeDto[];
|
|
createdAt: string;
|
|
totalReturnPct: number;
|
|
winRatePct: number;
|
|
tradeCount: number;
|
|
roundTripCount: 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 '수동';
|
|
}
|
|
|
|
function resolveStrategyLabel(
|
|
trades: PaperTradeDto[],
|
|
strategyNames: Record<number, string>,
|
|
): string {
|
|
for (const t of trades) {
|
|
if (t.strategyName?.trim()) return t.strategyName.trim();
|
|
}
|
|
const first = trades[0];
|
|
if (first?.strategyId != null) {
|
|
return strategyNames[first.strategyId] ?? `전략 #${first.strategyId}`;
|
|
}
|
|
return '수동 매매';
|
|
}
|
|
|
|
/** 자동매매: 종목·전략·시간봉·실행방식 기준 / 수동: 종목·일자 기준 */
|
|
function groupKey(t: PaperTradeDto): string {
|
|
if (t.source === 'STRATEGY') {
|
|
const tf = t.candleType ?? 'unknown';
|
|
const exec = t.executionType ?? '';
|
|
return `${t.symbol}|${t.strategyId ?? 0}|${tf}|${exec}|STRATEGY`;
|
|
}
|
|
return `${t.symbol}|manual|${dayKey(t.createdAt)}|MANUAL`;
|
|
}
|
|
|
|
/** 가상투자·모의투자 체결 이력을 실행 단위 목록으로 그룹 */
|
|
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 = groupKey(t);
|
|
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 analysis = buildAnalysisFromPaperTrades(sorted, initial);
|
|
const metrics = computePaperMetrics(sorted, summary);
|
|
|
|
return {
|
|
id: key,
|
|
symbol: first.symbol,
|
|
strategyLabel: resolveStrategyLabel(sorted, strategyNames),
|
|
sourceLabel: sourceLabel(first.source),
|
|
timeframe: first.candleType ?? 'unknown',
|
|
executionType: first.executionType ?? undefined,
|
|
strategyId: first.strategyId,
|
|
trades: sorted,
|
|
createdAt: last.createdAt ?? first.createdAt ?? new Date().toISOString(),
|
|
totalReturnPct: analysis.totalReturnPct,
|
|
winRatePct: analysis.winRate,
|
|
tradeCount: sorted.length,
|
|
roundTripCount: analysis.numberOfPositions,
|
|
mddPct: analysis.maxDrawdownPct,
|
|
sharpeRatio: analysis.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,
|
|
quantity: t.quantity,
|
|
barIndex: 0,
|
|
}));
|
|
}
|