Files
goldenChart/frontend/src/utils/analysisReportContext.ts
T
2026-06-08 10:08:39 +09:00

123 lines
3.8 KiB
TypeScript

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<string, number>();
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 };
});
}