백테스팅 수정

This commit is contained in:
Macbook
2026-06-12 00:00:06 +09:00
parent eb6cf894da
commit d5338274d4
10 changed files with 151 additions and 72 deletions
+40 -32
View File
@@ -28,6 +28,7 @@ import {
formatTimeframeKo,
normalizeEpochSec,
normalizeEpochSecOptional,
resolveBacktestRecordExecTimeframe,
toUpbitMarket,
} from '../utils/backtestUiUtils';
import BacktestExecutionList, { type ExecutionListTab } from './backtest/BacktestExecutionList';
@@ -52,6 +53,13 @@ import { repairUtf8Mojibake } from '../utils/textEncoding';
import { enrichStrategyNameMap, strategyNamesFromList } from '../utils/strategyNameResolver';
import { markStrategyIdsMissingUnlessValid } from '../utils/strategyMissingCache';
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
import {
BACKTEST_STRATEGY_TIMEFRAME,
buildQuickRunTimeframeSelectOptions,
parseBacktestRunTimeframeChoice,
resolveBacktestExecTimeframe,
type BacktestRunTimeframeChoice,
} from '../utils/backtestRunTimeframe';
const LEFT_KEY = 'btd-left-width';
const RIGHT_KEY = 'btd-right-width';
@@ -77,19 +85,6 @@ interface Props {
theme?: Theme;
}
const QUICK_RUN_TIMEFRAMES: { value: Timeframe; label: string }[] = [
{ value: '1m', label: '1분' },
{ value: '3m', label: '3분' },
{ value: '5m', label: '5분' },
{ value: '10m', label: '10분' },
{ value: '15m', label: '15분' },
{ value: '30m', label: '30분' },
{ value: '1h', label: '1시간' },
{ value: '4h', label: '4시간' },
{ value: '1D', label: '일봉' },
{ value: '1W', label: '주봉' },
];
export function BacktestHistoryPage({ theme = 'dark' }: Props) {
const [tab, setTab] = useState<ExecutionListTab>('backtest');
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
@@ -102,7 +97,7 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
// ── 빠른 백테스팅 실행 ──────────────────────────────────────────────────────
const [runMarket, setRunMarket] = useState<string>('KRW-BTC');
const [runTimeframe, setRunTimeframe] = useState<Timeframe>('1D');
const [runTimeframe, setRunTimeframe] = useState<BacktestRunTimeframeChoice>(BACKTEST_STRATEGY_TIMEFRAME);
const [runStrategyId, setRunStrategyId] = useState<number | null>(null);
const [runLoading, setRunLoading] = useState(false);
const [runError, setRunError] = useState<string | null>(null);
@@ -214,15 +209,6 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
useEffect(() => { void fetchAll(); }, [fetchAll]);
// 전략 선택 시 DSL 기준 시간봉으로 자동 동기화 (예: 전략 4=5m 조건인데 3m 실행 방지)
useEffect(() => {
if (!runStrategyId) return;
const strat = strategies.find(s => s.id === runStrategyId);
if (!strat) return;
const tf = resolveStrategyPrimaryTimeframe(strat);
setRunTimeframe(prev => (prev === tf ? prev : tf));
}, [runStrategyId, strategies]);
useEffect(() => {
const onTradesChanged = () => { void refreshLiveTrades(); };
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onTradesChanged);
@@ -245,7 +231,10 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
setRunError(null);
try {
const strategy = strategies.find(s => s.id === runStrategyId);
const execTimeframe = strategy ? resolveStrategyPrimaryTimeframe(strategy) : runTimeframe;
const strategyTimeframe = strategy
? resolveStrategyPrimaryTimeframe(strategy)
: '1m';
const execTimeframe = resolveBacktestExecTimeframe(runTimeframe, strategyTimeframe);
const [settings, indicatorParams, barsRes] = await Promise.all([
loadBacktestSettings(),
loadIndicatorSettings(),
@@ -272,15 +261,34 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
strategyName: strategy?.name,
});
if (!result) throw new Error('백테스팅 실행에 실패했습니다.');
// 결과 목록 갱신 후 새 결과 자동 선택
await fetchAll();
const list = await loadBacktestResults();
setRecords(list);
if (result.resultId) {
const fresh = list.find(r => r.id === result.resultId) ?? list[0] ?? null;
setSelectedBacktest(fresh);
} else {
setSelectedBacktest(list[0] ?? null);
}
setTab('backtest');
} catch (e) {
setRunError(e instanceof Error ? e.message : '실행 중 오류가 발생했습니다.');
} finally {
setRunLoading(false);
}
}, [runMarket, runTimeframe, runStrategyId, strategies, fetchAll]);
}, [runMarket, runTimeframe, runStrategyId, strategies]);
const runStrategy = useMemo(
() => (runStrategyId ? strategies.find(s => s.id === runStrategyId) : undefined),
[runStrategyId, strategies],
);
const runStrategyTimeframe = useMemo(
() => (runStrategy ? resolveStrategyPrimaryTimeframe(runStrategy) : undefined),
[runStrategy],
);
const runTimeframeOptions = useMemo(
() => buildQuickRunTimeframeSelectOptions(runStrategyTimeframe),
[runStrategyTimeframe],
);
const backtestDetail = selectedBacktest ? parseDetail(selectedBacktest) : null;
@@ -312,7 +320,7 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
const fromSec = normalizeEpochSecOptional(selectedBacktest.fromTime);
return {
symbol: selectedBacktest.symbol,
timeframe: selectedBacktest.timeframe,
timeframe: resolveBacktestRecordExecTimeframe(selectedBacktest),
toTimeSec: toSec,
fromTimeSec: fromSec > 0 ? fromSec : undefined,
barCount: selectedBacktest.barCount || 300,
@@ -341,7 +349,7 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
ko: getKoreanName(market),
strategy: selectedBacktest.strategyName || '전략 없음',
metaParts: [
formatTimeframeKo(selectedBacktest.timeframe),
formatTimeframeKo(resolveBacktestRecordExecTimeframe(selectedBacktest)),
formatChartTypeKo('candlestick'),
],
};
@@ -452,11 +460,11 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
<select
className="btd-run-select"
value={runTimeframe}
onChange={e => setRunTimeframe(e.target.value as Timeframe)}
onChange={e => setRunTimeframe(parseBacktestRunTimeframeChoice(e.target.value))}
disabled={runLoading}
title="타임프레임"
title="백테스팅 실행 시간봉"
>
{QUICK_RUN_TIMEFRAMES.map(tf => (
{runTimeframeOptions.map(tf => (
<option key={tf.value} value={tf.value}>{tf.label}</option>
))}
</select>
+26 -29
View File
@@ -105,22 +105,16 @@ import ReactDOM from 'react-dom';
import { MarketSearchPanel } from './MarketSearchPanel';
import { getKoreanName } from '../utils/marketNameCache';
import type { Timeframe } from '../types';
import {
BACKTEST_STRATEGY_TIMEFRAME,
buildQuickRunTimeframeSelectOptions,
parseBacktestRunTimeframeChoice,
resolveBacktestExecTimeframe,
type BacktestRunTimeframeChoice,
} from '../utils/backtestRunTimeframe';
import '../styles/strategyEditor.css';
import '../styles/strategyEditorTheme.css';
const QUICK_RUN_TIMEFRAMES: { value: Timeframe; label: string }[] = [
{ value: '1m', label: '1분' },
{ value: '3m', label: '3분' },
{ value: '5m', label: '5분' },
{ value: '10m', label: '10분' },
{ value: '15m', label: '15분' },
{ value: '30m', label: '30분' },
{ value: '1h', label: '1시간' },
{ value: '4h', label: '4시간' },
{ value: '1D', label: '일봉' },
{ value: '1W', label: '주봉' },
];
const BACKTEST_FOCUS_KEY = 'backtest_focus_id';
const LEFT_PANEL_MIN = 220;
@@ -222,23 +216,13 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
// ── 빠른 백테스팅 실행 ────────────────────────────────────────────────────
const [qbMarket, setQbMarket] = useState('KRW-BTC');
const [qbTimeframe, setQbTimeframe] = useState<Timeframe>('1D');
const [qbTimeframe, setQbTimeframe] = useState<BacktestRunTimeframeChoice>(BACKTEST_STRATEGY_TIMEFRAME);
const [qbLoading, setQbLoading] = useState(false);
const [qbError, setQbError] = useState<string | null>(null);
const [qbMarketOpen, setQbMarketOpen] = useState(false);
const [qbMarketQ, setQbMarketQ] = useState('');
const qbMarketBtnRef = useRef<HTMLButtonElement>(null);
// 전략 선택/조건 변경 시 DSL 기준 시간봉으로 빠른 실행 TF 동기화
useEffect(() => {
if (selectedId == null) return;
const tf = resolveStrategyPrimaryTimeframe({
buyCondition: buyCondition,
sellCondition: sellCondition,
} as ApiStrategyDto);
setQbTimeframe(prev => (prev === tf ? prev : tf));
}, [selectedId, buyCondition, sellCondition]);
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
const leftWidthRef = useRef(leftWidth);
@@ -1189,10 +1173,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
setQbError(null);
try {
const selectedStrategy = strategies.find(s => s.id === selectedId);
const execTimeframe = resolveStrategyPrimaryTimeframe({
const strategyTimeframe = resolveStrategyPrimaryTimeframe({
buyCondition: buyCondition,
sellCondition: sellCondition,
} as ApiStrategyDto);
const execTimeframe = resolveBacktestExecTimeframe(qbTimeframe, strategyTimeframe);
const [settings, indicatorParams, barsRes] = await Promise.all([
loadBacktestSettings(),
loadIndicatorSettings(),
@@ -1228,7 +1213,19 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
} finally {
setQbLoading(false);
}
}, [selectedId, strategies, qbMarket, buyCondition, sellCondition, onNavigateToBacktest]);
}, [selectedId, strategies, qbMarket, qbTimeframe, buyCondition, sellCondition, onNavigateToBacktest]);
const qbStrategyTimeframe = useMemo(
() => resolveStrategyPrimaryTimeframe({
buyCondition: buyCondition,
sellCondition: sellCondition,
} as ApiStrategyDto),
[buyCondition, sellCondition],
);
const qbTimeframeOptions = useMemo(
() => buildQuickRunTimeframeSelectOptions(qbStrategyTimeframe),
[qbStrategyTimeframe],
);
const operators = [
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
@@ -1303,11 +1300,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
<select
className="se-qr-select"
value={qbTimeframe}
onChange={e => setQbTimeframe(e.target.value as Timeframe)}
onChange={e => setQbTimeframe(parseBacktestRunTimeframeChoice(e.target.value))}
disabled={qbLoading}
title="타임프레임"
title="백테스팅 실행 시간봉"
>
{QUICK_RUN_TIMEFRAMES.map(tf => (
{qbTimeframeOptions.map(tf => (
<option key={tf.value} value={tf.value}>{tf.label}</option>
))}
</select>
@@ -35,7 +35,6 @@ import {
import {
buildVirtualTradingChartIndicators,
ensurePaperChartOverlays,
resolveStrategyPrimaryTimeframe,
} from '../../utils/strategyToChartIndicators';
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
@@ -145,10 +144,9 @@ const BacktestAnalysisChart: React.FC<Props> = ({
const market = symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
const chartTimeframe = useMemo((): Timeframe => {
// 백테스트 결과: 실행 타임프레임 우선 (전략 DSL TF와 실행 TF 불일치 시 차트·시그널 어긋남 방지)
const tf = timeframeRaw || (strategy ? resolveStrategyPrimaryTimeframe(strategy) : '1m');
const tf = timeframeRaw?.trim() ? timeframeRaw : '1m';
return normalizeChartTimeframe(tf) as Timeframe;
}, [strategy, timeframeRaw]);
}, [timeframeRaw]);
useEffect(() => {
if (!strategyId || isStrategyMissingOnServer(strategyId)) {
@@ -11,6 +11,7 @@ import {
formatTimeframeKo,
pct,
pctAbs,
resolveBacktestRecordExecTimeframe,
toUpbitMarket,
} from '../../utils/backtestUiUtils';
import { getKoreanName } from '../../utils/marketNameCache';
@@ -88,7 +89,7 @@ export default function BacktestExecutionList({
const ko = getKoreanName(market).toLowerCase();
const code = r.symbol.replace(/^KRW-/i, '').toLowerCase();
const strategy = (r.strategyName ?? '').toLowerCase();
const tf = r.timeframe?.toLowerCase() ?? '';
const tf = resolveBacktestRecordExecTimeframe(r).toLowerCase();
return ko.includes(normalizedQuery)
|| code.includes(normalizedQuery)
|| strategy.includes(normalizedQuery)
@@ -200,7 +201,7 @@ export default function BacktestExecutionList({
<div className="btd-history-card-row">
<div className="btd-history-card-main">
<span className="btd-history-ko">{ko}</span>
<span className="btd-history-strategy">{strategy} · {formatTimeframeKo(r.timeframe)}</span>
<span className="btd-history-strategy">{strategy} · {formatTimeframeKo(resolveBacktestRecordExecTimeframe(r))}</span>
<span className="btd-history-date">{fmtListTimestamp(r.createdAt)}</span>
</div>
<div className="btd-history-card-side">