전략평가 로직 수정
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user