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