분석레포트 화면 추가
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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<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 };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user