From 611823047153dd7f9a189b9af4de6d5a30d4f3a1 Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 12 Jun 2026 21:32:19 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=EB=B4=89=20=EC=84=A0=ED=83=9D=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 4 + frontend/src/chart/ChartWorkspaceView.tsx | 32 +++++++- frontend/src/chart/useChartWorkspace.ts | 80 ++++++++++++++++--- frontend/src/components/ChartSlot.tsx | 62 +++++++------- .../src/components/StrategyEditorPage.tsx | 33 +------- frontend/src/components/TradingChart.tsx | 23 +++++- frontend/src/utils/strategyConditionSerde.ts | 18 +---- frontend/src/utils/strategyStartNodes.ts | 9 ++- 8 files changed, 164 insertions(+), 97 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 23f6f50..e3cbaf8 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1276,6 +1276,10 @@ html.theme-blue { background: var(--bg, #131722); pointer-events: none; } +.multi-chart-grid .chart-loading--workspace, +.tv-chart-col .chart-loading--workspace { + z-index: 930; +} .loading-spinner { width: 32px; height: 32px; border: 3px solid var(--bg4); diff --git a/frontend/src/chart/ChartWorkspaceView.tsx b/frontend/src/chart/ChartWorkspaceView.tsx index 2f6bb9f..880a181 100644 --- a/frontend/src/chart/ChartWorkspaceView.tsx +++ b/frontend/src/chart/ChartWorkspaceView.tsx @@ -200,6 +200,9 @@ export interface ChartWorkspaceViewProps { liveQuote: ReturnType; isLoading: boolean; error: string | null; + chartConfigReady: boolean; + chartAreaLoading: boolean; + chartDataLoading: boolean; bulkSettingsIndicators: IndicatorConfig[]; chartIndicatorsForSettings: IndicatorConfig[]; getParams: ReturnType['getParams']; @@ -298,6 +301,8 @@ export interface ChartWorkspaceViewProps { handleSaveCustomOverlaySelection: (slot: ChartCustomOverlaySlotId, selection: ChartCustomOverlaySelection) => void; syncCustomOverlaysToManager: (mgr: ChartManager) => void; handleCandlesReady: () => void; + handleSingleChartPaintReadyChange: (ready: boolean) => void; + handleSlotPaintReadyChange: (slotIndex: number, ready: boolean) => void; applyBacktestMarkersWhenReady: (payload: NonNullable, attempt?: number) => void; btRun: ReturnType['run']; refreshPaperAccount: (forMarket?: string) => Promise; @@ -339,7 +344,7 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) { obWsStatus, spread, orderbookPrevClose, chartTicker, tradeFill, paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled, appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote, - isLoading, error, bulkSettingsIndicators, chartIndicatorsForSettings, + isLoading, error, chartConfigReady, chartAreaLoading, chartDataLoading, bulkSettingsIndicators, chartIndicatorsForSettings, getParams, getVisualConfig, activeSlotIndicators, setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid, setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible, @@ -368,7 +373,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) { handleTogglePriceExtremeLabels, handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange, handleSaveCustomOverlaySelection, syncCustomOverlaysToManager, - handleCandlesReady, applyBacktestMarkersWhenReady, btRun, refreshPaperAccount, + handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange, + applyBacktestMarkersWhenReady, btRun, refreshPaperAccount, handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers, handleTimezoneChange, } = props; @@ -587,8 +593,15 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) { gridTemplateColumns: layoutDef.cols, gridTemplateRows: layoutDef.rows, gridTemplateAreas: layoutDef.areas, + position: 'relative', }} > + {chartAreaLoading && ( +
+
+ 차트 로딩중... +
+ )} {/* 슬롯 0 = 메인 차트 영역 (a) */}
{/* 슬롯 1~7 */} @@ -673,6 +688,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) { marketTickers={marketTickers} compactMode chartVisible={chartVisible} + chartConfigReady={chartConfigReady} + onPaintReadyChange={handleSlotPaintReadyChange} />
))} @@ -680,7 +697,13 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) { )} {/* ── 싱글 레이아웃 (기존) ─────────────────────────────────────── */} -
1 ? 'none' : undefined }}> +
1 ? 'none' : undefined, position: 'relative' }}> + {chartAreaLoading && ( +
+
+ 차트 로딩중... +
+ )} (getFavorites); // DB 로드 완료 여부 (false → DB 로드 전, localStorage 기반 동작) const [dbLoaded, setDbLoaded] = useState(false); + /** DB·앱설정·지표 기본값 반영 완료 — 이후 STOMP 초기 로딩 시작 (이중 페인트 방지) */ + const [chartBootstrapComplete, setChartBootstrapComplete] = useState(false); + /** 싱글 레이아웃 차트 paint 완료 */ + const [singleChartPaintReady, setSingleChartPaintReady] = useState(false); + /** 멀티 슬롯별 paint 완료 */ + const slotPaintReadyRef = useRef>({}); + const [slotPaintEpoch, setSlotPaintEpoch] = useState(0); const priorityMarkets = useMemo( () => [symbol, ...watchlist], @@ -1111,6 +1118,7 @@ export function useChartWorkspace({ const handleCandlesReady = useCallback(() => { chartLiveReadyRef.current = true; + setSingleChartPaintReady(true); const pending = pendingRealtimeBarRef.current; const mgr = managerRef.current; if (!pending || !mgr || barsMarketRef.current !== chartContextRef.current.symbol) return; @@ -1118,16 +1126,26 @@ export function useChartWorkspace({ mgr.updateBar(pending); }, []); + const handleSingleChartPaintReadyChange = useCallback((ready: boolean) => { + setSingleChartPaintReady(ready); + }, []); + + const handleSlotPaintReadyChange = useCallback((slotIndex: number, ready: boolean) => { + slotPaintReadyRef.current[slotIndex] = ready; + setSlotPaintEpoch(v => v + 1); + }, []); + const chartRealtimeSource = appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP'; + const chartConfigReady = appSettingsLoaded && dbLoaded && chartBootstrapComplete; const { bars: upbitBars, barsMarket: upbitBarsMarket, latestBar, wsStatus, isLoading, error } = useChartRealtimeData( symbol, timeframe, useMemo(() => ({ onTickUpdate: handleTickUpdate, onNewCandle: handleNewCandle, - enabled: chartScreenActive && useUpbit, + enabled: chartScreenActive && useUpbit && chartConfigReady, source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT', - }), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartRealtimeSource]), + }), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartConfigReady, chartRealtimeSource]), ); // 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음) @@ -1167,8 +1185,15 @@ export function useChartWorkspace({ useLayoutEffect(() => { chartLiveReadyRef.current = false; pendingRealtimeBarRef.current = null; + setSingleChartPaintReady(false); }, [symbol, timeframe]); + useEffect(() => { + slotPaintReadyRef.current = {}; + setSlotPaintEpoch(v => v + 1); + setSingleChartPaintReady(false); + }, [layoutDef.count, sessionKey]); + // Upbit 초기 로딩 완료 시 stats 업데이트 useEffect(() => { if (useUpbit && upbitBars.length > 0) { @@ -1379,6 +1404,10 @@ export function useChartWorkspace({ }); }, [focusedIndicatorId, indicators, layoutDef.count]); + useEffect(() => { + setSingleChartPaintReady(false); + }, [effectiveIndicators]); + const handleExpandIndicator = useCallback((id: string) => { setFocusedIndicatorId(id); }, []); @@ -1701,7 +1730,7 @@ export function useChartWorkspace({ const bulkSettingsIndicators = layoutDef.count > 1 ? activeSlotIndicators : indicators; /** 설정 화면에서 DB 기본값 저장 후 차트 복귀 시 — 전역 기본값을 차트 인스턴스에 일괄 반영 */ - const lastSyncedSettingsRevisionRef = useRef(indicatorSettingsRevision); + const lastSyncedSettingsRevisionRef = useRef(-1); const syncChartsFromSavedDefaults = useCallback(() => { const merge = (inds: IndicatorConfig[]) => mergeGlobalDefaultsOntoChartIndicators(inds, getParams, getVisualConfig); @@ -1729,15 +1758,25 @@ export function useChartWorkspace({ }, [layoutDef.count, activeSlot, activeSlotIndicators, getParams, getVisualConfig]); useLayoutEffect(() => { - if (!chartVisible) return; - if (indicatorSettingsRevision <= lastSyncedSettingsRevisionRef.current) return; - if (consumeSkipNextChartDefaultsSync()) { - lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision; + if (!appSettingsLoaded || !dbLoaded) { + setChartBootstrapComplete(false); return; } - syncChartsFromSavedDefaults(); - lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision; - }, [chartVisible, indicatorSettingsRevision, syncChartsFromSavedDefaults]); + if (chartVisible && indicatorSettingsRevision > lastSyncedSettingsRevisionRef.current) { + if (consumeSkipNextChartDefaultsSync()) { + lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision; + } else { + syncChartsFromSavedDefaults(); + lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision; + } + } + setChartBootstrapComplete(true); + }, [appSettingsLoaded, dbLoaded, chartVisible, indicatorSettingsRevision, syncChartsFromSavedDefaults]); + + useEffect(() => { + setChartBootstrapComplete(false); + lastSyncedSettingsRevisionRef.current = -1; + }, [sessionKey]); /** 보조지표 일괄 설정 적용 — 전체 지표 DB + 차트 인스턴스 병합 */ const handleBulkIndicatorApply = useCallback((result: { @@ -1911,6 +1950,23 @@ export function useChartWorkspace({ settingsPageProps, ]); + const allSlotsPaintReady = useMemo(() => { + void slotPaintEpoch; + for (let i = 0; i < layoutDef.count; i++) { + if (!slotPaintReadyRef.current[i]) return false; + } + return layoutDef.count > 0; + }, [layoutDef.count, slotPaintEpoch]); + + const chartDataFetched = !useUpbit || (!isLoading && bars.length >= 2 && barsMarket === symbol); + const chartPaintComplete = layoutDef.count > 1 ? allSlotsPaintReady : singleChartPaintReady; + const chartDataLoading = useUpbit && chartVisible && (!chartConfigReady || !chartDataFetched); + const chartAreaLoading = useUpbit && chartVisible && ( + !chartConfigReady + || !chartPaintComplete + || (layoutDef.count === 1 && !chartDataFetched) + ); + const viewProps: ChartWorkspaceViewProps = { onOpenNotificationsPage, onOpenAppDownload, @@ -1946,6 +2002,7 @@ export function useChartWorkspace({ paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled, appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote, isLoading, error: error ?? null, bulkSettingsIndicators, chartIndicatorsForSettings, + chartConfigReady, chartAreaLoading, chartDataLoading, getParams, getVisualConfig, activeSlotIndicators, setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid, setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible, @@ -1974,7 +2031,8 @@ export function useChartWorkspace({ handleTogglePriceExtremeLabels, handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange, handleSaveCustomOverlaySelection, syncCustomOverlaysToManager, - handleCandlesReady, applyBacktestMarkersWhenReady, btRun, refreshPaperAccount, + handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange, + applyBacktestMarkersWhenReady, btRun, refreshPaperAccount, handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers, goToMarketChart, handleTimezoneChange, }; diff --git a/frontend/src/components/ChartSlot.tsx b/frontend/src/components/ChartSlot.tsx index 4ba5c58..61fe242 100644 --- a/frontend/src/components/ChartSlot.tsx +++ b/frontend/src/components/ChartSlot.tsx @@ -206,6 +206,10 @@ export interface ChartSlotProps { chartLiveReceiveHighlight?: boolean; /** 실시간 차트 화면 표시 여부 (설정 등 다른 메뉴에서 숨김 시 false) */ chartVisible?: boolean; + /** 워크스페이스·지표 설정 반영 완료 — STOMP 시작 게이트 */ + chartConfigReady?: boolean; + /** paint 완료 알림 (멀티차트 전체 로딩용) */ + onPaintReadyChange?: (slotIndex: number, ready: boolean) => void; /** pane 구분선 */ chartPaneSeparator?: ChartPaneSeparatorOptions; /** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */ @@ -241,6 +245,8 @@ const ChartSlot = forwardRef(function ChartSlot compactMode = false, chartLiveReceiveHighlight = true, chartVisible = true, + chartConfigReady = true, + onPaintReadyChange, chartPaneSeparator, chartCrosshairInfoVisible = true, chartHoverToolbarVisible = true, @@ -508,17 +514,6 @@ const ChartSlot = forwardRef(function ChartSlot // ※ TradingChart를 재마운트(key 변경)하면 managerRef가 초기화되어 MarketSearchPanel 등 // 다른 상태에 영향을 줄 수 있으므로, 재마운트 대신 resetPaneHeights 재시도만 수행한다. const slotContainerRef = useRef(null); - // 데이터 로딩 완료 후 차트가 여전히 비어 있으면 1회만 재마운트 (data + blank guard) - const [reloadTick, setReloadTick] = useState(0); - const reloadTriggeredRef = useRef(false); // 재마운트는 최대 1회 - useLayoutEffect(() => { - reloadTriggeredRef.current = false; - chartLiveReadyRef.current = false; - pendingRealtimeBarRef.current = null; - pendingSyncVisibleRef.current = null; - pendingSyncLogicalRef.current = null; - skipSyncApplyRef.current = true; - }, [symbol, timeframe]); useEffect(() => { const checks = [200, 500, 1000, 2000].map(delay => setTimeout(() => { @@ -528,21 +523,13 @@ const ChartSlot = forwardRef(function ChartSlot const h = el.clientHeight; if (h <= 0) return; if (mgr.hasMainSeries()) { - // 차트 데이터 있음 → pane 높이만 재적용 mgr.resetPaneHeights(h); - } else if (!reloadTriggeredRef.current) { - // 차트 비어 있고 아직 재시도 안 했음 → 1회만 재마운트 - // (bars 로딩 지연일 수 있으므로, 늦은 타이머에서만 트리거) - if (delay >= 1000) { - reloadTriggeredRef.current = true; - setReloadTick(t => t + 1); - } } }, delay) ); return () => checks.forEach(clearTimeout); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [symbol, timeframe]); // ── 데이터 ──────────────────────────────────────────────────────────── const useUpbit = isUpbitMarket(symbol); @@ -567,12 +554,26 @@ const ChartSlot = forwardRef(function ChartSlot const handleCandlesReady = useCallback(() => { chartLiveReadyRef.current = true; + onPaintReadyChange?.(slotIndex, true); const pending = pendingRealtimeBarRef.current; const mgr = managerRef.current; if (!pending || !mgr || barsMarketRef.current !== symbolRef.current) return; pendingRealtimeBarRef.current = null; mgr.updateBar(pending); - }, []); + }, [slotIndex, onPaintReadyChange]); + + const handleSlotPaintReadyChange = useCallback((ready: boolean) => { + onPaintReadyChange?.(slotIndex, ready); + }, [slotIndex, onPaintReadyChange]); + + useLayoutEffect(() => { + chartLiveReadyRef.current = false; + pendingRealtimeBarRef.current = null; + pendingSyncVisibleRef.current = null; + pendingSyncLogicalRef.current = null; + skipSyncApplyRef.current = true; + onPaintReadyChange?.(slotIndex, false); + }, [symbol, timeframe, slotIndex, onPaintReadyChange]); const handleTickUpdate = useCallback((bar: OHLCVBar) => { if (barsMarketRef.current !== symbolRef.current) return; @@ -604,15 +605,18 @@ const ChartSlot = forwardRef(function ChartSlot useMemo(() => ({ onTickUpdate: handleTickUpdate, onNewCandle: handleNewCandle, - enabled: useUpbit && chartVisible, + enabled: useUpbit && chartVisible && chartConfigReady, source: chartRealtimeSource, - }), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartRealtimeSource]), + }), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartConfigReady, chartRealtimeSource]), ); const bars = useUpbit ? upbitBars : simBars; const barsMarket = useUpbit ? upbitBarsMarket : symbol; barsMarketRef.current = barsMarket; + const slotDataFetched = !useUpbit || (!isLoading && bars.length >= 2 && barsMarket === symbol); + const slotChartLoading = useUpbit && chartVisible && (!chartConfigReady || !slotDataFetched); + const lastKnownBar = useUpbit ? (latestBar ?? (bars.length > 0 ? bars[bars.length - 1] : null)) : (bars.length > 0 ? bars[bars.length - 1] : null); @@ -966,15 +970,6 @@ const ChartSlot = forwardRef(function ChartSlot className={compactMode ? 'vtd-card-chart-canvas slot-chart-wrap slot-chart-wrap--compact' : 'slot-chart-wrap'} style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column' }} > - {useUpbit && isLoading && ( - compactMode ? ( -
차트 로딩…
- ) : ( -
-
-
- ) - )} {isLoadingMore && (
@@ -983,8 +978,9 @@ const ChartSlot = forwardRef(function ChartSlot )} { - if (startId === START_NODE_ID) { - const synced = syncBuySellPrimaryStartCandleTypes(buyEditorState, sellEditorState, candleTypes); - setBuyCondition(synced.buy.root); - setBuyLayout(prev => ({ - ...prev, - startMeta: synced.buy.startMeta, - extraStartIds: synced.buy.extraStartIds, - extraRoots: synced.buy.extraRoots, - startCombineOp: normalizeStartCombineOp(synced.buy.startCombineOp), - })); - setSellCondition(synced.sell.root); - setSellLayout(prev => ({ - ...prev, - startMeta: synced.sell.startMeta, - extraStartIds: synced.sell.extraStartIds, - extraRoots: synced.sell.extraRoots, - startCombineOp: normalizeStartCombineOp(synced.sell.startCombineOp), - })); - scheduleStrategyPersist(); - return; - } - const state = signalTab === 'buy' ? buyEditorState : sellEditorState; - handleEditorStateChange(updateStartCandleTypes(state, startId, candleTypes)); - }, [ - buyEditorState, - sellEditorState, - handleEditorStateChange, - scheduleStrategyPersist, - signalTab, - ]); + handleEditorStateChange(updateStartCandleTypes(currentEditorState, startId, candleTypes)); + }, [handleEditorStateChange, currentEditorState]); const handleStartCombineOpChange = useCallback((op: StartCombineOp) => { handleEditorStateChange(updateStartCombineOp(currentEditorState, op)); diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 73691ac..a0055f4 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -167,6 +167,8 @@ interface TradingChartProps { crosshairInfoVisible?: boolean; /** 외부 캔들 데이터 fetch 중 (실시간 차트 초기 로딩) */ dataLoading?: boolean; + /** chartPaintReady 변경 알림 (워크스페이스 전체 로딩 오버레이용) */ + onChartPaintReadyChange?: (ready: boolean) => void; /** 마운트 시 캔들 pane 전체보기(하단 보조지표 pane 숨김) — 가상매매 카드 등 */ defaultCandleOnly?: boolean; /** 캔들 pane 좌하단 전체보기/복원 버튼 (기본 true) */ @@ -236,6 +238,7 @@ const TradingChart: React.FC = ({ showCandleOverlayControls = false, crosshairInfoVisible = true, dataLoading = false, + onChartPaintReadyChange, defaultCandleOnly = false, showCandlePaneControls = true, candleOverlayToggles, @@ -399,6 +402,14 @@ const TradingChart: React.FC = ({ chartVisibleRef.current = chartVisible; }, [chartVisible]); + useEffect(() => { + if (dataLoading) setChartPaintReady(false); + }, [dataLoading]); + + useEffect(() => { + onChartPaintReadyChange?.(chartPaintReady && !dataLoading); + }, [chartPaintReady, dataLoading, onChartPaintReadyChange]); + useEffect(() => { const el = containerRef.current; if (!el) return; @@ -976,12 +987,18 @@ const TradingChart: React.FC = ({ } while (reloadCoalesceRef.current); } finally { reloadInFlightRef.current = false; - if (managerRef.current?.hasMainSeries()) { - setChartPaintReady(true); - } if (reloadCoalesceRef.current) { reloadCoalesceRef.current = false; queueFullReload(); + return; + } + const m = managerRef.current; + if (m?.hasMainSeries()) { + const expected = countExpectedVisibleIndicators(indicatorsRef.current); + const loaded = m.getLoadedIndicatorCount(); + if (expected === 0 || loaded >= expected) { + setChartPaintReady(true); + } } } }, [normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]); diff --git a/frontend/src/utils/strategyConditionSerde.ts b/frontend/src/utils/strategyConditionSerde.ts index 8889f54..0e8082b 100644 --- a/frontend/src/utils/strategyConditionSerde.ts +++ b/frontend/src/utils/strategyConditionSerde.ts @@ -204,11 +204,6 @@ function normalizeCandleTypesList(types: string[]): string[] { } } if (out.length === 0) return [DEFAULT_START_CANDLE]; - // 레거시/불일치로 multi 분봉에 stale 1m 이 섞인 경우가 있어, - // multi 인데 non-1m 이 있으면 1m 을 기본적으로 제외한다. - if (out.length > 1 && out.includes('1m') && out.some(ct => ct !== '1m')) { - return out.filter(ct => ct !== '1m'); - } return out; } @@ -414,7 +409,7 @@ export function collectTimeframesFromEditorState(state: EditorConditionState): s return [...set]; } -/** 매수·매도 START(기본) 평가 분봉을 동일하게 맞춤 — 한쪽만 3m·5m 체크 시 다른 쪽 1m 잔존 방지 */ +/** @deprecated 매수·매도 시간봉은 각각 독립 설정 — updateStartCandleTypes 사용 */ export function syncBuySellPrimaryStartCandleTypes( buy: EditorConditionState, sell: EditorConditionState, @@ -436,19 +431,14 @@ export function syncEditorStateCandleTypesForSave(state: EditorConditionState): return next; } -/** 저장 직전 — 양쪽 START 분봉 합집합을 각 탭에 반영 (매수만 설정한 경우 매도 DSL도 동기화) */ +/** 저장 직전 — 각 탭 START 분봉을 해당 조건 트리 left/rightCandleType 에 반영 (매수·매도 독립) */ export function alignBuySellStartCandleTypesForSave( buy: EditorConditionState, sell: EditorConditionState, ): { buy: EditorConditionState; sell: EditorConditionState } { - const merged = normalizeCandleTypesList([ - ...getStartCandleTypes(buy.startMeta[START_NODE_ID]), - ...getStartCandleTypes(sell.startMeta[START_NODE_ID]), - ]); - const synced = syncBuySellPrimaryStartCandleTypes(buy, sell, merged); return { - buy: syncEditorStateCandleTypesForSave(synced.buy), - sell: syncEditorStateCandleTypesForSave(synced.sell), + buy: syncEditorStateCandleTypesForSave(buy), + sell: syncEditorStateCandleTypesForSave(sell), }; } diff --git a/frontend/src/utils/strategyStartNodes.ts b/frontend/src/utils/strategyStartNodes.ts index 22d2c6c..12e5653 100644 --- a/frontend/src/utils/strategyStartNodes.ts +++ b/frontend/src/utils/strategyStartNodes.ts @@ -2,6 +2,8 @@ import { generateNodeId } from './strategyTypes'; export const START_NODE_ID = '__strategy_start__'; export const DEFAULT_START_CANDLE = '1m'; +/** 새 전략 START 노드 기본 평가 분봉 */ +export const DEFAULT_NEW_STRATEGY_START_CANDLE = '3m'; export const STRATEGY_CANDLE_TYPES = [ '1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1w', '1d', @@ -36,7 +38,12 @@ export function createStartNodeId(): string { } export function defaultStartMeta(): Record { - return { [START_NODE_ID]: { candleTypes: [DEFAULT_START_CANDLE], candleType: DEFAULT_START_CANDLE } }; + return { + [START_NODE_ID]: { + candleTypes: [DEFAULT_NEW_STRATEGY_START_CANDLE], + candleType: DEFAULT_NEW_STRATEGY_START_CANDLE, + }, + }; } /** START 메타에서 정렬된 평가 분봉 목록 (최소 1개) */