From 578cba3eb8197ca5bb5f0c10f81fd650f8b5e044 Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 12 Jun 2026 00:07:48 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=ED=85=8C=EC=8A=A4=ED=8C=85=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=B4=89=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/StrategyEditorPage.tsx | 20 ++++----- .../src/utils/strategyToChartIndicators.ts | 42 ++++++++++++------- .../src/utils/virtualStrategyConditions.ts | 10 +++-- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index 0bbe843..f9d6a8b 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -1173,10 +1173,10 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop setQbError(null); try { const selectedStrategy = strategies.find(s => s.id === selectedId); - const strategyTimeframe = resolveStrategyPrimaryTimeframe({ - buyCondition: buyCondition, - sellCondition: sellCondition, - } as ApiStrategyDto); + const strategyTimeframe = resolveStrategyPrimaryTimeframe( + selectedStrategy, + { buy: buyEditorState, sell: sellEditorState }, + ); const execTimeframe = resolveBacktestExecTimeframe(qbTimeframe, strategyTimeframe); const [settings, indicatorParams, barsRes] = await Promise.all([ loadBacktestSettings(), @@ -1213,14 +1213,14 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop } finally { setQbLoading(false); } - }, [selectedId, strategies, qbMarket, qbTimeframe, buyCondition, sellCondition, onNavigateToBacktest]); + }, [selectedId, strategies, qbMarket, qbTimeframe, buyEditorState, sellEditorState, onNavigateToBacktest]); const qbStrategyTimeframe = useMemo( - () => resolveStrategyPrimaryTimeframe({ - buyCondition: buyCondition, - sellCondition: sellCondition, - } as ApiStrategyDto), - [buyCondition, sellCondition], + () => resolveStrategyPrimaryTimeframe( + selectedId ? strategies.find(s => s.id === selectedId) : undefined, + { buy: buyEditorState, sell: sellEditorState }, + ), + [selectedId, strategies, buyEditorState, sellEditorState], ); const qbTimeframeOptions = useMemo( () => buildQuickRunTimeframeSelectOptions(qbStrategyTimeframe), diff --git a/frontend/src/utils/strategyToChartIndicators.ts b/frontend/src/utils/strategyToChartIndicators.ts index f10cf03..0b94667 100644 --- a/frontend/src/utils/strategyToChartIndicators.ts +++ b/frontend/src/utils/strategyToChartIndicators.ts @@ -17,7 +17,8 @@ import { mergeGlobalDefaultsOntoChartIndicators } from './indicatorMainConfig'; import { buildMainIndicatorConfig } from './indicatorMainConfig'; import { normalizeSmaConfig } from './smaConfig'; import { normalizeIchimokuConfig } from './ichimokuConfig'; -import { extractVirtualConditions } from './virtualStrategyConditions'; +import type { EditorConditionState } from './strategyConditionSerde'; +import { collectUiEvaluationTimeframes } from './strategyTimeframeSync'; const DSL_TO_REGISTRY: Record = { RSI: 'RSI', @@ -93,22 +94,33 @@ export function candleTypeToTimeframe(candleType: string): Timeframe { return '1m'; } -export function resolveStrategyTimeframes(strategy: StrategyDto | undefined): Timeframe[] { - if (!strategy) return ['1m']; - const conditions = extractVirtualConditions(strategy); - const set = new Set(); - for (const row of conditions) { - set.add(candleTypeToTimeframe(row.timeframe)); - } - if (set.size === 0) set.add('1m'); - return [...set].sort((a, b) => { - const order: Timeframe[] = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1D', '1W', '1M']; - return order.indexOf(a) - order.indexOf(b); - }); +const TIMEFRAME_SORT_ORDER: Timeframe[] = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1D', '1W', '1M']; + +function sortStrategyTimeframes(tfs: Timeframe[]): Timeframe[] { + return [...tfs].sort( + (a, b) => TIMEFRAME_SORT_ORDER.indexOf(a) - TIMEFRAME_SORT_ORDER.indexOf(b), + ); } -export function resolveStrategyPrimaryTimeframe(strategy: StrategyDto | undefined): Timeframe { - const tfs = resolveStrategyTimeframes(strategy); +/** 편집기 START·flowLayout·저장 DSL에서 전략 평가 시간봉 수집 */ +export function resolveStrategyTimeframes( + strategy: StrategyDto | undefined, + editorState?: { buy: EditorConditionState; sell: EditorConditionState }, +): Timeframe[] { + const candleTypes = collectUiEvaluationTimeframes(strategy?.id ?? 0, strategy ?? null, editorState); + if (candleTypes.length === 0) return ['1m']; + const set = new Set(); + for (const ct of candleTypes) { + set.add(candleTypeToTimeframe(ct)); + } + return sortStrategyTimeframes([...set]); +} + +export function resolveStrategyPrimaryTimeframe( + strategy: StrategyDto | undefined, + editorState?: { buy: EditorConditionState; sell: EditorConditionState }, +): Timeframe { + const tfs = resolveStrategyTimeframes(strategy, editorState); return tfs[0] ?? '1m'; } diff --git a/frontend/src/utils/virtualStrategyConditions.ts b/frontend/src/utils/virtualStrategyConditions.ts index 19b83f2..963a647 100644 --- a/frontend/src/utils/virtualStrategyConditions.ts +++ b/frontend/src/utils/virtualStrategyConditions.ts @@ -7,6 +7,7 @@ import type { StrategyDto } from './backendApi'; import { formatIndicatorDisplayLabel } from './indicatorRegistry'; import { coerceFiniteNumber, safeToFixed } from './safeFormat'; import { asLogicNode } from './strategyHydrate'; +import { normalizeStartCandleType } from './strategyStartNodes'; export { coerceFiniteNumber } from './safeFormat'; @@ -37,7 +38,9 @@ function walk( if (!node) return; if (node.type === 'TIMEFRAME') { - const tf = node.candleType ?? timeframe; + const tf = normalizeStartCandleType( + node.candleTypes?.[0] ?? node.candleType ?? timeframe, + ); node.children?.forEach(c => walk(c, tf, side, out, seen)); return; } @@ -51,7 +54,8 @@ function walk( // StrategyDslToTa4jAdapter 와 동일: type 미설정(구버전 DSL) 시 CONDITION 으로 처리 if ((!node.type || node.type === 'CONDITION') && node.condition) { const c = node.condition; - const key = `${side}:${timeframe}:${c.indicatorType}:${c.conditionType}:${c.targetValue ?? ''}:${c.leftField ?? ''}`; + const rowTf = normalizeStartCandleType(c.leftCandleType ?? c.rightCandleType ?? timeframe); + const key = `${side}:${rowTf}:${c.indicatorType}:${c.conditionType}:${c.targetValue ?? ''}:${c.leftField ?? ''}`; if (seen.has(key)) return; seen.add(key); @@ -66,7 +70,7 @@ function walk( conditionType: c.conditionType, conditionLabel: condLabel, targetValue: target, - timeframe, + timeframe: rowTf, side, plotKey, });