전략평가 로직 수정

This commit is contained in:
Macbook
2026-06-20 23:41:51 +09:00
parent 8bf157137a
commit fa597e007d
3 changed files with 57 additions and 5 deletions
@@ -460,12 +460,24 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
const analysisSummary = useMemo(() => { const analysisSummary = useMemo(() => {
if (!selectedStrategy) return null; if (!selectedStrategy) return null;
const { evalBars } = resolveEvaluationBarSlice(bars, evalWindowMeta.evaluationBarCount);
const lastEvalBar = evalBars[evalBars.length - 1];
return buildStrategyEvaluationToolbarSummary(backtestSignals, { return buildStrategyEvaluationToolbarSummary(backtestSignals, {
initialCapital, initialCapital,
symbol: market.replace(/^KRW-/, ''), symbol: market.replace(/^KRW-/, ''),
commissionRate: evaluationCommissionRate, commissionRate: evaluationCommissionRate,
lastBarClose: lastEvalBar?.close,
lastBarTimeSec: lastEvalBar?.time,
}); });
}, [selectedStrategy, backtestSignals, initialCapital, market, evaluationCommissionRate]); }, [
selectedStrategy,
backtestSignals,
initialCapital,
market,
evaluationCommissionRate,
bars,
evalWindowMeta.evaluationBarCount,
]);
const bulkEval = useStrategyBulkEvaluation({ const bulkEval = useStrategyBulkEvaluation({
strategies, strategies,
@@ -754,6 +766,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
barCount: evaluationBarCount, barCount: evaluationBarCount,
firstBarClose: evalBars[0]?.close, firstBarClose: evalBars[0]?.close,
lastBarClose: evalBars[evalBars.length - 1]?.close, lastBarClose: evalBars[evalBars.length - 1]?.close,
lastBarTimeSec: evalBars[evalBars.length - 1]?.time,
commissionRate: evaluationCommissionRate, commissionRate: evaluationCommissionRate,
tradeExecutionMode, tradeExecutionMode,
}); });
@@ -1,5 +1,6 @@
import type { OHLCVBar } from '../types'; import type { OHLCVBar } from '../types';
import type { StrategyDto } from './backendApi'; import type { StrategyDto } from './backendApi';
import { resolveEvaluationBarSlice } from './backtestWarmup';
import { fetchStrategyEvaluationSignals } from './strategyEvaluationSignals'; import { fetchStrategyEvaluationSignals } from './strategyEvaluationSignals';
import { buildStrategyEvaluationToolbarSummary } from './strategyEvaluationReport'; import { buildStrategyEvaluationToolbarSummary } from './strategyEvaluationReport';
@@ -58,10 +59,15 @@ export async function evaluateStrategyBulkReturns(opts: {
getParams: opts.getParams, getParams: opts.getParams,
}); });
const { evalBars } = resolveEvaluationBarSlice(opts.bars, opts.evaluationBarCount);
const lastEvalBar = evalBars[evalBars.length - 1];
const summary = buildStrategyEvaluationToolbarSummary(signals, { const summary = buildStrategyEvaluationToolbarSummary(signals, {
initialCapital: initialCapital ?? opts.initialCapital, initialCapital: initialCapital ?? opts.initialCapital,
symbol: opts.market.replace(/^KRW-/, ''), symbol: opts.market.replace(/^KRW-/, ''),
commissionRate, commissionRate,
lastBarClose: lastEvalBar?.close,
lastBarTimeSec: lastEvalBar?.time,
}); });
return { return {
+37 -4
View File
@@ -61,6 +61,12 @@ export interface EvaluationSimulationResult {
lastPrice: number; lastPrice: number;
} }
export interface EvaluationSimulationOptions {
/** 평가 구간 종료 시점 미청산 포지션 평가가 (없으면 마지막 시그널 가격) */
markToMarketPrice?: number;
markToMarketTime?: number;
}
/** 필터링된 시그널 기준 포트폴리오 시뮬레이션 (매도는 전량) */ /** 필터링된 시그널 기준 포트폴리오 시뮬레이션 (매도는 전량) */
export function simulateEvaluationPortfolio( export function simulateEvaluationPortfolio(
signals: BacktestSignal[], signals: BacktestSignal[],
@@ -68,6 +74,7 @@ export function simulateEvaluationPortfolio(
symbol: string, symbol: string,
mode: BuyAllocationMode, mode: BuyAllocationMode,
commissionRate = 0, commissionRate = 0,
simOpts?: EvaluationSimulationOptions,
): EvaluationSimulationResult { ): EvaluationSimulationResult {
const sorted = sortSignals(signals); const sorted = sortSignals(signals);
const curve: EquityPoint[] = []; const curve: EquityPoint[] = [];
@@ -156,7 +163,11 @@ export function simulateEvaluationPortfolio(
} }
if (qty > 0) { 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) { } else if (curve.length === 1) {
const last = sorted[sorted.length - 1]; const last = sorted[sorted.length - 1];
markEquity(last.time, last.price); 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( export function buildStrategyEvaluationToolbarSummary(
signals: BacktestSignal[], signals: BacktestSignal[],
opts?: { opts?: {
initialCapital?: number; initialCapital?: number;
symbol?: string; symbol?: string;
commissionRate?: number; commissionRate?: number;
lastBarClose?: number;
lastBarTimeSec?: number;
}, },
): StrategyEvaluationToolbarSummary { ): StrategyEvaluationToolbarSummary {
const filtered = filterSignalsForEvaluationReport(signals); const filtered = filterSignalsForEvaluationReport(signals);
@@ -332,9 +359,10 @@ export function buildStrategyEvaluationToolbarSummary(
const symbol = opts?.symbol ?? ''; const symbol = opts?.symbol ?? '';
const commissionRate = opts?.commissionRate ?? 0; const commissionRate = opts?.commissionRate ?? 0;
const stillHolding = isStillHolding(filtered); const stillHolding = isStillHolding(filtered);
const mtmOpts = resolveMarkToMarketOpts(stillHolding, opts);
const fullSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'full', commissionRate); const fullSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'full', commissionRate, mtmOpts);
const splitSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'split', commissionRate); const splitSim = simulateEvaluationPortfolio(filtered, capital, symbol, 'split', commissionRate, mtmOpts);
const fullAnalysis = buildAnalysisFromSimulation(fullSim, capital, 0, stillHolding); const fullAnalysis = buildAnalysisFromSimulation(fullSim, capital, 0, stillHolding);
const splitAnalysis = buildAnalysisFromSimulation(splitSim, capital, 0, stillHolding); const splitAnalysis = buildAnalysisFromSimulation(splitSim, capital, 0, stillHolding);
@@ -357,6 +385,7 @@ export function buildStrategyEvaluationReportModel(opts: {
createdAt?: string; createdAt?: string;
firstBarClose?: number; firstBarClose?: number;
lastBarClose?: number; lastBarClose?: number;
lastBarTimeSec?: number;
commissionRate?: number; commissionRate?: number;
tradeExecutionMode?: TradeExecutionMode; tradeExecutionMode?: TradeExecutionMode;
}): BacktestAnalysisReportModel | null { }): BacktestAnalysisReportModel | null {
@@ -367,11 +396,15 @@ export function buildStrategyEvaluationReportModel(opts: {
const firstClose = opts.firstBarClose ?? filtered[0]?.price ?? 0; const firstClose = opts.firstBarClose ?? filtered[0]?.price ?? 0;
const lastClose = opts.lastBarClose ?? filtered[filtered.length - 1]?.price ?? 0; const lastClose = opts.lastBarClose ?? filtered[filtered.length - 1]?.price ?? 0;
const buyAndHoldReturnPct = firstClose > 0 ? (lastClose - firstClose) / firstClose : 0; const buyAndHoldReturnPct = firstClose > 0 ? (lastClose - firstClose) / firstClose : 0;
const mtmOpts = resolveMarkToMarketOpts(stillHolding, {
lastBarClose: lastClose,
lastBarTimeSec: opts.lastBarTimeSec,
});
const modes: BuyAllocationMode[] = ['full', 'split']; const modes: BuyAllocationMode[] = ['full', 'split'];
const evaluationAllocations: EvaluationAllocationReport[] = modes.map(mode => { const evaluationAllocations: EvaluationAllocationReport[] = modes.map(mode => {
const sim = simulateEvaluationPortfolio( const sim = simulateEvaluationPortfolio(
filtered, opts.initialCapital, opts.symbol, mode, commissionRate, filtered, opts.initialCapital, opts.symbol, mode, commissionRate, mtmOpts,
); );
return { return {
mode, mode,