import type { BacktestAnalysis, BacktestResultRecord, BacktestSignal, PaperTradeDto, } from './backendApi'; import type { LiveExecutionItem } from './liveExecutionGroups'; import type { EquityPoint, TradeHistoryRow } from './backtestEquity'; import { buildEquityFromSignals } from './backtestEquity'; import { paperTradesToSignals } from './liveExecutionGroups'; import { buildAnalysisFromPaperTrades } from './paperMetrics'; import { repairUtf8Mojibake } from './textEncoding'; export type AnalysisSourceTab = 'backtest' | 'live'; export interface AnalysisReportContext { sourceTab: AnalysisSourceTab; strategyName: string; symbol: string; timeframe: string; barCount: number; createdAt?: string; analysis: BacktestAnalysis | null; signals: BacktestSignal[]; trades: TradeHistoryRow[]; equityCurve: EquityPoint[]; paperTrades: PaperTradeDto[]; backtestRecord?: BacktestResultRecord; liveItem?: LiveExecutionItem; compareBacktest?: BacktestResultRecord | null; } function parseBacktestRecord(r: BacktestResultRecord) { let analysis: BacktestAnalysis | null = null; let signals: BacktestSignal[] = []; try { if (r.analysisJson) analysis = JSON.parse(r.analysisJson); if (r.signalsJson) signals = JSON.parse(r.signalsJson); } catch { /* ignore */ } const cap = analysis?.initialCapital ?? 10_000_000; const eq = buildEquityFromSignals(signals, cap, r.symbol); return { analysis, signals, trades: eq.trades, equityCurve: eq.curve, }; } export function buildContextFromBacktest( r: BacktestResultRecord, compare?: BacktestResultRecord | null, ): AnalysisReportContext { const parsed = parseBacktestRecord(r); return { sourceTab: 'backtest', strategyName: repairUtf8Mojibake(r.strategyName || '전략 없음'), symbol: r.symbol, timeframe: r.timeframe, barCount: r.barCount || 300, createdAt: r.createdAt, analysis: parsed.analysis, signals: parsed.signals, trades: parsed.trades, equityCurve: parsed.equityCurve, paperTrades: [], backtestRecord: r, compareBacktest: compare ?? null, }; } export function buildContextFromLive(item: LiveExecutionItem): AnalysisReportContext { const signals = paperTradesToSignals(item.trades); const cap = 10_000_000; const eq = buildEquityFromSignals(signals, cap, item.symbol); const analysis = buildAnalysisFromPaperTrades(item.trades, cap); const timeframe = item.timeframe !== 'unknown' ? item.timeframe : '—'; return { sourceTab: 'live', strategyName: repairUtf8Mojibake(item.strategyLabel), symbol: item.symbol, timeframe, barCount: item.trades.length, createdAt: item.createdAt, analysis, signals, trades: eq.trades, equityCurve: eq.curve, paperTrades: item.trades, liveItem: item, compareBacktest: null, }; } /** 월별 수익률 히트맵 셀 (0~1 비율) */ export function buildMonthlyHeatmap( trades: TradeHistoryRow[], initialCapital: number, ): { year: number; month: number; returnPct: number }[] { const byKey = new Map(); for (const t of trades) { if (t.pnlPct == null) continue; const d = new Date(t.time * 1000); const key = `${d.getFullYear()}-${d.getMonth()}`; byKey.set(key, (byKey.get(key) ?? 0) + t.pnlPct); } const out: { year: number; month: number; returnPct: number }[] = []; for (const [key, v] of byKey) { const [y, m] = key.split('-').map(Number); out.push({ year: y, month: m, returnPct: v }); } return out.sort((a, b) => a.year - b.year || a.month - b.month); } export function drawdownSeries(curve: EquityPoint[]): { time: number; ddPct: number }[] { let peak = curve[0]?.equity ?? 0; return curve.map(p => { if (p.equity > peak) peak = p.equity; const dd = peak > 0 ? (p.equity - peak) / peak : 0; return { time: p.time, ddPct: dd }; }); }