From f2208aab99184e5644055c3a2913f6f575b7fefd Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 1 Jun 2026 01:35:31 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=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 | 7 + frontend/src/App.tsx | 39 +++- .../src/components/BacktestSettingsPanel.tsx | 2 +- frontend/src/components/ChartSlot.tsx | 1 + frontend/src/components/ColorInput.tsx | 64 ++++-- frontend/src/components/LiveStrategyPanel.tsx | 11 +- frontend/src/components/MarketSearchPanel.tsx | 10 +- .../src/components/PaneSeparatorOverlay.tsx | 11 +- frontend/src/components/SettingsPage.tsx | 58 +++-- .../src/components/StrategyEditorPage.tsx | 8 +- frontend/src/components/TradingChart.tsx | 46 +++- .../src/components/VirtualTradingPage.tsx | 21 +- .../virtual/VirtualGridUnifiedHeader.tsx | 216 ++++++------------ .../virtual/VirtualLeftTargetPanel.tsx | 6 +- .../virtual/VirtualSessionToolbar.tsx | 91 +++----- .../src/hooks/useVirtualTargetRegistry.ts | 4 - frontend/src/hooks/useVirtualTradingCore.ts | 24 +- frontend/src/styles/builderPageShell.css | 6 + .../src/styles/virtualTradingDashboard.css | 96 +++++++- frontend/src/types/chartPaneSeparator.ts | 14 ++ frontend/src/utils/ChartManager.ts | 18 +- frontend/src/utils/backendApi.ts | 54 ++++- frontend/src/utils/plotColorUtils.ts | 2 + frontend/src/utils/strategyTimeframePin.ts | 9 +- frontend/src/utils/strategyTimeframeSync.ts | 25 +- frontend/src/utils/upbitWsBroker.ts | 33 ++- frontend/src/utils/virtualTargetMutations.ts | 1 - frontend/src/utils/virtualTradingStorage.ts | 2 +- 28 files changed, 546 insertions(+), 333 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index f73ca6a..e53fade 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -3196,6 +3196,13 @@ html.theme-blue { padding-bottom: 10px; border-bottom: 1px solid var(--sep); } +.ism-color-popover-footer { + display: flex; + justify-content: flex-end; + margin-top: 12px; + padding-top: 10px; + border-top: 1px solid var(--sep); +} .ism-color-native-label { display: flex; align-items: center; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9b9a7a2..ff6e683 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -84,6 +84,7 @@ import LiveStrategyPanel from './components/LiveStrategyPanel'; import { TradeNotificationProvider } from './contexts/TradeNotificationContext'; import { VerificationIssueNotificationProvider } from './contexts/VerificationIssueNotificationContext'; import { LiveSignalNotifier, type LiveSignalNotifierHandle } from './components/LiveSignalNotifier'; +import type { LiveMarker } from './hooks/useLiveStrategyMarkers'; import { FormalLoginGate } from './components/FormalLoginGate'; import { isFormalLogin, @@ -999,13 +1000,18 @@ function App() { // 전략 설정 화면에서 추가/수정된 전략이 즉시 반영되도록 한다. const refreshLiveStrategies = useCallback(() => { loadStrategiesForLive() - .then(list => setLiveStrategies( - list + .then(list => { + const rows = list .filter(s => s.id != null) - .map(s => ({ id: s.id as number, name: s.name })) - )) + .map(s => ({ id: s.id as number, name: s.name })); + setLiveStrategies(rows); + const sid = appDefaults.liveStrategyId; + if (sid != null && !rows.some(r => r.id === sid)) { + saveAppDef({ liveStrategyId: undefined, liveStrategyCheck: false }); + } + }) .catch(() => {}); - }, []); + }, [appDefaults.liveStrategyId, saveAppDef]); useEffect(() => { if (!formalLogin) { @@ -1016,7 +1022,9 @@ function App() { }, [menuPage, showLivePanel, formalLogin, refreshLiveStrategies]); const liveNotifierRef = useRef(null); + const liveMarkersRef = useRef([]); const clearLiveMarkers = useCallback(() => { + liveMarkersRef.current = []; liveNotifierRef.current?.clearMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }, []); @@ -1068,9 +1076,16 @@ function App() { useEffect(() => { if (!appSettingsLoaded) return; - managerRef.current?.setPaneSeparatorOptions(chartPaneSeparator); - slotRefs.current.forEach(slot => slot?.setPaneSeparatorOptions(chartPaneSeparator)); - }, [appSettingsLoaded, chartPaneSeparator]); + const apply = () => { + managerRef.current?.setPaneSeparatorOptions(chartPaneSeparator); + managerRef.current?.refreshPaneSeparatorOverlay(); + slotRefs.current.forEach(slot => slot?.setPaneSeparatorOptions(chartPaneSeparator)); + }; + apply(); + if (menuPage !== 'chart') return; + const raf = requestAnimationFrame(() => requestAnimationFrame(apply)); + return () => cancelAnimationFrame(raf); + }, [appSettingsLoaded, menuPage, chartPaneSeparator]); // mainChartStyle 변경 → ChartManager 즉시 적용 useEffect(() => { @@ -1112,6 +1127,12 @@ function App() { }; }, [btMatchesChart, btSignals, btRunSeq, symbol, timeframe, appDefaults.btShowPrice, applyBacktestMarkersWhenReady, syncBacktestMarkers]); + // 금액 표시 토글 변경 시 실시간 시그널 마커 텍스트 재적용 + useEffect(() => { + if (menuPage !== 'chart' || liveMarkersRef.current.length === 0) return; + managerRef.current?.setLiveStrategyMarkers(liveMarkersRef.current, appDefaults.btShowPrice); + }, [menuPage, appDefaults.btShowPrice]); + // ── 시뮬레이션 데이터 (Upbit 아닐 때) ────────────────────────────────── const [simBars, setSimBars] = useState(() => !isUpbitMarket(initial.symbol) ? generateBars(initial.symbol, initial.timeframe) : [] @@ -1751,6 +1772,7 @@ function App() { popupEnabled={appDefaults.tradeAlertPopup ?? true} chartMarkersActive={menuPage === 'chart'} onMarkersChange={markers => { + liveMarkersRef.current = markers; managerRef.current?.setLiveStrategyMarkers(markers, appDefaults.btShowPrice); }} /> @@ -1989,6 +2011,7 @@ function App() { onChartPaneSeparatorChange={opts => { saveAppDef({ chartPaneSeparator: opts }); managerRef.current?.setPaneSeparatorOptions(opts); + managerRef.current?.refreshPaneSeparatorOverlay(); slotRefs.current.forEach(slot => slot?.setPaneSeparatorOptions(opts)); }} paperTradingEnabled={paperTradingEnabled} diff --git a/frontend/src/components/BacktestSettingsPanel.tsx b/frontend/src/components/BacktestSettingsPanel.tsx index deac132..9167ceb 100644 --- a/frontend/src/components/BacktestSettingsPanel.tsx +++ b/frontend/src/components/BacktestSettingsPanel.tsx @@ -96,7 +96,7 @@ export const BacktestSettingsPanel: React.FC = ({ + + + {chartPaneSeparator && onChartPaneSeparatorChange && (
@@ -961,10 +979,12 @@ const ChartPanel: React.FC = ({
onChartPaneSeparatorChange({ - ...chartPaneSeparator, - mainToIndicator, - })} + onChange={mainToIndicator => onChartPaneSeparatorChange( + cloneChartPaneSeparatorOptions({ + ...chartPaneSeparator, + mainToIndicator, + }), + )} />
보조지표 간 @@ -972,10 +992,12 @@ const ChartPanel: React.FC = ({
onChartPaneSeparatorChange({ - ...chartPaneSeparator, - indicatorToIndicator, - })} + onChange={indicatorToIndicator => onChartPaneSeparatorChange( + cloneChartPaneSeparatorOptions({ + ...chartPaneSeparator, + indicatorToIndicator, + }), + )} />
)} @@ -1810,6 +1832,8 @@ const SettingsPage: React.FC = ({ onChartPaneSeparatorChange={onChartPaneSeparatorChange} chartTimeFormat={chartTimeFormat} onChartTimeFormatChange={onChartTimeFormatChange} + btShowPrice={btShowPrice} + onBtShowPrice={onBtShowPrice} /> ); case 'indicators': diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index 4d80c25..63044a6 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -515,7 +515,7 @@ export default function StrategyEditorPage({ theme }: Props) { if (!hasRegisteredUser()) return; loadStrategies().then(async list => { if (list?.length) { - setStrategies(list.map(s => ({ + const mapped = list.map(s => ({ id: s.id ?? Date.now(), name: s.name, description: s.description, @@ -525,7 +525,11 @@ export default function StrategyEditorPage({ theme }: Props) { enabled: s.enabled ?? true, createdAt: s.createdAt, updatedAt: s.updatedAt, - }))); + })); + setStrategies(mapped); + setSelectedId(prev => + prev != null && mapped.some(x => x.id === prev) ? prev : null, + ); } else { const local = loadStratsLocal(); if (!local.length) return; diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index b634ff8..47c0db5 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -337,7 +337,6 @@ const TradingChart: React.FC = ({ const el = containerRef.current; if (el && !panStateRef.current.active) { el.style.cursor = canPanRef.current ? 'grab' : ''; - el.style.touchAction = canPanRef.current ? 'none' : ''; } }, [mode, drawingTool]); @@ -365,6 +364,7 @@ const TradingChart: React.FC = ({ useEffect(() => { if (!paneSeparatorOptions) return; managerRef.current?.setPaneSeparatorOptions(paneSeparatorOptions); + managerRef.current?.refreshPaneSeparatorOverlay(); }, [paneSeparatorOptions]); /** @@ -913,15 +913,29 @@ const TradingChart: React.FC = ({ }; container.addEventListener('contextmenu', onContextMenuCapture, { capture: true }); + const wrapper = wrapperRef.current; + const isChartPlotPointer = (clientX: number, clientY: number) => { + const rect = container.getBoundingClientRect(); + const chartX = clientX - rect.left; + const chartY = clientY - rect.top; + if (chartX < 0 || chartX > container.clientWidth) return null; + if (chartY < 0 || chartY > container.clientHeight) return null; + return { chartX, chartY }; + }; + const onPanPointerDown = (e: PointerEvent) => { if (!canPanRef.current || e.button !== 0) return; - const rect = container.getBoundingClientRect(); - const chartX = e.clientX - rect.left; - const chartY = e.clientY - rect.top; - if (mgr.isOnPriceAxis(chartX, chartY) || mgr.isOnTimeAxis(chartY)) return; - const originPaneIndex = mgr.getPaneIndexAtChartY(chartY); + const target = e.target as HTMLElement | null; + if (target?.closest('.chart-right-toolbar, .pane-legend-item, .pane-drag-handle, .pane-btn, .candle-pane-controls')) { + return; + } + const pt = isChartPlotPointer(e.clientX, e.clientY); + if (!pt) return; + if (mgr.isOnPriceAxis(pt.chartX, pt.chartY) || mgr.isOnTimeAxis(pt.chartY)) return; + const originPaneIndex = mgr.getPaneIndexAtChartY(pt.chartY); + const captureEl = wrapper ?? container; try { - container.setPointerCapture(e.pointerId); + captureEl.setPointerCapture(e.pointerId); } catch { /* ignore */ } @@ -953,8 +967,16 @@ const TradingChart: React.FC = ({ }); }; - const onPanPointerUp = () => { + const onPanPointerUp = (e: PointerEvent) => { if (!panStateRef.current.active) return; + const captureEl = wrapper ?? container; + try { + if (captureEl.hasPointerCapture(e.pointerId)) { + captureEl.releasePointerCapture(e.pointerId); + } + } catch { + /* ignore */ + } if (panStateRef.current.moved) { suppressClickRef.current = true; managerRef.current?.notifyViewportChanged(); @@ -963,8 +985,10 @@ const TradingChart: React.FC = ({ container.style.cursor = canPanRef.current ? 'grab' : ''; }; - container.addEventListener('pointerdown', onPanPointerDown); - window.addEventListener('pointermove', onPanPointerMove); + const panCapture = true; + const panTarget = wrapper ?? container; + panTarget.addEventListener('pointerdown', onPanPointerDown, panCapture); + window.addEventListener('pointermove', onPanPointerMove, { passive: false }); window.addEventListener('pointerup', onPanPointerUp); window.addEventListener('pointercancel', onPanPointerUp); @@ -1122,7 +1146,7 @@ const TradingChart: React.FC = ({ container.removeEventListener('click', handleDrawingCapture, { capture: true }); container.removeEventListener('contextmenu', onContextMenuCapture, { capture: true }); container.removeEventListener('dblclick', onCandlePaneDblClick, { capture: true }); - container.removeEventListener('pointerdown', onPanPointerDown); + panTarget.removeEventListener('pointerdown', onPanPointerDown, panCapture); container.removeEventListener('wheel', onWheelCapture, { capture: true }); window.removeEventListener('pointermove', onPanPointerMove); window.removeEventListener('pointerup', onPanPointerUp); diff --git a/frontend/src/components/VirtualTradingPage.tsx b/frontend/src/components/VirtualTradingPage.tsx index f7d1f50..7603141 100644 --- a/frontend/src/components/VirtualTradingPage.tsx +++ b/frontend/src/components/VirtualTradingPage.tsx @@ -114,7 +114,26 @@ const VirtualTradingPage: React.FC = ({ useEffect(() => { void Promise.all([ - loadStrategies().then(setStrategies).catch(() => setStrategies([])), + loadStrategies() + .then(list => { + setStrategies(list); + const validIds = new Set( + list.map(s => s.id).filter((id): id is number => id != null && id > 0), + ); + setSession(prev => + prev.globalStrategyId != null && !validIds.has(prev.globalStrategyId) + ? { ...prev, globalStrategyId: null } + : prev, + ); + setTargets(prev => + prev.map(t => + t.strategyId != null && !validIds.has(t.strategyId) + ? { ...t, strategyId: null } + : t, + ), + ); + }) + .catch(() => setStrategies([])), loadPaperSummary().then(setSummary), loadPaperTrades().then(setTrades), ]).finally(() => setLoading(false)); diff --git a/frontend/src/components/virtual/VirtualGridUnifiedHeader.tsx b/frontend/src/components/virtual/VirtualGridUnifiedHeader.tsx index 7de9a79..86fe400 100644 --- a/frontend/src/components/virtual/VirtualGridUnifiedHeader.tsx +++ b/frontend/src/components/virtual/VirtualGridUnifiedHeader.tsx @@ -42,103 +42,65 @@ export const VirtualSessionHeaderControls: React.FC = ({ onPaperAutoTradeEnabled, paperTradingEnabled = true, }) => ( -
-
- 투자전략 - -
+
+ -
- 실행방식 -
- - -
-
+ -
- 시그널 모드 -
- - -
-
+ -
- 자동매매 -
- - -
-
+ -
+
@@ -147,6 +109,7 @@ export const VirtualSessionHeaderControls: React.FC = ({ className="bps-btn bps-btn--danger vtd-session-btn" disabled={!session.running} onClick={onStop} + title="가상투자 종료" > 종료 @@ -165,63 +128,36 @@ export const VirtualViewHeaderControls: React.FC = ({ onGridPresetChange, showGridLayoutPicker = true, }) => ( -
+
{showGridLayoutPicker && onGridPresetChange && ( - <> - -
); diff --git a/frontend/src/components/virtual/VirtualLeftTargetPanel.tsx b/frontend/src/components/virtual/VirtualLeftTargetPanel.tsx index de89f2a..1785eb3 100644 --- a/frontend/src/components/virtual/VirtualLeftTargetPanel.tsx +++ b/frontend/src/components/virtual/VirtualLeftTargetPanel.tsx @@ -88,8 +88,6 @@ const VirtualLeftTargetPanel: React.FC = ({ }, [addedSet, targets, globalStrategyId, onTargetsChange, onSelectMarket, virtualTargetMaxCount]); const handleRemove = useCallback((market: string) => { - const item = targets.find(t => t.market === market); - if (item?.pinned) return; onTargetsChange(targets.filter(t => t.market !== market)); }, [targets, onTargetsChange]); @@ -119,6 +117,7 @@ const VirtualLeftTargetPanel: React.FC = ({ addedMarkets={addedSet} maxTargetsReached={atTargetLimit} onAdd={handleAdd} + onRemove={handleRemove} onSelect={onSelectMarket} onClose={closeDropdown} /> @@ -174,9 +173,8 @@ const VirtualLeftTargetPanel: React.FC = ({ - -
-
+ -
- 시그널 모드 -
- - -
-
+ -
+