수익률 평기 기준 통일
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
loadStrategies,
|
||||
loadStrategy,
|
||||
loadBacktestSettings,
|
||||
runBacktest,
|
||||
type BacktestSettingsDto,
|
||||
type BacktestSignal,
|
||||
type StrategyDto,
|
||||
} from '../utils/backendApi';
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
formatEvaluationReturnPct,
|
||||
padEvaluationSignalCount,
|
||||
} from '../utils/strategyEvaluationReport';
|
||||
import { resolveEvaluationCommissionRate } from '../utils/strategyEvaluationSignals';
|
||||
import { BACKTEST_DISPLAY_BAR_COUNT, resolveEvaluationBarSlice } from '../utils/backtestWarmup';
|
||||
import type { StrategyEvaluationWindowMeta } from './strategyEvaluation/StrategyEvaluationChart';
|
||||
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
||||
@@ -152,6 +153,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
const [backtestSignals, setBacktestSignals] = useState<BacktestSignal[]>([]);
|
||||
const [signalScanRunning, setSignalScanRunning] = useState(false);
|
||||
const [initialCapital, setInitialCapital] = useState(10_000_000);
|
||||
const [backtestSettings, setBacktestSettings] = useState<BacktestSettingsDto | null>(null);
|
||||
const [editorModalOpen, setEditorModalOpen] = useState(false);
|
||||
const [reportOpen, setReportOpen] = useState(false);
|
||||
const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null);
|
||||
@@ -440,17 +442,24 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
void loadBacktestSettings().then(s => {
|
||||
setBacktestSettings(s);
|
||||
if (s?.initialCapital) setInitialCapital(s.initialCapital);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const evaluationCommissionRate = useMemo(
|
||||
() => (backtestSettings ? resolveEvaluationCommissionRate(backtestSettings) : 0.0015),
|
||||
[backtestSettings],
|
||||
);
|
||||
|
||||
const analysisSummary = useMemo(() => {
|
||||
if (!selectedStrategy) return null;
|
||||
return buildStrategyEvaluationToolbarSummary(backtestSignals, {
|
||||
initialCapital,
|
||||
symbol: market.replace(/^KRW-/, ''),
|
||||
commissionRate: evaluationCommissionRate,
|
||||
});
|
||||
}, [selectedStrategy, backtestSignals, initialCapital, market]);
|
||||
}, [selectedStrategy, backtestSignals, initialCapital, market, evaluationCommissionRate]);
|
||||
|
||||
const bulkEval = useStrategyBulkEvaluation({
|
||||
strategies,
|
||||
@@ -622,10 +631,14 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
if (!selectedStrategyId || bars.length < 10) return null;
|
||||
const lastBarTime = bars[bars.length - 1]?.time ?? 0;
|
||||
const windowKey = evalWindowMeta.windowEndTimeSec ?? 'latest';
|
||||
return `${selectedStrategyId}|${market}|${chartTimeframe}|${paramsRevision}|${bars.length}|${lastBarTime}|${windowKey}`;
|
||||
}, [selectedStrategyId, market, chartTimeframe, paramsRevision, bars, evalWindowMeta.windowEndTimeSec]);
|
||||
const lastSig = backtestSignals[backtestSignals.length - 1];
|
||||
const sigKey = backtestSignals.length > 0
|
||||
? `${backtestSignals.length}:${lastSig?.time}:${lastSig?.price}`
|
||||
: '0';
|
||||
return `${selectedStrategyId}|${market}|${chartTimeframe}|${paramsRevision}|${bars.length}|${lastBarTime}|${windowKey}|${sigKey}`;
|
||||
}, [selectedStrategyId, market, chartTimeframe, paramsRevision, bars, evalWindowMeta.windowEndTimeSec, backtestSignals]);
|
||||
|
||||
const reportDisabled = !selectedStrategyId || !selectedStrategy || bars.length < 10 || chartLoading;
|
||||
const reportDisabled = !selectedStrategyId || !selectedStrategy || bars.length < 10 || chartLoading || signalScanRunning;
|
||||
|
||||
const aiVerifyDisabled = reportDisabled
|
||||
|| evalLoading
|
||||
@@ -724,36 +737,10 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
const gen = ++reportGenRef.current;
|
||||
setReportLoading(true);
|
||||
try {
|
||||
const [btSettings] = await Promise.all([loadBacktestSettings()]);
|
||||
const indicatorParams = buildEvalParamsFromStrategy(selectedStrategy, getEvalParams);
|
||||
const evaluationBarCount = evalWindowMeta.evaluationBarCount;
|
||||
const { evalBars } = resolveEvaluationBarSlice(bars, evaluationBarCount);
|
||||
const res = await runBacktest({
|
||||
strategyId: selectedStrategyId,
|
||||
bars: bars.map(b => ({
|
||||
time: b.time,
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
})),
|
||||
timeframe: chartTimeframe,
|
||||
symbol: market,
|
||||
strategyName: selectedStrategy.name,
|
||||
settings: { ...btSettings, positionMode: 'SIGNAL_ONLY' },
|
||||
indicatorParams,
|
||||
evaluationBarCount,
|
||||
});
|
||||
if (gen !== reportGenRef.current) return;
|
||||
if (!res) {
|
||||
window.alert('분석 레포트 생성에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const initialCapital = btSettings.initialCapital ?? res.analysis?.initialCapital ?? 10_000_000;
|
||||
const model = buildStrategyEvaluationReportModel({
|
||||
signals: res.signals,
|
||||
signals: backtestSignals,
|
||||
initialCapital,
|
||||
strategyName: selectedStrategy.name ?? strategyLabel,
|
||||
symbol: market.replace(/^KRW-/, ''),
|
||||
@@ -761,7 +748,9 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
barCount: evaluationBarCount,
|
||||
firstBarClose: evalBars[0]?.close,
|
||||
lastBarClose: evalBars[evalBars.length - 1]?.close,
|
||||
commissionRate: evaluationCommissionRate,
|
||||
});
|
||||
if (gen !== reportGenRef.current) return;
|
||||
if (!model) {
|
||||
window.alert('분석 데이터를 생성할 수 없습니다.');
|
||||
return;
|
||||
@@ -782,12 +771,14 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
selectedStrategy,
|
||||
reportCacheKey,
|
||||
reportModel,
|
||||
getEvalParams,
|
||||
bars,
|
||||
chartTimeframe,
|
||||
market,
|
||||
backtestSignals,
|
||||
initialCapital,
|
||||
strategyLabel,
|
||||
market,
|
||||
chartTimeframe,
|
||||
bars,
|
||||
evalWindowMeta.evaluationBarCount,
|
||||
evaluationCommissionRate,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getIndicatorDef } from '../../utils/indicatorRegistry';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
||||
import { getKoreanName } from '../../utils/marketNameCache';
|
||||
import type { BacktestSignal, StrategyDto } from '../../utils/backendApi';
|
||||
import { fetchLiveConditionScanSignals } from '../../utils/backendApi';
|
||||
import { fetchStrategyEvaluationSignals } from '../../utils/strategyEvaluationSignals';
|
||||
import type { MarketInfo, TickerData } from '../../hooks/useMarketTicker';
|
||||
import {
|
||||
BACKTEST_DISPLAY_BAR_COUNT,
|
||||
@@ -394,23 +394,17 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
setBacktestRunning(true);
|
||||
setBacktestError(null);
|
||||
try {
|
||||
const res = await fetchLiveConditionScanSignals(
|
||||
const { signals: nextSignals } = await fetchStrategyEvaluationSignals({
|
||||
strategyId: strategy.id,
|
||||
strategy,
|
||||
market,
|
||||
strategy.id,
|
||||
barData.map(b => ({
|
||||
time: b.time,
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
})),
|
||||
timeframe,
|
||||
indicatorParams,
|
||||
evaluationBarCountRef.current,
|
||||
);
|
||||
bars: barData,
|
||||
evaluationBarCount: evaluationBarCountRef.current,
|
||||
getParams,
|
||||
});
|
||||
if (gen !== backtestGenRef.current) return;
|
||||
setSignals(res);
|
||||
setSignals(nextSignals);
|
||||
applyMarkers();
|
||||
} catch (e) {
|
||||
if (gen !== backtestGenRef.current) return;
|
||||
@@ -420,7 +414,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
} finally {
|
||||
if (gen === backtestGenRef.current) setBacktestRunning(false);
|
||||
}
|
||||
}, [strategy, market, timeframe, indicatorParams, applyMarkers]);
|
||||
}, [strategy, market, timeframe, getParams, applyMarkers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategy?.id || loading || bars.length < 10) {
|
||||
|
||||
Reference in New Issue
Block a user