전략평가 화면 수익률 요약표시

This commit is contained in:
Macbook
2026-06-19 07:44:14 +09:00
parent 2514e0adae
commit 994a2024c7
5 changed files with 415 additions and 51 deletions
+50
View File
@@ -195,6 +195,56 @@ export async function fetchBacktestCandleBundle(opts: {
return { bars, evaluationBarCount, warmupBarCount };
}
/** 로드된 캔들에서 실제 Rule 평가 구간(마지막 evaluationBarCount 봉) */
export function resolveEvaluationBarSlice(
bars: OHLCVBar[],
evaluationBarCount: number,
): { evalBars: OHLCVBar[]; firstEvalIndex: number } {
const count = Math.min(Math.max(evaluationBarCount, 1), bars.length);
const firstEvalIndex = Math.max(0, bars.length - count);
return { evalBars: bars.slice(firstEvalIndex), firstEvalIndex };
}
/**
* 이전 200봉 평가 구간 — 현재 평가 창 직전 봉을 새 창의 종료 시점(to)으로 사용.
* 워밍업은 fetchBacktestCandleBundle 이 별도로 앞쪽에 추가 요청한다.
*/
export function computePreviousWindowEndTime(
bars: OHLCVBar[],
evaluationBarCount: number,
barSec: number,
): number | null {
if (bars.length === 0) return null;
const { firstEvalIndex } = resolveEvaluationBarSlice(bars, evaluationBarCount);
if (firstEvalIndex > 0) {
return bars[firstEvalIndex - 1].time;
}
return bars[0].time - Math.max(barSec, 1);
}
/**
* 이후 200봉 평가 구간 — 현재 창에서 evaluationBarCount 만큼 앞으로 이동.
* 'latest' = 최신 데이터 창으로 복귀, null = 이미 최신(이동 불가).
*/
export function computeNextWindowEndTime(
bars: OHLCVBar[],
evaluationBarCount: number,
latestBarTimeSec: number | null,
barSec: number,
): 'latest' | number | null {
if (bars.length === 0) return null;
const lastBarTime = bars[bars.length - 1].time;
const tolerance = Math.max(barSec / 2, 1);
if (latestBarTimeSec != null && lastBarTime >= latestBarTimeSec - tolerance) {
return null;
}
const nextEndTime = lastBarTime + evaluationBarCount * Math.max(barSec, 1);
if (latestBarTimeSec != null && nextEndTime >= latestBarTimeSec - tolerance) {
return 'latest';
}
return nextEndTime;
}
/** 이미 로드된 캔들(차트 rawBars)에 대한 평가·워밍업 구간 계산 */
export function resolveEvaluationFromLoadedBars(
barCount: number,
@@ -285,6 +285,60 @@ const ALLOCATION_LABELS: Record<BuyAllocationMode, string> = {
split: '분할매수 (1차 40% · 2차 30% · 3차 잔여 전액)',
};
export interface StrategyEvaluationToolbarSummary {
buyCount: number;
sellCount: number;
/** 일괄매수 수익률 (소수) */
fullBuyReturnPct: number;
/** 분할매수 수익률 (소수) */
splitBuyReturnPct: number;
}
export function padEvaluationSignalCount(n: number): string {
return String(Math.max(0, Math.floor(n))).padStart(2, '0');
}
export function formatEvaluationReturnPct(v: number): string {
if (!Number.isFinite(v) || v === 0) return '0.00%';
return `${v >= 0 ? '+' : ''}${(v * 100).toFixed(2)}%`;
}
/** 차트 상단 툴바 — 필터·일괄/분할매수 시뮬레이션 기준 요약 */
export function buildStrategyEvaluationToolbarSummary(
signals: BacktestSignal[],
opts?: {
initialCapital?: number;
symbol?: string;
},
): StrategyEvaluationToolbarSummary {
const filtered = filterSignalsForEvaluationReport(signals);
let buyCount = 0;
let sellCount = 0;
for (const s of filtered) {
if (BUY_TYPES.has(s.type)) buyCount += 1;
else if (SELL_TYPES.has(s.type)) sellCount += 1;
}
const empty = { buyCount: 0, sellCount: 0, fullBuyReturnPct: 0, splitBuyReturnPct: 0 };
if (filtered.length === 0) return empty;
const capital = opts?.initialCapital ?? 10_000_000;
const symbol = opts?.symbol ?? '';
const stillHolding = isStillHolding(filtered);
const fullSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'full');
const splitSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'split');
const fullAnalysis = buildAnalysisFromSimulation(fullSim, capital, 0, stillHolding);
const splitAnalysis = buildAnalysisFromSimulation(splitSim, capital, 0, stillHolding);
return {
buyCount,
sellCount,
fullBuyReturnPct: fullAnalysis.totalReturnPct,
splitBuyReturnPct: splitAnalysis.totalReturnPct,
};
}
/** 전략평가 화면 — 필터·시뮬레이션 기반 분석 레포트 모델 */
export function buildStrategyEvaluationReportModel(opts: {
signals: BacktestSignal[];