전략평가 로직 수정

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(() => {
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,
});
@@ -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 {
+37 -4
View File
@@ -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,