Files
goldenChart/frontend/src/chart/ChartWorkspaceView.tsx
T
2026-06-16 08:53:35 +09:00

1199 lines
56 KiB
TypeScript

import '../styles/paperDashboard.css';
import '../styles/tradeRightPanel.css';
import React from 'react';
import DrawingToolbar from '../components/DrawingToolbar';
import BottomBar from '../components/BottomBar';
import TradingChart from '../components/TradingChart';
import StatsPanel from '../components/StatsPanel';
import WatchList from '../components/WatchList';
import AlertManager from '../components/AlertManager';
import ObjectTree from '../components/ObjectTree';
import IndicatorContextToolbar from '../components/IndicatorContextToolbar';
import IndicatorSettingsModal from '../components/IndicatorSettingsModal';
import IndicatorBulkSettingsModal from '../components/IndicatorBulkSettingsModal';
import MainChartSettingsModal from '../components/MainChartSettingsModal';
import DrawingContextToolbar from '../components/DrawingContextToolbar';
import DrawingSettingsModal from '../components/DrawingSettingsModal';
import FibTZSettingsModal from '../components/FibTZSettingsModal';
import ChartSlot, { type ChartSlotHandle } from '../components/ChartSlot';
import { type LayoutDef, type SyncOptions } from '../utils/layoutTypes';
import type {
OHLCVBar, ChartType, Theme, Timeframe, IndicatorConfig, LegendData, Drawing,
ChartMode, MainChartStyle, TradeOrderFillRequest,
} from '../types';
import type { ChartManager } from '../utils/ChartManager';
import { LOAD_MORE_TRIGGER } from '../hooks/useHistoryLoader';
import type { WsStatus } from '../hooks/useChartRealtimeData';
import type { TickerData } from '../hooks/useMarketTicker';
import type { OrderbookState } from '../hooks/useUpbitOrderbook';
import type { BacktestStats } from '../utils/backendApi';
import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal';
import BacktestAnalysisReportModal from '../components/backtest/BacktestAnalysisReportModal';
import { type MenuPage } from '../components/TopMenuBar';
import BacktestPanel from '../components/BacktestPanel';
import LiveStrategyPanel from '../components/LiveStrategyPanel';
import BacktestSettingsModal from '../components/BacktestSettingsModal';
import MarketPanel from '../components/MarketPanel';
import ChartLegendBar from '../components/ChartLegendBar';
import RightSidePanel from '../components/RightSidePanel';
import ChartCustomOverlaySettingsModal from '../components/ChartCustomOverlaySettingsModal';
import MobileChartDock from '../components/MobileChartDock';
import { ChartToolbarNotify } from '../components/NotifyUiBindings';
import {
buildMainIndicatorConfig,
indicatorSettingsTemplateId,
indicatorTypeFromSettingsId,
isIndicatorSettingsTemplateId,
} from '../utils/indicatorMainConfig';
import { initializeIndicatorConfigForEditor } from '../utils/indicatorSettingsEditor';
import type { PriceStats } from '../utils/calculations';
import type { ChartOverlayToggleKey, ChartOverlayVisibility } from '../utils/chartOverlayVisibility';
import type { ChartCustomOverlaySelection, ChartCustomOverlaySlotId } from '../utils/chartCustomOverlay';
import type { ChartLegendVisibility } from '../types/chartLegend';
import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator';
import type { LiveStrategySettingsDto } from '../utils/backendApi';
import type { BacktestSettingsDto } from '../utils/backendApi';
import { addWatchlistItem, removeWatchlistItem } from '../utils/backendApi';
import { fmtListTimestamp } from '../utils/backtestUiUtils';
export interface ChartWorkspaceViewProps {
onOpenNotificationsPage: () => void;
symbol: string;
timeframe: Timeframe;
chartType: ChartType;
theme: Theme;
mode: ChartMode;
logScale: boolean;
showGrid: boolean;
toolbarActiveIndicators: IndicatorConfig[];
layoutDef: LayoutDef;
syncOptions: SyncOptions;
activeSlot: number;
activeSlotSymbol: string;
activeSlotTf: Timeframe;
slot0InitProps: { symbol: string; timeframe: Timeframe; indicators: IndicatorConfig[] } | null;
slotInitProps: Record<number, {
symbol?: string;
timeframe?: Timeframe;
indicators?: IndicatorConfig[];
mainChartStyle?: MainChartStyle;
}>;
syncTime: number;
syncVisibleRange: { from: number; to: number } | null;
syncLogicalRange: { from: number; to: number } | null;
slotRefs: React.MutableRefObject<(ChartSlotHandle | null)[]>;
managerRef: React.MutableRefObject<ChartManager | null>;
undoStack: React.MutableRefObject<Drawing[][]>;
redoStack: React.MutableRefObject<Drawing[][]>;
canUndo: boolean;
canRedo: boolean;
drawingTool: string;
drawings: Drawing[];
drawingsLocked: boolean;
drawingsVisible: boolean;
indicatorsVisible: boolean;
magnetMode: 'off' | 'weak' | 'strong';
snapToIndicator: boolean;
keepLockedOnDelete: boolean;
legend: LegendData | null;
showStats: boolean;
showWatch: boolean;
showAlert: boolean;
showObjectTree: boolean;
percentScale: boolean;
magnifierActive: boolean;
showMarketPanel: boolean;
mobileRightOpen: boolean;
mobileRightTab: 'trade' | 'orderbook';
marketTickers: Map<string, TickerData>;
marketInfos: import('../hooks/useMarketTicker').MarketInfo[];
marketLoading: boolean;
usdRate: number;
refreshAllTickers: () => Promise<void>;
setMarketSearchOpen: React.Dispatch<React.SetStateAction<boolean>>;
formalLogin: boolean;
requireFormalLogin: () => void;
chartVisible: boolean;
menuPage: MenuPage;
isMobile: boolean;
indicators: IndicatorConfig[];
effectiveIndicators: IndicatorConfig[];
mainChartStyle: MainChartStyle;
showMainChartModal: boolean;
showBulkIndSettings: boolean;
settingsModalId: string | null;
ctxToolbar: { entryId: string; screenX: number; screenY: number } | null;
drawingCtxToolbar: { id: string; screenX: number; screenY: number } | null;
drawingSettingsId: string | null;
selectedDrawingId: string | null;
focusedIndicatorId: string | null;
overlayVisibility: ChartOverlayVisibility;
overlayVisibilityRef: React.MutableRefObject<ChartOverlayVisibility>;
candleOverlayToggles: ReturnType<typeof import('../utils/chartOverlayVisibility').listChartOverlayToggleItems>;
customOverlaySelection: ChartCustomOverlaySelection;
custom1OverlaySelection: ChartCustomOverlaySelection;
activeCustomOverlaySlot: ChartCustomOverlaySlotId | null;
customOverlaySettingsSlot: ChartCustomOverlaySlotId | null;
customOverlayActive: boolean;
custom1OverlayActive: boolean;
chartCandleAreaPriceLabels: boolean;
chartSeriesPriceLabels: boolean;
chartVolumeVisible: boolean;
chartPriceExtremeLabelsVisible: boolean;
chartPaneSeparator: ChartPaneSeparatorOptions;
chartCrosshairInfoVisible: boolean;
chartHoverToolbarVisible: boolean;
chartLiveReceiveHighlight: boolean;
chartRealtimeSource: 'BACKEND_STOMP' | 'UPBIT_DIRECT';
displayTimezone: string;
chartTimeFormat: string;
chartLegendOptions: ChartLegendVisibility;
useUpbit: boolean;
wsStatus: WsStatus;
bars: OHLCVBar[];
barsMarket: string;
barsMarketRef: React.MutableRefObject<string | null>;
chartMountKey: number;
chartLiveReadyRef: React.MutableRefObject<boolean>;
pendingRealtimeBarRef: React.MutableRefObject<OHLCVBar | null>;
isSingleLoadingMore: boolean;
singleLoadMoreRef: React.MutableRefObject<() => void>;
btMatchesChart: boolean;
btRunning: boolean;
btSettings: BacktestSettingsDto;
btStats: BacktestStats | null;
btSignals: import('../utils/backendApi').BacktestSignal[];
btClear: () => void;
btMarkerPayloadRef: React.MutableRefObject<{
symbol: string;
timeframe: string;
signals: import('../utils/backendApi').BacktestSignal[];
showPrice: boolean;
} | null>;
syncBacktestMarkersRef: React.MutableRefObject<() => void>;
showBtSettings: boolean;
showBtResult: boolean;
showLivePanel: boolean;
liveStrategies: { id: number; name: string }[];
liveStrategySettings: LiveStrategySettingsDto;
monitoredMarkets: string[];
isVirtualActive: boolean;
watchlist: string[];
chartBtReportModel: BacktestAnalysisReportModel | null;
chartSymbol: string;
chartScreenActive: boolean;
orderbook: OrderbookState;
obWsStatus: WsStatus;
spread: { bestAsk: number; bestBid: number; spread: number; pct: number };
orderbookPrevClose: number;
chartTicker: TickerData | undefined;
tradeFill: TradeOrderFillRequest | null;
paperCashBalance: number;
paperCoinQty: number;
paperTradingEnabled: boolean;
paperAutoTradeEnabled: boolean;
appDefaults: import('../utils/backendApi').AppSettingsDto;
stats: PriceStats | null;
priceMap: Record<string, { price: number; changePct: number }>;
alerts: Array<{ price: number; label: string }>;
hoverBar: LegendData | OHLCVBar | null;
quoteBar: OHLCVBar | null;
liveQuote: ReturnType<typeof import('../utils/chartLiveQuote').resolveChartLiveQuote>;
isLoading: boolean;
error: string | null;
chartConfigReady: boolean;
chartAreaLoading: boolean;
chartDataLoading: boolean;
bulkSettingsIndicators: IndicatorConfig[];
chartIndicatorsForSettings: IndicatorConfig[];
getParams: ReturnType<typeof import('../hooks/useIndicatorSettings').useIndicatorSettings>['getParams'];
getVisualConfig: ReturnType<typeof import('../hooks/useIndicatorSettings').useIndicatorSettings>['getVisualConfig'];
activeSlotIndicators: IndicatorConfig[];
setSymbol: React.Dispatch<React.SetStateAction<string>>;
setLegend: React.Dispatch<React.SetStateAction<LegendData | null>>;
setChartType: React.Dispatch<React.SetStateAction<ChartType>>;
setMode: React.Dispatch<React.SetStateAction<ChartMode>>;
setLogScale: React.Dispatch<React.SetStateAction<boolean>>;
setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
setDrawingTool: React.Dispatch<React.SetStateAction<string>>;
setDrawings: React.Dispatch<React.SetStateAction<Drawing[]>>;
setDrawingsLocked: React.Dispatch<React.SetStateAction<boolean>>;
setDrawingsVisible: React.Dispatch<React.SetStateAction<boolean>>;
setIndicatorsVisible: React.Dispatch<React.SetStateAction<boolean>>;
setMagnetMode: React.Dispatch<React.SetStateAction<'off' | 'weak' | 'strong'>>;
setSnapToIndicator: React.Dispatch<React.SetStateAction<boolean>>;
setKeepLockedOnDelete: React.Dispatch<React.SetStateAction<boolean>>;
setShowStats: React.Dispatch<React.SetStateAction<boolean>>;
setShowWatch: React.Dispatch<React.SetStateAction<boolean>>;
setShowAlert: React.Dispatch<React.SetStateAction<boolean>>;
setShowObjectTree: React.Dispatch<React.SetStateAction<boolean>>;
setMagnifierActive: React.Dispatch<React.SetStateAction<boolean>>;
setShowMarketPanel: React.Dispatch<React.SetStateAction<boolean>>;
setMobileRightOpen: React.Dispatch<React.SetStateAction<boolean>>;
setMobileRightTab: React.Dispatch<React.SetStateAction<'trade' | 'orderbook'>>;
setSyncTime: React.Dispatch<React.SetStateAction<number>>;
setSyncVisibleRange: React.Dispatch<React.SetStateAction<{ from: number; to: number } | null>>;
setSyncLogicalRange: React.Dispatch<React.SetStateAction<{ from: number; to: number } | null>>;
setActiveSlotSymbol: React.Dispatch<React.SetStateAction<string>>;
setCanUndo: React.Dispatch<React.SetStateAction<boolean>>;
setCanRedo: React.Dispatch<React.SetStateAction<boolean>>;
setCtxToolbar: React.Dispatch<React.SetStateAction<{ entryId: string; screenX: number; screenY: number } | null>>;
setDrawingCtxToolbar: React.Dispatch<React.SetStateAction<{ id: string; screenX: number; screenY: number } | null>>;
setDrawingSettingsId: React.Dispatch<React.SetStateAction<string | null>>;
setSelectedDrawingId: React.Dispatch<React.SetStateAction<string | null>>;
setShowMainChartModal: React.Dispatch<React.SetStateAction<boolean>>;
setShowBulkIndSettings: React.Dispatch<React.SetStateAction<boolean>>;
setSettingsModalId: React.Dispatch<React.SetStateAction<string | null>>;
setCustomOverlaySettingsSlot: React.Dispatch<React.SetStateAction<ChartCustomOverlaySlotId | null>>;
setShowBtSettings: React.Dispatch<React.SetStateAction<boolean>>;
setShowBtResult: React.Dispatch<React.SetStateAction<boolean>>;
setShowLivePanel: React.Dispatch<React.SetStateAction<boolean>>;
setWatchlist: React.Dispatch<React.SetStateAction<string[]>>;
setAlerts: React.Dispatch<React.SetStateAction<Array<{ price: number; label: string }>>>;
setIndicators: React.Dispatch<React.SetStateAction<IndicatorConfig[]>>;
setSyncOptions: React.Dispatch<React.SetStateAction<SyncOptions>>;
setPercentScale: React.Dispatch<React.SetStateAction<boolean>>;
setBtStrategyName: React.Dispatch<React.SetStateAction<string>>;
setBtReportCreatedAt: React.Dispatch<React.SetStateAction<string | undefined>>;
setBtSettings: React.Dispatch<React.SetStateAction<BacktestSettingsDto>>;
saveAppDef: (patch: Partial<import('../utils/backendApi').AppSettingsDto>) => void;
applyChartIndicators: (chartIndicators: IndicatorConfig[]) => void;
onThemeToggle: () => void;
handleTimeframe: (tf: Timeframe) => void;
handleLayoutSelect: (def: LayoutDef) => void;
handleSetActiveSlot: (idx: number) => void;
handleActiveSlotIndicatorsChange: (inds: IndicatorConfig[]) => void;
handleActiveSlotSymbolChange: (sym: string) => void;
handleVisibleRangeChange: (r: { from: number; to: number }) => void;
handleLogicalRangeChange: (r: { from: number; to: number }) => void;
handleSlotFullView: (slotSymbol: string, slotIdx: number) => void;
applyTradeFill: (req: { market: string; price: number; side: 'buy' | 'sell' }, slotIndex?: number) => void;
handleOrderbookPick: (price: number, rowType: 'ask' | 'bid') => void;
handleTradeMarketSelect: (s: string) => void;
pushDrawing: (newDrawings: Drawing[]) => void;
handleUndo: () => void;
handleRedo: () => void;
handleHoverPaneLegend: (id: string, sx: number, sy: number) => void;
handleLeavePaneLegend: () => void;
clearHoverTimer: () => void;
handleDrawingClick: (id: string | null, sx: number, sy: number) => void;
handleDrawingDoubleClick: (id: string, sx: number, sy: number) => void;
handleMainChartSave: (style: MainChartStyle) => void;
handleAddIndicator: (type: string, fromConfig?: IndicatorConfig) => void;
handleAddIndicators: (types: string[]) => void;
handleRemoveByType: (type: string) => void;
handleIndicatorListOrderChange: (orderedTypes: string[]) => void;
handleOpenIndicatorSettings: (type: string) => void;
handleIndicatorSave: (updated: IndicatorConfig) => void;
handleBulkIndicatorApply: (result: { chartIndicators: IndicatorConfig[]; allConfigs: IndicatorConfig[] }) => void;
handleToggleIndicatorHidden: (id: string) => void;
handleDuplicateIndicator: (sourceId: string) => void;
handleRemoveIndicatorById: (id: string) => void;
handleScreenshot: () => void;
handleReorderIndicators: (fromId: string, insertBeforeId: string | null) => void;
handleMergeIndicators: (fromId: string, intoId: string) => void;
handleSplitIndicatorPane: (hostId: string) => void;
handleExpandIndicator: (id: string) => void;
handleRestoreIndicators: () => void;
handleToggleCandleOverlay: (key: ChartOverlayToggleKey) => void;
handleTogglePriceExtremeLabels: () => void;
handleCustomOverlayActiveChange: (active: boolean) => void;
handleCustom1OverlayActiveChange: (active: boolean) => void;
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<ChartWorkspaceViewProps['btMarkerPayloadRef']['current']>, attempt?: number) => void;
btRun: ReturnType<typeof import('../hooks/useBacktest').useBacktest>['run'];
refreshPaperAccount: (forMarket?: string) => Promise<void>;
handlePaperOrderFilled: () => void;
handleLiveSettingsChange: (saved: LiveStrategySettingsDto) => void;
clearLiveMarkers: () => void;
goToMarketChart: (market: string) => void;
handleTimezoneChange: (tz: string) => void;
}
function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
const {
onOpenNotificationsPage,
symbol, timeframe, chartType, theme, mode, logScale, showGrid,
toolbarActiveIndicators, layoutDef, syncOptions, activeSlot,
activeSlotSymbol, activeSlotTf, slot0InitProps, slotInitProps,
syncTime, syncVisibleRange, syncLogicalRange, slotRefs, managerRef,
undoStack, redoStack, canUndo, canRedo, drawingTool, drawings,
drawingsLocked, drawingsVisible, indicatorsVisible, magnetMode,
snapToIndicator, keepLockedOnDelete, legend, showStats, showWatch,
showAlert, showObjectTree, percentScale, magnifierActive, showMarketPanel,
mobileRightOpen, mobileRightTab, marketTickers, marketInfos, marketLoading, usdRate,
refreshAllTickers, setMarketSearchOpen,
formalLogin, requireFormalLogin, chartVisible, menuPage, isMobile,
indicators, effectiveIndicators, mainChartStyle, showMainChartModal,
showBulkIndSettings, settingsModalId, ctxToolbar, drawingCtxToolbar,
drawingSettingsId, selectedDrawingId, focusedIndicatorId,
overlayVisibilityRef, candleOverlayToggles, customOverlaySelection,
custom1OverlaySelection, customOverlaySettingsSlot, customOverlayActive,
custom1OverlayActive, chartCandleAreaPriceLabels, chartSeriesPriceLabels,
chartVolumeVisible, chartPriceExtremeLabelsVisible, chartPaneSeparator, chartCrosshairInfoVisible,
chartHoverToolbarVisible, chartLiveReceiveHighlight, chartRealtimeSource,
displayTimezone, chartTimeFormat, chartLegendOptions, useUpbit, wsStatus, bars, barsMarket,
barsMarketRef, chartMountKey, chartLiveReadyRef, pendingRealtimeBarRef,
isSingleLoadingMore, singleLoadMoreRef, btMatchesChart, btRunning,
btSettings, btStats, btSignals, btClear, btMarkerPayloadRef,
syncBacktestMarkersRef, showBtSettings, showBtResult, showLivePanel,
liveStrategies, liveStrategySettings, monitoredMarkets, isVirtualActive,
watchlist, chartBtReportModel, chartSymbol, chartScreenActive, orderbook,
obWsStatus, spread, orderbookPrevClose, chartTicker, tradeFill,
paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled,
appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote,
isLoading, error, chartConfigReady, chartAreaLoading, chartDataLoading, bulkSettingsIndicators, chartIndicatorsForSettings,
getParams, getVisualConfig, activeSlotIndicators,
setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid,
setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible,
setIndicatorsVisible, setMagnetMode, setSnapToIndicator, setKeepLockedOnDelete,
setShowStats, setShowWatch, setShowAlert, setShowObjectTree, setMagnifierActive,
setShowMarketPanel, setMobileRightOpen, setMobileRightTab, setSyncTime,
setSyncVisibleRange, setSyncLogicalRange, setActiveSlotSymbol, setCanUndo,
setCanRedo, setCtxToolbar, setDrawingCtxToolbar, setDrawingSettingsId,
setSelectedDrawingId, setShowMainChartModal, setShowBulkIndSettings,
setSettingsModalId, setCustomOverlaySettingsSlot, setShowBtSettings,
setShowBtResult, setShowLivePanel, setWatchlist, setAlerts, setIndicators,
setSyncOptions, setPercentScale, setBtStrategyName, setBtReportCreatedAt, setBtSettings,
saveAppDef, applyChartIndicators,
onThemeToggle, handleTimeframe, handleLayoutSelect, handleSetActiveSlot,
handleActiveSlotIndicatorsChange, handleActiveSlotSymbolChange,
handleVisibleRangeChange, handleLogicalRangeChange, handleSlotFullView,
applyTradeFill, handleOrderbookPick, handleTradeMarketSelect, pushDrawing,
handleUndo, handleRedo, handleHoverPaneLegend, handleLeavePaneLegend,
clearHoverTimer, handleDrawingClick, handleDrawingDoubleClick,
handleMainChartSave, handleAddIndicator, handleAddIndicators,
handleRemoveByType, handleIndicatorListOrderChange, handleOpenIndicatorSettings,
handleIndicatorSave, handleBulkIndicatorApply, handleToggleIndicatorHidden,
handleDuplicateIndicator, handleRemoveIndicatorById, handleScreenshot,
handleReorderIndicators, handleMergeIndicators, handleSplitIndicatorPane,
handleExpandIndicator, handleRestoreIndicators, handleToggleCandleOverlay,
handleTogglePriceExtremeLabels,
handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange,
handleSaveCustomOverlaySelection, syncCustomOverlaysToManager,
handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange,
applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers,
handleTimezoneChange,
} = props;
return (
<>
<ChartToolbarNotify
onOpenNotifyList={onOpenNotificationsPage}
symbol={symbol} timeframe={layoutDef.count > 1 ? activeSlotTf : timeframe} chartType={chartType}
theme={theme} mode={mode} logScale={logScale} showGrid={showGrid}
activeIndicators={toolbarActiveIndicators}
onSymbol={s => {
if (layoutDef.count > 1) {
// 멀티 레이아웃: 활성 슬롯의 심볼 변경
slotRefs.current[activeSlot]?.setSymbol(s);
// symbol sync 활성화된 경우 모든 슬롯 일괄 변경
if (syncOptions.symbol) {
slotRefs.current.slice(0, layoutDef.count).forEach(r => r?.setSymbol(s));
}
} else {
setSymbol(s); setLegend(null);
}
}}
onTimeframe={handleTimeframe}
onChartType={setChartType}
onTheme={onThemeToggle}
onMode={() => setMode(m => m === 'chart' ? 'trading' : 'chart')}
onLogScale={() => setLogScale(v => !v)}
onToggleGrid={() => setShowGrid(v => !v)}
onFitContent={() => managerRef.current?.fitContent()}
onScrollToNow={() => managerRef.current?.scrollToRealTime()}
onAddIndicator={handleAddIndicator}
onAddIndicators={handleAddIndicators}
onIndicatorOrderChange={handleIndicatorListOrderChange}
onRemoveByType={handleRemoveByType}
onOpenBulkIndicatorSettings={() => setShowBulkIndSettings(true)}
onOpenIndicatorSettings={handleOpenIndicatorSettings}
onAutoFib={() => managerRef.current?.drawAutoFib(80)}
onClearFib={() => managerRef.current?.clearFib()}
onScreenshot={handleScreenshot}
onToggleStats={() => setShowStats(v => !v)}
onToggleWatch={() => setShowWatch(v => !v)}
onToggleAlert={() => setShowAlert(v => !v)}
magnetMode={magnetMode}
onToggleMagnet={() => setMagnetMode(m => m === 'off' ? 'strong' : 'off')}
onUndo={handleUndo}
onRedo={handleRedo}
canUndo={canUndo}
canRedo={canRedo}
onClearDrawings={() => {
// 모든 드로잉 삭제 + 피보나치 삭제 + undo/redo 스택 초기화
undoStack.current = [];
redoStack.current = [];
setCanUndo(false);
setCanRedo(false);
setDrawings([]);
managerRef.current?.clearFib();
managerRef.current?.clearDrawings();
}}
layoutId={layoutDef.id}
syncOptions={syncOptions}
onLayoutSelect={handleLayoutSelect}
onSyncChange={(key, val) => {
setSyncOptions(prev => {
const next = { ...prev, [key]: val };
saveAppDef({ syncOptions: next as unknown as Record<string, boolean> });
return next;
});
}}
magnifierActive={magnifierActive}
onToggleMagnifier={() => setMagnifierActive(v => !v)}
marketPanelActive={showMarketPanel}
onToggleMarketPanel={() => {
setShowMarketPanel(v => {
const next = !v;
if (isMobile && next) setMobileRightOpen(false);
return next;
});
}}
marketTickers={marketTickers}
marketInfos={marketInfos}
marketLoading={marketLoading}
refreshAllTickers={refreshAllTickers}
onMarketSearchOpenChange={setMarketSearchOpen}
backtestActive={btMatchesChart}
backtestRunning={btRunning}
backtestRunningId={undefined}
onRunBacktest={menuPage === 'chart' && layoutDef.count === 1 && formalLogin
? async (opts) => {
if (opts.strategyName) setBtStrategyName(opts.strategyName);
if (useUpbit && (isLoading || bars.length === 0)) return;
const runSymbol = symbol;
const runTf = timeframe;
const r = await btRun({
...opts,
bars,
timeframe: runTf,
settings: btSettings,
symbol: runSymbol,
});
if (
r &&
r.symbol === runSymbol &&
r.timeframe === runTf &&
r.signals.length > 0
) {
const showPrice = appDefaults.btShowPrice ?? false;
const payload = {
symbol: runSymbol,
timeframe: runTf,
signals: r.signals,
showPrice,
};
btMarkerPayloadRef.current = payload;
applyBacktestMarkersWhenReady(payload);
setBtReportCreatedAt(fmtListTimestamp(new Date().toISOString()));
}
if (appDefaults.btAutoPopup) setShowBtResult(true);
}
: undefined}
onOpenBtSettings={() => {
if (!formalLogin) { requireFormalLogin(); return; }
setShowBtSettings(true);
}}
onOpenBtResults={() => {
if (!formalLogin) { requireFormalLogin(); return; }
setShowBtResult(true);
}}
hasBtResult={btMatchesChart}
onClearBtMarkers={() => {
btMarkerPayloadRef.current = null;
btClear();
managerRef.current?.clearBacktestMarkers();
}}
onToggleLiveStrategy={() => {
if (!formalLogin) { requireFormalLogin(); return; }
setShowLivePanel(v => !v);
}}
liveStrategyActive={formalLogin && (showLivePanel || monitoredMarkets.length > 0)}
showChartQuote={useUpbit}
chartLiveQuote={liveQuote}
chartQuoteLoading={isLoading}
chartQuoteError={error}
priceExtremeLabelsVisible={chartPriceExtremeLabelsVisible}
onTogglePriceExtremeLabels={handleTogglePriceExtremeLabels}
tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })}
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
/>
{isMobile && (showMarketPanel || mobileRightOpen) && (
<button
type="button"
className="mobile-panel-backdrop"
aria-label="패널 닫기"
onClick={() => { setShowMarketPanel(false); setMobileRightOpen(false); }}
/>
)}
<div className="tv-main">
{/* ── 좌측 마켓 패널 ─────────────────────────────────────────── */}
<MarketPanel
open={showMarketPanel}
tickers={marketTickers}
marketInfos={marketInfos}
loading={marketLoading}
usdRate={usdRate}
refreshAllTickers={refreshAllTickers}
activeSymbol={layoutDef.count > 1 ? activeSlotSymbol : symbol}
onSymbolSelect={s => {
if (layoutDef.count > 1) {
// 다중차트: 현재 활성 슬롯에만 적용, 호가창 심볼도 즉시 갱신
slotRefs.current[activeSlot]?.setSymbol(s);
setActiveSlotSymbol(s);
} else {
setSymbol(s);
setLegend(null);
}
}}
/>
{/* 마켓 패널 접기/펼치기 핸들 – tv-main flex 자식으로 항상 표시 */}
<button
className={`mp-panel-handle ${showMarketPanel ? 'mp-panel-handle--open' : ''}`}
onClick={() => setShowMarketPanel(v => !v)}
title={showMarketPanel ? '마켓 패널 닫기' : '마켓 패널 열기'}
>
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
{showMarketPanel
? <polyline points="6,1 2,7 6,13" />
: <polyline points="2,1 6,7 2,13" />}
</svg>
</button>
<DrawingToolbar
activeTool={drawingTool} mode={mode}
onTool={setDrawingTool}
onClearAll={() => { setDrawings([]); managerRef.current?.clearDrawings(); }}
onUndo={() => setDrawings(prev => prev.slice(0, -1))}
locked={drawingsLocked}
visible={drawingsVisible}
onToggleLock={() => setDrawingsLocked(v => !v)}
onToggleEye={() => setDrawingsVisible(v => !v)}
indicatorsVisible={indicatorsVisible}
magnetMode={magnetMode}
snapToIndicator={snapToIndicator}
drawingCount={drawings.length}
indicatorCount={indicators.length}
onMagnetMode={setMagnetMode}
onSnapToIndicator={setSnapToIndicator}
onHideDrawings={() => setDrawingsVisible(v => !v)}
onHideIndicators={() => setIndicatorsVisible(v => !v)}
onHideAll={() => { setDrawingsVisible(false); setIndicatorsVisible(false); }}
onClearIndicators={() => setIndicators([])}
keepLockedOnDelete={keepLockedOnDelete}
onToggleKeepLocked={() => setKeepLockedOnDelete(v => !v)}
/>
{/* ── 멀티 레이아웃: 슬롯 2개 이상일 때 별도 그리드 렌더 ─────────── */}
{layoutDef.count > 1 && (
<div
className="multi-chart-grid"
style={{
gridTemplateColumns: layoutDef.cols,
gridTemplateRows: layoutDef.rows,
gridTemplateAreas: layoutDef.areas,
position: 'relative',
}}
>
{chartAreaLoading && (
<div className="chart-loading chart-loading--chart-area chart-loading--workspace" role="status" aria-live="polite">
<div className="loading-spinner" />
<span>차트 로딩중...</span>
</div>
)}
{/* 슬롯 0 = 메인 차트 영역 (a) */}
<div className="multi-slot-wrap" style={{ gridArea: 'a', minHeight: 0, minWidth: 0 }}>
<ChartSlot
ref={el => { slotRefs.current[0] = el; }}
slotIndex={0}
theme={theme}
active={activeSlot === 0}
onActivate={() => handleSetActiveSlot(0)}
initialSymbol={slot0InitProps?.symbol}
initialTimeframe={slot0InitProps?.timeframe}
initialIndicators={slot0InitProps?.indicators}
forcedSymbol={syncOptions.symbol ? symbol : undefined}
forcedTf={syncOptions.interval ? timeframe : undefined}
syncTime={syncOptions.crosshair ? syncTime : undefined}
onCrosshairTime={t => { if (syncOptions.crosshair) setSyncTime(t); }}
syncVisibleRange={syncOptions.time ? syncVisibleRange : undefined}
onVisibleRangeChange={handleVisibleRangeChange}
syncLogicalRange={syncOptions.dateRange ? syncLogicalRange : undefined}
onLogicalRangeChange={handleLogicalRangeChange}
onFullView={sym => handleSlotFullView(sym, 0)}
magnifierEnabled={magnifierActive && activeSlot === 0}
onMagnifierClose={() => setMagnifierActive(false)}
onIndicatorsChange={activeSlot === 0 ? handleActiveSlotIndicatorsChange : undefined}
onSymbolChange={activeSlot === 0 ? handleActiveSlotSymbolChange : undefined}
magnetMode={magnetMode}
onTradeOrderRequest={(req, idx) => applyTradeFill(req, idx)}
chartCandleAreaPriceLabels={chartCandleAreaPriceLabels}
chartSeriesPriceLabels={chartSeriesPriceLabels}
chartVolumeVisible={chartVolumeVisible}
chartPaneSeparator={chartPaneSeparator}
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
chartHoverToolbarVisible={chartHoverToolbarVisible}
chartPriceExtremeLabelsVisible={chartPriceExtremeLabelsVisible}
onTogglePriceExtremeLabels={handleTogglePriceExtremeLabels}
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
displayTimezone={displayTimezone}
marketTickers={marketTickers}
marketInfos={marketInfos}
marketLoading={marketLoading}
refreshAllTickers={refreshAllTickers}
onMarketSearchOpenChange={setMarketSearchOpen}
compactMode
chartVisible={chartVisible}
chartConfigReady={chartConfigReady}
onPaintReadyChange={handleSlotPaintReadyChange}
/>
</div>
{/* 슬롯 1~7 */}
{(['b','c','d','e','f','g','h'] as const)
.slice(0, layoutDef.count - 1)
.map((area, i) => (
<div key={area} className="multi-slot-wrap" style={{ gridArea: area, minHeight: 0, minWidth: 0 }}>
<ChartSlot
ref={el => { slotRefs.current[i + 1] = el; }}
slotIndex={i + 1}
theme={theme}
active={activeSlot === i + 1}
onActivate={() => handleSetActiveSlot(i + 1)}
initialSymbol={slotInitProps[i + 1]?.symbol}
initialTimeframe={slotInitProps[i + 1]?.timeframe}
initialIndicators={slotInitProps[i + 1]?.indicators}
forcedSymbol={syncOptions.symbol ? symbol : undefined}
forcedTf={syncOptions.interval ? timeframe : undefined}
syncTime={syncOptions.crosshair ? syncTime : undefined}
onCrosshairTime={t => { if (syncOptions.crosshair) setSyncTime(t); }}
syncVisibleRange={syncOptions.time ? syncVisibleRange : undefined}
onVisibleRangeChange={handleVisibleRangeChange}
syncLogicalRange={syncOptions.dateRange ? syncLogicalRange : undefined}
onLogicalRangeChange={handleLogicalRangeChange}
onFullView={sym => handleSlotFullView(sym, i + 1)}
magnifierEnabled={magnifierActive && activeSlot === i + 1}
onMagnifierClose={() => setMagnifierActive(false)}
onIndicatorsChange={activeSlot === i + 1 ? handleActiveSlotIndicatorsChange : undefined}
onSymbolChange={activeSlot === i + 1 ? handleActiveSlotSymbolChange : undefined}
magnetMode={magnetMode}
onTradeOrderRequest={(req, idx) => applyTradeFill(req, idx)}
chartCandleAreaPriceLabels={chartCandleAreaPriceLabels}
chartSeriesPriceLabels={chartSeriesPriceLabels}
chartVolumeVisible={chartVolumeVisible}
chartPaneSeparator={chartPaneSeparator}
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
chartHoverToolbarVisible={chartHoverToolbarVisible}
chartPriceExtremeLabelsVisible={chartPriceExtremeLabelsVisible}
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
displayTimezone={displayTimezone}
marketTickers={marketTickers}
marketInfos={marketInfos}
marketLoading={marketLoading}
refreshAllTickers={refreshAllTickers}
onMarketSearchOpenChange={setMarketSearchOpen}
compactMode
chartVisible={chartVisible}
chartConfigReady={chartConfigReady}
onPaintReadyChange={handleSlotPaintReadyChange}
/>
</div>
))}
</div>
)}
{/* ── 싱글 레이아웃 (기존) ─────────────────────────────────────── */}
<div className="tv-chart-col" style={{ display: layoutDef.count > 1 ? 'none' : undefined, position: 'relative' }}>
{chartAreaLoading && (
<div className="chart-loading chart-loading--chart-area chart-loading--workspace" role="status" aria-live="polite">
<div className="loading-spinner" />
<span>차트 로딩중...</span>
</div>
)}
<ChartLegendBar
visibility={chartLegendOptions}
crosshairInfoVisible={chartCrosshairInfoVisible}
symbol={symbol}
timeframe={timeframe}
useUpbit={useUpbit}
wsStatus={wsStatus}
mode={mode}
currentBar={hoverBar}
liveQuote={liveQuote}
indicators={indicators}
legend={legend}
/>
<TradingChart
key={chartMountKey}
chartVisible={chartVisible}
dataLoading={chartDataLoading}
onChartPaintReadyChange={handleSingleChartPaintReadyChange}
bars={bars} barsMarket={barsMarket} market={symbol} timeframe={timeframe} displayTimezone={displayTimezone}
chartType={chartType} theme={theme} mode={mode}
indicators={effectiveIndicators} drawingTool={drawingTool}
drawings={drawings} logScale={logScale}
drawingsLocked={drawingsLocked}
drawingsVisible={drawingsVisible}
crosshairInfoVisible={chartCrosshairInfoVisible}
onCrosshair={setLegend}
onTradeOrderRequest={applyTradeFill}
onDataLoaded={() => {
const mgr = managerRef.current;
mgr?.setChartOverlayVisibility(overlayVisibilityRef.current);
if (mgr) syncCustomOverlaysToManager(mgr);
syncBacktestMarkersRef.current();
}}
onCandlesReady={handleCandlesReady}
onManagerReady={mgr => {
managerRef.current = mgr;
const pending = pendingRealtimeBarRef.current;
if (pending && chartLiveReadyRef.current && barsMarketRef.current === symbol && bars.length > 0) {
pendingRealtimeBarRef.current = null;
const last = bars[bars.length - 1];
if (last && pending.time === last.time) mgr.updateBar(pending);
else if (!last || pending.time > last.time) void mgr.appendBar(pending);
else mgr.updateBar({ ...pending, time: last.time });
} else if (chartLiveReadyRef.current) {
pendingRealtimeBarRef.current = null;
}
mgr.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels);
mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels);
const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null;
mgr.setVolumeVisible(chartVolumeVisible, wrapper?.clientHeight);
mgr.setPriceExtremeLabelsVisible(chartPriceExtremeLabelsVisible);
mgr.setPaneSeparatorOptions(chartPaneSeparator);
mgr.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
syncCustomOverlaysToManager(mgr);
syncBacktestMarkersRef.current();
// 왼쪽 스크롤 감지 → 과거 데이터 추가 로드
mgr.subscribeVisibleLogicalRange(r => {
if (r && r.from < LOAD_MORE_TRIGGER) {
singleLoadMoreRef.current();
}
});
}}
onAddDrawing={d => pushDrawing([...drawings, d])}
onSeriesClick={(entryId, point) => {
if (!entryId) { setCtxToolbar(null); return; }
setCtxToolbar({ entryId, screenX: point.x, screenY: point.y });
setDrawingCtxToolbar(null); // 드로잉 툴바 닫기
}}
onSeriesDoubleClick={(entryId) => {
setCtxToolbar(null);
if (!entryId) return;
if (entryId === '__main__') {
setShowMainChartModal(true);
} else {
setSettingsModalId(entryId);
}
}}
onHoverPaneLegend={handleHoverPaneLegend}
onLeavePaneLegend={handleLeavePaneLegend}
selectedDrawingId={selectedDrawingId}
onDrawingClick={handleDrawingClick}
onDrawingDoubleClick={handleDrawingDoubleClick}
magnifierEnabled={magnifierActive}
onMagnifierClose={() => setMagnifierActive(false)}
onReorderIndicators={handleReorderIndicators}
onMergeIndicators={handleMergeIndicators}
onSplitIndicatorPane={handleSplitIndicatorPane}
onExpandIndicator={handleExpandIndicator}
onRemoveIndicator={handleRemoveIndicatorById}
onDuplicateIndicator={handleDuplicateIndicator}
focusedIndicatorId={focusedIndicatorId}
onRestoreIndicators={handleRestoreIndicators}
onToggleIndicatorHidden={handleToggleIndicatorHidden}
onOpenIndicatorSettings={id => setSettingsModalId(id)}
volumeVisible={chartVolumeVisible}
candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels}
indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels}
paneSeparatorOptions={chartPaneSeparator}
candleOverlayToggles={candleOverlayToggles}
onToggleCandleOverlay={handleToggleCandleOverlay}
priceExtremeLabelsVisible={chartPriceExtremeLabelsVisible}
onTogglePriceExtremeLabels={handleTogglePriceExtremeLabels}
customOverlayActive={customOverlayActive}
onCustomOverlayActiveChange={handleCustomOverlayActiveChange}
onOpenCustomOverlaySettings={() => setCustomOverlaySettingsSlot('custom')}
custom1OverlayActive={custom1OverlayActive}
onCustom1OverlayActiveChange={handleCustom1OverlayActiveChange}
onOpenCustom1OverlaySettings={() => setCustomOverlaySettingsSlot('custom1')}
showHoverToolbar={chartHoverToolbarVisible}
/>
{customOverlaySettingsSlot === 'custom' && (
<ChartCustomOverlaySettingsModal
title="Custom 표시 설정"
selection={customOverlaySelection}
onSave={sel => handleSaveCustomOverlaySelection('custom', sel)}
onCancel={() => setCustomOverlaySettingsSlot(null)}
smaParams={indicators.find(i => i.type === 'SMA')?.params}
/>
)}
{customOverlaySettingsSlot === 'custom1' && (
<ChartCustomOverlaySettingsModal
title="Custom1 표시 설정"
selection={custom1OverlaySelection}
onSave={sel => handleSaveCustomOverlaySelection('custom1', sel)}
onCancel={() => setCustomOverlaySettingsSlot(null)}
smaParams={indicators.find(i => i.type === 'SMA')?.params}
/>
)}
{isSingleLoadingMore && (
<div className="chart-history-loading">
<div className="loading-spinner" style={{ width: 14, height: 14 }} />
<span>과거 데이터 로딩...</span>
</div>
)}
{/* 백테스팅 결과 통계 배지 */}
{btMatchesChart && layoutDef.count === 1 && (
<BacktestPanel
stats={btStats}
signalCount={btSignals.length}
onClear={() => {
btMarkerPayloadRef.current = null;
btClear();
managerRef.current?.clearBacktestMarkers();
}}
/>
)}
{/* 실시간 전략 체크 패널 */}
{showLivePanel && menuPage === 'chart' && layoutDef.count === 1 && (
<LiveStrategyPanel
theme={theme}
market={symbol}
strategies={liveStrategies}
settings={liveStrategySettings}
watchlistCount={isVirtualActive ? monitoredMarkets.length : watchlist.length}
monitoredMarkets={monitoredMarkets}
virtualDriven={isVirtualActive}
onClearMarkers={() => { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }}
onSettingsChange={handleLiveSettingsChange}
onClose={() => setShowLivePanel(false)}
/>
)}
{/* 백테스팅 설정 모달 */}
{showBtSettings && (
<BacktestSettingsModal
onClose={() => setShowBtSettings(false)}
onSettingsChange={s => { setBtSettings(s); setShowBtSettings(false); }}
/>
)}
{/* 백테스팅 결과 — 투자분석 상세 레포트 (인쇄·PDF) */}
<BacktestAnalysisReportModal
open={showBtResult && chartBtReportModel != null}
onClose={() => setShowBtResult(false)}
model={chartBtReportModel}
/>
{/* 시리즈 클릭/호버 컨텍스트 툴바 */}
{ctxToolbar && (() => {
const cfg = ctxToolbar.entryId !== '__main__'
? indicators.find(i => i.id === ctxToolbar.entryId) ?? null
: null;
return (
<IndicatorContextToolbar
indicatorId={ctxToolbar.entryId}
config={cfg}
chartType={chartType}
screenX={ctxToolbar.screenX}
screenY={ctxToolbar.screenY}
onMouseEnterToolbar={clearHoverTimer}
onMouseLeaveToolbar={handleLeavePaneLegend}
onSettings={() => {
const id = ctxToolbar.entryId;
setCtxToolbar(null);
if (id === '__main__') {
setShowMainChartModal(true);
} else {
setSettingsModalId(id);
}
}}
onToggleHidden={() => {
handleToggleIndicatorHidden(ctxToolbar.entryId);
}}
onRemove={() => handleRemoveIndicatorById(ctxToolbar.entryId)}
onClose={() => setCtxToolbar(null)}
/>
);
})()}
{/* 드로잉 컨텍스트 툴바 */}
{drawingCtxToolbar && (() => {
const d = drawings.find(dr => dr.id === drawingCtxToolbar.id);
if (!d) return null;
return (
<DrawingContextToolbar
drawing={d}
screenX={drawingCtxToolbar.screenX}
screenY={drawingCtxToolbar.screenY}
onSettings={() => {
setDrawingCtxToolbar(null);
setDrawingSettingsId(d.id);
}}
onToggleVisible={() => {
setDrawings(prev =>
prev.map(dr =>
dr.id === d.id
? { ...dr, visible: dr.visible !== false ? false : true }
: dr
)
);
setDrawingCtxToolbar(null);
}}
onRemove={() => {
pushDrawing(drawings.filter(dr => dr.id !== d.id));
setDrawingCtxToolbar(null);
setSelectedDrawingId(null);
}}
onClose={() => {
setDrawingCtxToolbar(null);
setSelectedDrawingId(null);
}}
/>
);
})()}
{/* 드로잉 설정 모달 */}
{drawingSettingsId && (() => {
const d = drawings.find(dr => dr.id === drawingSettingsId);
if (!d) return null;
const handleSaveDrawing = (updated: typeof d) => {
setDrawings(prev => prev.map(dr => dr.id === updated!.id ? updated! : dr));
setDrawingSettingsId(null);
setSelectedDrawingId(updated!.id);
};
const handleCancelDrawing = () => { setDrawingSettingsId(null); };
if (d.type === 'fibtz') {
return (
<FibTZSettingsModal
drawing={d}
onSave={handleSaveDrawing}
onCancel={handleCancelDrawing}
/>
);
}
return (
<DrawingSettingsModal
drawing={d}
onSave={updated => {
setDrawings(prev => prev.map(dr => dr.id === updated.id ? updated : dr));
setDrawingSettingsId(null);
setSelectedDrawingId(updated.id);
}}
onCancel={() => {
setDrawingSettingsId(null);
}}
/>
);
})()}
{/* 메인 차트 설정 모달 */}
{showMainChartModal && (
<MainChartSettingsModal
style={mainChartStyle}
onSave={handleMainChartSave}
onCancel={() => setShowMainChartModal(false)}
/>
)}
{/* 보조지표 일괄 설정 */}
{showBulkIndSettings && (
<IndicatorBulkSettingsModal
open={showBulkIndSettings}
indicators={bulkSettingsIndicators}
chartMarket={symbol}
getParams={getParams}
getVisualConfig={getVisualConfig}
onApply={handleBulkIndicatorApply}
onLiveChartChange={applyChartIndicators}
onClose={() => setShowBulkIndSettings(false)}
/>
)}
{/* 인디케이터 설정 모달 */}
{settingsModalId && settingsModalId !== '__main__' && (() => {
let cfg: IndicatorConfig | null = null;
if (isIndicatorSettingsTemplateId(settingsModalId)) {
const type = indicatorTypeFromSettingsId(settingsModalId);
if (!type) return null;
cfg = buildMainIndicatorConfig(
type,
chartIndicatorsForSettings,
getParams,
getVisualConfig,
);
} else {
const onChart = chartIndicatorsForSettings.find(i => i.id === settingsModalId);
if (onChart) {
cfg = initializeIndicatorConfigForEditor(onChart, getParams, getVisualConfig);
}
}
if (!cfg) return null;
return (
<IndicatorSettingsModal
config={cfg}
chartMarket={symbol}
onSave={handleIndicatorSave}
onCancel={() => setSettingsModalId(null)}
/>
);
})()}
</div>
{/* ── 우측 슬라이드 패널 (매수/매도 + 호가) ───────────────────── */}
<RightSidePanel
open={isMobile ? mobileRightOpen : undefined}
onOpenChange={isMobile ? setMobileRightOpen : undefined}
activeTab={isMobile ? mobileRightTab : undefined}
onTabChange={isMobile ? setMobileRightTab : undefined}
wsFeedEnabled={chartScreenActive && useUpbit}
market={chartSymbol}
tradePrice={chartTicker?.tradePrice ?? null}
asks={orderbook.asks}
bids={orderbook.bids}
totalAskSize={orderbook.totalAskSize}
totalBidSize={orderbook.totalBidSize}
wsStatus={obWsStatus}
bestAsk={spread.bestAsk}
bestBid={spread.bestBid}
spread={spread.spread}
spreadPct={spread.pct}
prevClose={orderbookPrevClose}
tickerInfo={chartTicker ? {
tradePrice: chartTicker.tradePrice,
changeRate: chartTicker.changeRate,
changePrice: chartTicker.changePrice,
accTradePrice24: chartTicker.accTradePrice24,
accTradeVolume24: chartTicker.accTradeVolume24,
highPrice: chartTicker.highPrice,
lowPrice: chartTicker.lowPrice,
openingPrice: chartTicker.openingPrice,
} : undefined}
fillRequest={tradeFill}
onOrderbookPick={handleOrderbookPick}
onMarketSelect={handleTradeMarketSelect}
availableKrw={paperCashBalance}
availableCoinQty={paperCoinQty}
paperTradingEnabled={formalLogin && paperTradingEnabled}
paperAutoTradeEnabled={formalLogin && paperAutoTradeEnabled}
onPaperOrderFilled={() => {
refreshPaperAccount(symbol);
handlePaperOrderFilled();
}}
/>
</div>
{/* ── 플로팅 패널: position:fixed로 항상 표시 (싱글/멀티 모드 공통) ─── */}
{showObjectTree && (
<ObjectTree
drawings={drawings}
onToggleVisible={id => setDrawings(prev =>
prev.map(d => d.id === id ? { ...d, visible: d.visible !== false ? false : true } : d)
)}
onRemove={id => setDrawings(prev => prev.filter(d => d.id !== id))}
onClose={() => setShowObjectTree(false)}
/>
)}
{showStats && <StatsPanel stats={stats} symbol={symbol} onClose={() => setShowStats(false)} />}
{showWatch && (
<WatchList
watchlist={watchlist} currentSymbol={symbol} priceMap={priceMap}
onSelect={s => { setSymbol(s); setShowWatch(false); }}
onAdd={s => {
setWatchlist(prev => [...new Set([...prev, s])]);
addWatchlistItem({ symbol: s }).catch(() => {});
}}
onRemove={s => {
setWatchlist(prev => prev.filter(x => x !== s));
removeWatchlistItem(s).catch(() => {});
}}
onClose={() => setShowWatch(false)}
/>
)}
{showAlert && (
<AlertManager
alerts={alerts} currentPrice={liveQuote.price ?? quoteBar?.close ?? 0}
onAdd={(price, label) => { managerRef.current?.addAlert(price, label); setAlerts(prev => [...prev, { price, label }]); }}
onRemove={price => { managerRef.current?.removeAlert(price); setAlerts(prev => prev.filter(a => Math.abs(a.price - price) > 0.0001)); }}
onClose={() => setShowAlert(false)}
/>
)}
{isMobile && (
<MobileChartDock
marketOpen={showMarketPanel}
rightOpen={mobileRightOpen}
rightTab={mobileRightTab}
drawingTool={drawingTool}
onToggleMarket={() => {
setShowMarketPanel(v => {
const next = !v;
if (next) setMobileRightOpen(false);
return next;
});
}}
onOpenTrade={() => {
setShowMarketPanel(false);
setMobileRightTab('trade');
setMobileRightOpen(true);
}}
onOpenOrderbook={() => {
setShowMarketPanel(false);
setMobileRightTab('orderbook');
setMobileRightOpen(true);
}}
onClosePanels={() => {
setShowMarketPanel(false);
setMobileRightOpen(false);
}}
onTool={setDrawingTool}
/>
)}
<BottomBar
timeframe={layoutDef.count > 1 ? activeSlotTf : timeframe}
logScale={logScale}
percentScale={percentScale} showGrid={showGrid}
lastTime={bars[bars.length - 1]?.time ?? 0}
displayTimezone={displayTimezone}
onTimezoneChange={handleTimezoneChange}
onTimeframe={handleTimeframe}
onLogScale={() => setLogScale(v => !v)}
onPercentScale={() => setPercentScale(v => !v)}
onAutoScale={() => managerRef.current?.autoScale()}
onFitContent={() => managerRef.current?.fitContent()}
onToggleGrid={() => setShowGrid(v => !v)}
onAddIndicators={handleAddIndicators}
onAddIndicator={handleAddIndicator}
/>
</>
);
}
export const ChartWorkspaceView = React.memo(ChartWorkspaceViewInner);