fix: 백테스트 시간봉·캔들 수 불일치로 시그널 누락 문제 해결

- 전략 DSL 시간봉(5m)과 실행 시간봉(3m) 불일치 시 자동 동기화
- 업비트 캔들 200봉 제한 → 페이지네이션으로 800봉까지 조회
- 백테스트 차트는 실행 타임프레임 기준으로 표시 (시그널·지표 정렬)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-11 18:16:53 +09:00
parent 44c46eee1b
commit b34bdda073
4 changed files with 70 additions and 12 deletions
+21 -6
View File
@@ -5,7 +5,9 @@ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { ReactFlowProvider } from '@xyflow/react';
import DraggableModalFrame from './DraggableModalFrame';
import type { Theme } from '../types/index';
import { hasRegisteredUser, loadStrategies, loadStrategy, saveStrategy, deleteStrategy, type StrategyDto as ApiStrategyDto } from '../utils/backendApi';
import { hasRegisteredUser, loadStrategies, loadStrategy, saveStrategy, deleteStrategy, runBacktest, loadBacktestSettings, loadIndicatorSettings, API_BASE, type StrategyDto as ApiStrategyDto } from '../utils/backendApi';
import { timeframeToCandleType } from '../utils/chartCandleType';
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
import { syncStrategyTimeframesFromLayoutIfNeeded } from '../utils/strategyTimeframeSync';
import type { LogicNode } from '../utils/strategyTypes';
import {
@@ -86,8 +88,6 @@ import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/useP
import StrategyDescriptionModal from './strategyEditor/StrategyDescriptionModal';
import ReactDOM from 'react-dom';
import { MarketSearchPanel } from './MarketSearchPanel';
import { runBacktest, loadBacktestSettings, loadIndicatorSettings, API_BASE } from '../utils/backendApi';
import { timeframeToCandleType } from '../utils/chartCandleType';
import { getKoreanName } from '../utils/marketNameCache';
import type { Timeframe } from '../types';
import '../styles/strategyEditor.css';
@@ -207,6 +207,17 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
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);
@@ -981,12 +992,16 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
setQbError(null);
try {
const selectedStrategy = strategies.find(s => s.id === selectedId);
const execTimeframe = resolveStrategyPrimaryTimeframe({
buyCondition: buyCondition,
sellCondition: sellCondition,
} as ApiStrategyDto);
const [settings, indicatorParams, barsRes] = await Promise.all([
loadBacktestSettings(),
loadIndicatorSettings(),
fetch(`${API_BASE}/candles/history?${new URLSearchParams({
market: qbMarket,
type: timeframeToCandleType(qbTimeframe),
type: timeframeToCandleType(execTimeframe),
count: '800',
})}`),
]);
@@ -1000,7 +1015,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
const result = await runBacktest({
strategyId: selectedId,
bars,
timeframe: qbTimeframe,
timeframe: execTimeframe,
settings,
indicatorParams,
symbol: qbMarket,
@@ -1016,7 +1031,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
} finally {
setQbLoading(false);
}
}, [selectedId, strategies, qbMarket, qbTimeframe, onNavigateToBacktest]);
}, [selectedId, strategies, qbMarket, buyCondition, sellCondition, onNavigateToBacktest]);
const operators = [
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },