fix: 백테스트 시간봉·캔들 수 불일치로 시그널 누락 문제 해결
- 전략 DSL 시간봉(5m)과 실행 시간봉(3m) 불일치 시 자동 동기화 - 업비트 캔들 200봉 제한 → 페이지네이션으로 800봉까지 조회 - 백테스트 차트는 실행 타임프레임 기준으로 표시 (시그널·지표 정렬) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -51,6 +51,7 @@ import { getKoreanName } from '../utils/marketNameCache';
|
||||
import { repairUtf8Mojibake } from '../utils/textEncoding';
|
||||
import { enrichStrategyNameMap, strategyNamesFromList } from '../utils/strategyNameResolver';
|
||||
import { markStrategyIdsMissingUnlessValid } from '../utils/strategyMissingCache';
|
||||
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
|
||||
|
||||
const LEFT_KEY = 'btd-left-width';
|
||||
const RIGHT_KEY = 'btd-right-width';
|
||||
@@ -213,6 +214,15 @@ 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);
|
||||
@@ -234,12 +244,14 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
||||
setRunLoading(true);
|
||||
setRunError(null);
|
||||
try {
|
||||
const strategy = strategies.find(s => s.id === runStrategyId);
|
||||
const execTimeframe = strategy ? resolveStrategyPrimaryTimeframe(strategy) : runTimeframe;
|
||||
const [settings, indicatorParams, barsRes] = await Promise.all([
|
||||
loadBacktestSettings(),
|
||||
loadIndicatorSettings(),
|
||||
fetch(`${API_BASE}/candles/history?${new URLSearchParams({
|
||||
market: runMarket,
|
||||
type: timeframeToCandleType(runTimeframe),
|
||||
type: timeframeToCandleType(execTimeframe),
|
||||
count: '800',
|
||||
})}`),
|
||||
]);
|
||||
@@ -250,11 +262,10 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
||||
if (bars.length < 5) {
|
||||
throw new Error('캔들 데이터가 부족합니다. 잠시 후 다시 시도하거나 타임프레임을 변경해 주세요.');
|
||||
}
|
||||
const strategy = strategies.find(s => s.id === runStrategyId);
|
||||
const result = await runBacktest({
|
||||
strategyId: runStrategyId,
|
||||
bars,
|
||||
timeframe: runTimeframe,
|
||||
timeframe: execTimeframe,
|
||||
settings,
|
||||
indicatorParams,
|
||||
symbol: runMarket,
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -145,7 +145,8 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
const market = symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
|
||||
|
||||
const chartTimeframe = useMemo((): Timeframe => {
|
||||
const tf = strategy ? resolveStrategyPrimaryTimeframe(strategy) : timeframeRaw;
|
||||
// 백테스트 결과: 실행 타임프레임 우선 (전략 DSL TF와 실행 TF 불일치 시 차트·시그널 어긋남 방지)
|
||||
const tf = timeframeRaw || (strategy ? resolveStrategyPrimaryTimeframe(strategy) : '1m');
|
||||
return normalizeChartTimeframe(tf) as Timeframe;
|
||||
}, [strategy, timeframeRaw]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user