전략평가 화면 수익률 요약표시
This commit is contained in:
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user