diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index baddb5c..e347d68 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -460,12 +460,24 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { const analysisSummary = useMemo(() => { if (!selectedStrategy) return null; + const { evalBars } = resolveEvaluationBarSlice(bars, evalWindowMeta.evaluationBarCount); + const lastEvalBar = evalBars[evalBars.length - 1]; return buildStrategyEvaluationToolbarSummary(backtestSignals, { initialCapital, symbol: market.replace(/^KRW-/, ''), commissionRate: evaluationCommissionRate, + lastBarClose: lastEvalBar?.close, + lastBarTimeSec: lastEvalBar?.time, }); - }, [selectedStrategy, backtestSignals, initialCapital, market, evaluationCommissionRate]); + }, [ + selectedStrategy, + backtestSignals, + initialCapital, + market, + evaluationCommissionRate, + bars, + evalWindowMeta.evaluationBarCount, + ]); const bulkEval = useStrategyBulkEvaluation({ strategies, @@ -754,6 +766,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { barCount: evaluationBarCount, firstBarClose: evalBars[0]?.close, lastBarClose: evalBars[evalBars.length - 1]?.close, + lastBarTimeSec: evalBars[evalBars.length - 1]?.time, commissionRate: evaluationCommissionRate, tradeExecutionMode, }); diff --git a/frontend/src/utils/strategyBulkEvaluation.ts b/frontend/src/utils/strategyBulkEvaluation.ts index 175f626..953a356 100644 --- a/frontend/src/utils/strategyBulkEvaluation.ts +++ b/frontend/src/utils/strategyBulkEvaluation.ts @@ -1,5 +1,6 @@ import type { OHLCVBar } from '../types'; import type { StrategyDto } from './backendApi'; +import { resolveEvaluationBarSlice } from './backtestWarmup'; import { fetchStrategyEvaluationSignals } from './strategyEvaluationSignals'; import { buildStrategyEvaluationToolbarSummary } from './strategyEvaluationReport'; @@ -58,10 +59,15 @@ export async function evaluateStrategyBulkReturns(opts: { getParams: opts.getParams, }); + const { evalBars } = resolveEvaluationBarSlice(opts.bars, opts.evaluationBarCount); + const lastEvalBar = evalBars[evalBars.length - 1]; + const summary = buildStrategyEvaluationToolbarSummary(signals, { initialCapital: initialCapital ?? opts.initialCapital, symbol: opts.market.replace(/^KRW-/, ''), commissionRate, + lastBarClose: lastEvalBar?.close, + lastBarTimeSec: lastEvalBar?.time, }); return { diff --git a/frontend/src/utils/strategyEvaluationReport.ts b/frontend/src/utils/strategyEvaluationReport.ts index ecb0ec3..e72c0db 100644 --- a/frontend/src/utils/strategyEvaluationReport.ts +++ b/frontend/src/utils/strategyEvaluationReport.ts @@ -61,6 +61,12 @@ export interface EvaluationSimulationResult { lastPrice: number; } +export interface EvaluationSimulationOptions { + /** 평가 구간 종료 시점 미청산 포지션 평가가 (없으면 마지막 시그널 가격) */ + markToMarketPrice?: number; + markToMarketTime?: number; +} + /** 필터링된 시그널 기준 포트폴리오 시뮬레이션 (매도는 전량) */ export function simulateEvaluationPortfolio( signals: BacktestSignal[], @@ -68,6 +74,7 @@ export function simulateEvaluationPortfolio( symbol: string, mode: BuyAllocationMode, commissionRate = 0, + simOpts?: EvaluationSimulationOptions, ): EvaluationSimulationResult { const sorted = sortSignals(signals); const curve: EquityPoint[] = []; @@ -156,7 +163,11 @@ export function simulateEvaluationPortfolio( } if (qty > 0) { - markEquity(sorted[sorted.length - 1].time, lastPrice); + const mtmPrice = simOpts?.markToMarketPrice != null && simOpts.markToMarketPrice > 0 + ? simOpts.markToMarketPrice + : lastPrice; + const mtmTime = simOpts?.markToMarketTime ?? sorted[sorted.length - 1].time; + markEquity(mtmTime, mtmPrice); } else if (curve.length === 1) { const last = sorted[sorted.length - 1]; markEquity(last.time, last.price); @@ -309,12 +320,28 @@ export function formatEvaluationReturnPct(v: number): string { } /** 차트 상단 툴바 — 필터·일괄/분할매수 시뮬레이션 기준 요약 */ +function resolveMarkToMarketOpts( + stillHolding: boolean, + opts?: { + lastBarClose?: number; + lastBarTimeSec?: number; + }, +): EvaluationSimulationOptions | undefined { + if (!stillHolding || opts?.lastBarClose == null || opts.lastBarClose <= 0) return undefined; + return { + markToMarketPrice: opts.lastBarClose, + markToMarketTime: opts.lastBarTimeSec, + }; +} + export function buildStrategyEvaluationToolbarSummary( signals: BacktestSignal[], opts?: { initialCapital?: number; symbol?: string; commissionRate?: number; + lastBarClose?: number; + lastBarTimeSec?: number; }, ): StrategyEvaluationToolbarSummary { const filtered = filterSignalsForEvaluationReport(signals); @@ -332,9 +359,10 @@ export function buildStrategyEvaluationToolbarSummary( const symbol = opts?.symbol ?? ''; const commissionRate = opts?.commissionRate ?? 0; const stillHolding = isStillHolding(filtered); + const mtmOpts = resolveMarkToMarketOpts(stillHolding, opts); - const fullSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'full', commissionRate); - const splitSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'split', commissionRate); + const fullSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'full', commissionRate, mtmOpts); + const splitSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'split', commissionRate, mtmOpts); const fullAnalysis = buildAnalysisFromSimulation(fullSim, capital, 0, stillHolding); const splitAnalysis = buildAnalysisFromSimulation(splitSim, capital, 0, stillHolding); @@ -357,6 +385,7 @@ export function buildStrategyEvaluationReportModel(opts: { createdAt?: string; firstBarClose?: number; lastBarClose?: number; + lastBarTimeSec?: number; commissionRate?: number; tradeExecutionMode?: TradeExecutionMode; }): BacktestAnalysisReportModel | null { @@ -367,11 +396,15 @@ export function buildStrategyEvaluationReportModel(opts: { const firstClose = opts.firstBarClose ?? filtered[0]?.price ?? 0; const lastClose = opts.lastBarClose ?? filtered[filtered.length - 1]?.price ?? 0; const buyAndHoldReturnPct = firstClose > 0 ? (lastClose - firstClose) / firstClose : 0; + const mtmOpts = resolveMarkToMarketOpts(stillHolding, { + lastBarClose: lastClose, + lastBarTimeSec: opts.lastBarTimeSec, + }); const modes: BuyAllocationMode[] = ['full', 'split']; const evaluationAllocations: EvaluationAllocationReport[] = modes.map(mode => { const sim = simulateEvaluationPortfolio( - filtered, opts.initialCapital, opts.symbol, mode, commissionRate, + filtered, opts.initialCapital, opts.symbol, mode, commissionRate, mtmOpts, ); return { mode,