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 { 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 finalEquity = cap * (1 + item.totalReturnPct); const analysis: BacktestAnalysis = { initialCapital: cap, finalEquity, totalReturnPct: item.totalReturnPct, totalProfitLoss: finalEquity - cap, grossProfit: 0, grossLoss: 0, avgReturnPct: item.tradeCount > 0 ? item.totalReturnPct / item.tradeCount : 0, profitLossRatio: item.profitFactor, numberOfPositions: item.tradeCount, numberOfWinning: Math.round(item.tradeCount * item.winRatePct), numberOfLosing: Math.max(0, item.tradeCount - Math.round(item.tradeCount * item.winRatePct)), numberOfBreakEven: 0, winRate: item.winRatePct, maxDrawdownPct: item.mddPct, maxRunupPct: 0, sharpeRatio: item.sharpeRatio, sortinoRatio: 0, calmarRatio: 0, valueAtRisk95: 0, expectedShortfall: 0, buyAndHoldReturnPct: 0, vsBuyAndHold: 0, }; return { sourceTab: 'live', strategyName: repairUtf8Mojibake(item.strategyLabel), symbol: item.symbol, timeframe: '1h', barCount: 300, 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 }; }); }