캔들차트, 이동평균선, 일목균형표, 볼린저밴드 보이기 숨기기 기능적용
This commit is contained in:
@@ -122,6 +122,11 @@ public class GcAppSettings {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Boolean chartCrosshairInfoVisible = true;
|
private Boolean chartCrosshairInfoVisible = true;
|
||||||
|
|
||||||
|
/** 차트 마우스오버 줌·이동 플로팅 툴바(− + ⌐↔ ‹ ›) 표시 (기본 true) */
|
||||||
|
@Column(name = "chart_hover_toolbar_visible", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean chartHoverToolbarVisible = true;
|
||||||
|
|
||||||
/** 차트 상단 범례(tv-legend) 항목별 표시 옵션 JSON */
|
/** 차트 상단 범례(tv-legend) 항목별 표시 옵션 JSON */
|
||||||
@Column(name = "chart_legend_options_json", columnDefinition = "JSON")
|
@Column(name = "chart_legend_options_json", columnDefinition = "JSON")
|
||||||
@JdbcTypeCode(SqlTypes.JSON)
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ public class AppSettingsService {
|
|||||||
Boolean.parseBoolean(d.get("chartLiveReceiveHighlight").toString()));
|
Boolean.parseBoolean(d.get("chartLiveReceiveHighlight").toString()));
|
||||||
if (d.containsKey("chartCrosshairInfoVisible")) s.setChartCrosshairInfoVisible(
|
if (d.containsKey("chartCrosshairInfoVisible")) s.setChartCrosshairInfoVisible(
|
||||||
Boolean.parseBoolean(d.get("chartCrosshairInfoVisible").toString()));
|
Boolean.parseBoolean(d.get("chartCrosshairInfoVisible").toString()));
|
||||||
|
if (d.containsKey("chartHoverToolbarVisible")) s.setChartHoverToolbarVisible(
|
||||||
|
Boolean.parseBoolean(d.get("chartHoverToolbarVisible").toString()));
|
||||||
if (d.containsKey("chartLegendOptions")) s.setChartLegendOptionsJson(toJson(d.get("chartLegendOptions")));
|
if (d.containsKey("chartLegendOptions")) s.setChartLegendOptionsJson(toJson(d.get("chartLegendOptions")));
|
||||||
if (d.containsKey("chartPaneSeparator")) s.setChartPaneSeparatorJson(toJson(d.get("chartPaneSeparator")));
|
if (d.containsKey("chartPaneSeparator")) s.setChartPaneSeparatorJson(toJson(d.get("chartPaneSeparator")));
|
||||||
if (d.containsKey("tradeAlertPopup")) s.setTradeAlertPopup(
|
if (d.containsKey("tradeAlertPopup")) s.setTradeAlertPopup(
|
||||||
@@ -205,6 +207,7 @@ public class AppSettingsService {
|
|||||||
m.put("chartVolumeVisible", s.getChartVolumeVisible() != null ? s.getChartVolumeVisible() : true);
|
m.put("chartVolumeVisible", s.getChartVolumeVisible() != null ? s.getChartVolumeVisible() : true);
|
||||||
m.put("chartLiveReceiveHighlight", s.getChartLiveReceiveHighlight() != null ? s.getChartLiveReceiveHighlight() : true);
|
m.put("chartLiveReceiveHighlight", s.getChartLiveReceiveHighlight() != null ? s.getChartLiveReceiveHighlight() : true);
|
||||||
m.put("chartCrosshairInfoVisible", s.getChartCrosshairInfoVisible() != null ? s.getChartCrosshairInfoVisible() : true);
|
m.put("chartCrosshairInfoVisible", s.getChartCrosshairInfoVisible() != null ? s.getChartCrosshairInfoVisible() : true);
|
||||||
|
m.put("chartHoverToolbarVisible", s.getChartHoverToolbarVisible() != null ? s.getChartHoverToolbarVisible() : true);
|
||||||
m.put("chartLegendOptions", parseJson(s.getChartLegendOptionsJson()));
|
m.put("chartLegendOptions", parseJson(s.getChartLegendOptionsJson()));
|
||||||
m.put("chartPaneSeparator", parseJson(s.getChartPaneSeparatorJson()));
|
m.put("chartPaneSeparator", parseJson(s.getChartPaneSeparatorJson()));
|
||||||
m.put("tradeAlertPopup", s.getTradeAlertPopup() != null ? s.getTradeAlertPopup() : true);
|
m.put("tradeAlertPopup", s.getTradeAlertPopup() != null ? s.getTradeAlertPopup() : true);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE gc_app_settings
|
||||||
|
ADD COLUMN chart_hover_toolbar_visible BOOLEAN NOT NULL DEFAULT TRUE
|
||||||
|
COMMENT '차트 마우스오버 줌·이동 플로팅 툴바 표시';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- V57 이 flyway_schema_history 에만 기록되고 컬럼이 없는 DB 복구
|
||||||
|
SET @col_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'gc_app_settings'
|
||||||
|
AND COLUMN_NAME = 'chart_hover_toolbar_visible'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @ddl := IF(
|
||||||
|
@col_exists = 0,
|
||||||
|
'ALTER TABLE gc_app_settings ADD COLUMN chart_hover_toolbar_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''차트 마우스오버 줌·이동 플로팅 툴바 표시''',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -2540,6 +2540,22 @@ html.theme-blue {
|
|||||||
right: 0;
|
right: 0;
|
||||||
left: auto;
|
left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 캔들 pane — 오버레이 표시 토글 아이콘 (이평·볼린저·일목·캔들) */
|
||||||
|
.chart-right-toolbar-cluster--overlay-icons {
|
||||||
|
z-index: 5;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.chart-right-toolbar-btn--overlay {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
.chart-right-toolbar-btn--overlay-off {
|
||||||
|
opacity: 0.38;
|
||||||
|
color: var(--text-muted, #787b86);
|
||||||
|
}
|
||||||
|
.chart-right-toolbar-btn--overlay-off:hover {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
.chart-right-toolbar-btn--restore.active {
|
.chart-right-toolbar-btn--restore.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: rgba(122, 162, 247, 0.65);
|
border-color: rgba(122, 162, 247, 0.65);
|
||||||
|
|||||||
+53
-1
@@ -47,6 +47,13 @@ import {
|
|||||||
} from './utils/indicatorMainConfig';
|
} from './utils/indicatorMainConfig';
|
||||||
import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from './utils/smaConfig';
|
import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from './utils/smaConfig';
|
||||||
import { normalizeIchimokuConfig } from './utils/ichimokuConfig';
|
import { normalizeIchimokuConfig } from './utils/ichimokuConfig';
|
||||||
|
import {
|
||||||
|
type ChartOverlayToggleKey,
|
||||||
|
type ChartOverlayVisibility,
|
||||||
|
chartOverlayKeyForType,
|
||||||
|
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||||
|
listChartOverlayToggleItems,
|
||||||
|
} from './utils/chartOverlayVisibility';
|
||||||
import { isUpbitMarket, UPBIT_MARKETS } from './utils/upbitApi';
|
import { isUpbitMarket, UPBIT_MARKETS } from './utils/upbitApi';
|
||||||
import { getFavorites, saveFavorites, initFavoritesFromDb, initHoldingsFromDb, FAVORITES_CHANGED_EVENT } from './utils/marketStorage';
|
import { getFavorites, saveFavorites, initFavoritesFromDb, initHoldingsFromDb, FAVORITES_CHANGED_EVENT } from './utils/marketStorage';
|
||||||
import { useChartRealtimeData, type WsStatus } from './hooks/useChartRealtimeData';
|
import { useChartRealtimeData, type WsStatus } from './hooks/useChartRealtimeData';
|
||||||
@@ -215,6 +222,8 @@ function App() {
|
|||||||
const [mode, setMode] = useState<ChartMode>('chart');
|
const [mode, setMode] = useState<ChartMode>('chart');
|
||||||
const [logScale, setLogScale] = useState(false);
|
const [logScale, setLogScale] = useState(false);
|
||||||
const [indicators, setIndicators] = useState<IndicatorConfig[]>(initial.indicators ?? []);
|
const [indicators, setIndicators] = useState<IndicatorConfig[]>(initial.indicators ?? []);
|
||||||
|
const indicatorsRef = useRef(indicators);
|
||||||
|
useEffect(() => { indicatorsRef.current = indicators; }, [indicators]);
|
||||||
const [drawingTool, setDrawingTool] = useState('cursor');
|
const [drawingTool, setDrawingTool] = useState('cursor');
|
||||||
const [drawings, setDrawings] = useState<Drawing[]>([]);
|
const [drawings, setDrawings] = useState<Drawing[]>([]);
|
||||||
const [drawingsLocked, setDrawingsLocked] = useState(false);
|
const [drawingsLocked, setDrawingsLocked] = useState(false);
|
||||||
@@ -668,6 +677,7 @@ function App() {
|
|||||||
const chartLiveReceiveHighlight = appDefaults.chartLiveReceiveHighlight ?? true;
|
const chartLiveReceiveHighlight = appDefaults.chartLiveReceiveHighlight ?? true;
|
||||||
const chartLegendOptions = appDefaults.chartLegendOptions;
|
const chartLegendOptions = appDefaults.chartLegendOptions;
|
||||||
const chartCrosshairInfoVisible = appDefaults.chartCrosshairInfoVisible ?? true;
|
const chartCrosshairInfoVisible = appDefaults.chartCrosshairInfoVisible ?? true;
|
||||||
|
const chartHoverToolbarVisible = appDefaults.chartHoverToolbarVisible ?? true;
|
||||||
const chartPaneSeparator = appDefaults.chartPaneSeparator;
|
const chartPaneSeparator = appDefaults.chartPaneSeparator;
|
||||||
const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
|
const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
|
||||||
const paperAutoTradeEnabled = appDefaults.paperAutoTradeEnabled ?? false;
|
const paperAutoTradeEnabled = appDefaults.paperAutoTradeEnabled ?? false;
|
||||||
@@ -1432,6 +1442,37 @@ function App() {
|
|||||||
setFocusedIndicatorId(null);
|
setFocusedIndicatorId(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [overlayVisibility, setOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||||
|
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
||||||
|
);
|
||||||
|
const overlayVisibilityRef = useRef(overlayVisibility);
|
||||||
|
useEffect(() => {
|
||||||
|
overlayVisibilityRef.current = overlayVisibility;
|
||||||
|
}, [overlayVisibility]);
|
||||||
|
|
||||||
|
const candleOverlayToggles = useMemo(
|
||||||
|
() => listChartOverlayToggleItems(overlayVisibility),
|
||||||
|
[overlayVisibility],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
||||||
|
setOverlayVisibility(prev => {
|
||||||
|
const next = { ...prev, [key]: !prev[key] };
|
||||||
|
const mgr = managerRef.current;
|
||||||
|
mgr?.setChartOverlayGroupVisible(key, next[key]);
|
||||||
|
if (mgr) {
|
||||||
|
for (const info of mgr.getIndicatorPaneInfo()) {
|
||||||
|
if (chartOverlayKeyForType(info.type) !== key) continue;
|
||||||
|
const reactInd = indicatorsRef.current.find(i => i.id === info.id);
|
||||||
|
mgr.applyIndicatorStyle(reactInd
|
||||||
|
? { ...info.config, ...reactInd, id: info.id, type: info.type }
|
||||||
|
: info.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 보조지표 순서 변경 (드래그 핸들): fromId 를 indicators 배열에서 이동.
|
* 보조지표 순서 변경 (드래그 핸들): fromId 를 indicators 배열에서 이동.
|
||||||
* overlay/markers 지표는 건드리지 않고 비오버레이 지표 위치만 변경됨.
|
* overlay/markers 지표는 건드리지 않고 비오버레이 지표 위치만 변경됨.
|
||||||
@@ -2007,6 +2048,8 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
||||||
onChartCrosshairInfoVisible={v => saveAppDef({ chartCrosshairInfoVisible: v })}
|
onChartCrosshairInfoVisible={v => saveAppDef({ chartCrosshairInfoVisible: v })}
|
||||||
|
chartHoverToolbarVisible={chartHoverToolbarVisible}
|
||||||
|
onChartHoverToolbarVisible={v => saveAppDef({ chartHoverToolbarVisible: v })}
|
||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
onChartPaneSeparatorChange={opts => {
|
onChartPaneSeparatorChange={opts => {
|
||||||
saveAppDef({ chartPaneSeparator: opts });
|
saveAppDef({ chartPaneSeparator: opts });
|
||||||
@@ -2322,6 +2365,7 @@ function App() {
|
|||||||
chartVolumeVisible={chartVolumeVisible}
|
chartVolumeVisible={chartVolumeVisible}
|
||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
||||||
|
chartHoverToolbarVisible={chartHoverToolbarVisible}
|
||||||
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
||||||
chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
|
chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
|
||||||
displayTimezone={displayTimezone}
|
displayTimezone={displayTimezone}
|
||||||
@@ -2364,6 +2408,7 @@ function App() {
|
|||||||
chartVolumeVisible={chartVolumeVisible}
|
chartVolumeVisible={chartVolumeVisible}
|
||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
||||||
|
chartHoverToolbarVisible={chartHoverToolbarVisible}
|
||||||
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
||||||
chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
|
chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
|
||||||
displayTimezone={displayTimezone}
|
displayTimezone={displayTimezone}
|
||||||
@@ -2405,7 +2450,10 @@ function App() {
|
|||||||
crosshairInfoVisible={chartCrosshairInfoVisible}
|
crosshairInfoVisible={chartCrosshairInfoVisible}
|
||||||
onCrosshair={setLegend}
|
onCrosshair={setLegend}
|
||||||
onTradeOrderRequest={applyTradeFill}
|
onTradeOrderRequest={applyTradeFill}
|
||||||
onDataLoaded={() => syncBacktestMarkersRef.current()}
|
onDataLoaded={() => {
|
||||||
|
managerRef.current?.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||||
|
syncBacktestMarkersRef.current();
|
||||||
|
}}
|
||||||
onCandlesReady={handleCandlesReady}
|
onCandlesReady={handleCandlesReady}
|
||||||
onManagerReady={mgr => {
|
onManagerReady={mgr => {
|
||||||
managerRef.current = mgr;
|
managerRef.current = mgr;
|
||||||
@@ -2425,6 +2473,7 @@ function App() {
|
|||||||
mgr.setVolumeVisible(chartVolumeVisible, wrapper?.clientHeight);
|
mgr.setVolumeVisible(chartVolumeVisible, wrapper?.clientHeight);
|
||||||
mgr.setPaneSeparatorOptions(chartPaneSeparator);
|
mgr.setPaneSeparatorOptions(chartPaneSeparator);
|
||||||
mgr.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
|
mgr.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
|
||||||
|
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||||
syncBacktestMarkersRef.current();
|
syncBacktestMarkersRef.current();
|
||||||
// 왼쪽 스크롤 감지 → 과거 데이터 추가 로드
|
// 왼쪽 스크롤 감지 → 과거 데이터 추가 로드
|
||||||
mgr.subscribeVisibleLogicalRange(r => {
|
mgr.subscribeVisibleLogicalRange(r => {
|
||||||
@@ -2469,6 +2518,9 @@ function App() {
|
|||||||
candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels}
|
candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels}
|
||||||
indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels}
|
indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels}
|
||||||
paneSeparatorOptions={chartPaneSeparator}
|
paneSeparatorOptions={chartPaneSeparator}
|
||||||
|
candleOverlayToggles={candleOverlayToggles}
|
||||||
|
onToggleCandleOverlay={handleToggleCandleOverlay}
|
||||||
|
showHoverToolbar={chartHoverToolbarVisible}
|
||||||
/>
|
/>
|
||||||
{isSingleLoadingMore && (
|
{isSingleLoadingMore && (
|
||||||
<div className="chart-history-loading">
|
<div className="chart-history-loading">
|
||||||
|
|||||||
@@ -8,13 +8,10 @@ import { ChartManager } from '../utils/ChartManager';
|
|||||||
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
||||||
import { isIndicatorPaneFocused, resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
|
import { isIndicatorPaneFocused, resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
|
||||||
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
|
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
|
||||||
import { PAPER_OVERLAY_MENU_TITLE } from '../utils/paperChartOverlayVisibility';
|
import type { ChartOverlayToggleKey } from '../utils/chartOverlayVisibility';
|
||||||
|
|
||||||
/** 캔들 오버레이 표시 설정 그룹 메뉴 id */
|
|
||||||
export const CANDLE_OVERLAY_MENU_ID = '__candle_overlay_visibility__';
|
|
||||||
|
|
||||||
export interface CandleOverlayToggleItem {
|
export interface CandleOverlayToggleItem {
|
||||||
key: string;
|
key: ChartOverlayToggleKey;
|
||||||
label: string;
|
label: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
@@ -26,7 +23,7 @@ export interface ChartRightToolbarProps {
|
|||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
/** 캔들 pane 상단 — 이동평균·일목·볼린저 표시/숨김 (투자관리 차트 탭) */
|
/** 캔들 pane 상단 — 이동평균·일목·볼린저 표시/숨김 (투자관리 차트 탭) */
|
||||||
candleOverlayToggles?: CandleOverlayToggleItem[];
|
candleOverlayToggles?: CandleOverlayToggleItem[];
|
||||||
onToggleCandleOverlay?: (key: string) => void;
|
onToggleCandleOverlay?: (key: ChartOverlayToggleKey) => void;
|
||||||
onSplit?: (hostId: string) => void;
|
onSplit?: (hostId: string) => void;
|
||||||
onRemove?: (id: string) => void;
|
onRemove?: (id: string) => void;
|
||||||
onDuplicate?: (id: string) => void;
|
onDuplicate?: (id: string) => void;
|
||||||
@@ -107,6 +104,49 @@ const IconMore = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const IconMa = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||||
|
stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
|
||||||
|
<path d="M1 10 L5 4 L8 7 L13 2"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const IconBollinger = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||||
|
stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
|
||||||
|
<path d="M1 4 Q7 1 13 4"/>
|
||||||
|
<path d="M1 7 Q7 4 13 7"/>
|
||||||
|
<path d="M1 10 Q7 13 13 10"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const IconCandle = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||||
|
stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
|
||||||
|
<line x1="4" y1="2" x2="4" y2="12"/>
|
||||||
|
<rect x="2.5" y="5" width="3" height="4" rx="0.5" fill="currentColor" stroke="none"/>
|
||||||
|
<line x1="10" y1="3" x2="10" y2="11"/>
|
||||||
|
<rect x="8.5" y="4" width="3" height="5" rx="0.5" fill="currentColor" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const IconIchimoku = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||||
|
stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M1 9 L7 5 L13 9" fill="rgba(239,83,80,0.25)"/>
|
||||||
|
<path d="M1 5 L7 9 L13 5" fill="rgba(38,166,154,0.25)"/>
|
||||||
|
<path d="M1 9 L7 5 L13 9"/>
|
||||||
|
<path d="M1 5 L7 9 L13 5"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const OVERLAY_ICONS: Record<ChartOverlayToggleKey, React.ReactNode> = {
|
||||||
|
ma: <IconMa />,
|
||||||
|
bollinger: <IconBollinger />,
|
||||||
|
ichimoku: <IconIchimoku />,
|
||||||
|
candle: <IconCandle />,
|
||||||
|
};
|
||||||
|
|
||||||
const IconEye = ({ hidden }: { hidden?: boolean }) => hidden ? (
|
const IconEye = ({ hidden }: { hidden?: boolean }) => hidden ? (
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||||
<path d="M1 7 Q4 2.5 7 2.5 Q10 2.5 13 7 Q10 11.5 7 11.5 Q4 11.5 1 7 Z" strokeOpacity="0.35"/>
|
<path d="M1 7 Q4 2.5 7 2.5 Q10 2.5 13 7 Q10 11.5 7 11.5 Q4 11.5 1 7 Z" strokeOpacity="0.35"/>
|
||||||
@@ -295,13 +335,12 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
|||||||
const allowDrag = !!(onDragStart && paneItems.length > 1);
|
const allowDrag = !!(onDragStart && paneItems.length > 1);
|
||||||
const clusterHeight = (allowDrag ? BTN_SIZE + BTN_GAP : 0) + BTN_SIZE;
|
const clusterHeight = (allowDrag ? BTN_SIZE + BTN_GAP : 0) + BTN_SIZE;
|
||||||
|
|
||||||
const openItem = openMenuId && openMenuId !== CANDLE_OVERLAY_MENU_ID
|
const openItem = openMenuId
|
||||||
? paneItems.find(p => p.id === openMenuId)
|
? paneItems.find(p => p.id === openMenuId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const candlePane = paneLayouts.find(l => l.paneIndex === 0);
|
const candlePane = paneLayouts.find(l => l.paneIndex === 0);
|
||||||
const showOverlayMenu = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
const showOverlayIcons = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
||||||
const overlayMenuOpen = openMenuId === CANDLE_OVERLAY_MENU_ID;
|
|
||||||
|
|
||||||
const focusedLayoutId = focusedId
|
const focusedLayoutId = focusedId
|
||||||
? resolveFocusedPaneLayoutId(focusedId, indicators)
|
? resolveFocusedPaneLayoutId(focusedId, indicators)
|
||||||
@@ -314,18 +353,6 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
|||||||
})()
|
})()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const overlayMenuItems: MenuItemDef[] = showOverlayMenu
|
|
||||||
? candleOverlayToggles!.map(item => ({
|
|
||||||
key: item.key,
|
|
||||||
title: item.visible ? `${item.label} 숨기기` : `${item.label} 표시`,
|
|
||||||
icon: <IconEye hidden={!item.visible} />,
|
|
||||||
active: !item.visible,
|
|
||||||
onClick: () => {
|
|
||||||
onToggleCandleOverlay?.(item.key);
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="chart-right-toolbar" aria-label="차트 우측 도구">
|
<aside className="chart-right-toolbar" aria-label="차트 우측 도구">
|
||||||
<div
|
<div
|
||||||
@@ -348,21 +375,26 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showOverlayMenu && (
|
{showOverlayIcons && (
|
||||||
<div
|
<div
|
||||||
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay"
|
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay-icons"
|
||||||
style={{ top: candlePane!.topY + 4 }}
|
style={{ top: candlePane!.topY + 4 }}
|
||||||
>
|
>
|
||||||
|
{candleOverlayToggles!.map((item, idx) => (
|
||||||
<button
|
<button
|
||||||
|
key={item.key}
|
||||||
type="button"
|
type="button"
|
||||||
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--menu${overlayMenuOpen ? ' active' : ''}`}
|
className={`tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--overlay${item.visible ? '' : ' chart-right-toolbar-btn--overlay-off'}`}
|
||||||
title="캔들 지표 표시 설정"
|
title={item.visible ? `${item.label} 숨기기` : `${item.label} 표시`}
|
||||||
onMouseEnter={e => openFlyout(CANDLE_OVERLAY_MENU_ID, e.currentTarget)}
|
style={{ marginTop: idx > 0 ? BTN_GAP : 0 }}
|
||||||
onMouseLeave={scheduleClose}
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggleCandleOverlay?.(item.key);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconMore />
|
{OVERLAY_ICONS[item.key]}
|
||||||
<span className="tv-side-expand-arrow" />
|
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -425,28 +457,6 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{overlayMenuOpen && overlayMenuItems.length > 0 && (
|
|
||||||
<div
|
|
||||||
className="tv-side-flyout tv-side-flyout--right-anchor"
|
|
||||||
style={{ top: flyoutPos.top, left: flyoutPos.left }}
|
|
||||||
onMouseEnter={cancelClose}
|
|
||||||
onMouseLeave={scheduleClose}
|
|
||||||
>
|
|
||||||
<div className="tv-side-flyout-header">{PAPER_OVERLAY_MENU_TITLE}</div>
|
|
||||||
{overlayMenuItems.map(mi => (
|
|
||||||
<button
|
|
||||||
key={mi.key}
|
|
||||||
type="button"
|
|
||||||
className={`tv-side-flyout-item${mi.active ? ' active' : ''}`}
|
|
||||||
onClick={e => { e.stopPropagation(); mi.onClick(); }}
|
|
||||||
>
|
|
||||||
<span className="tv-side-flyout-icon">{mi.icon}</span>
|
|
||||||
<span className="tv-side-flyout-title">{mi.title}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{openItem && openMenuId && (() => {
|
{openItem && openMenuId && (() => {
|
||||||
const menuItems = buildMenuItems(openItem, indicators, paneItems, {
|
const menuItems = buildMenuItems(openItem, indicators, paneItems, {
|
||||||
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
|
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ import {
|
|||||||
replaceIndicatorConfigsFromTypes,
|
replaceIndicatorConfigsFromTypes,
|
||||||
} from '../utils/indicatorPaneMerge';
|
} from '../utils/indicatorPaneMerge';
|
||||||
import { sortIndicatorsByTypeOrder } from '../utils/indicatorListOrder';
|
import { sortIndicatorsByTypeOrder } from '../utils/indicatorListOrder';
|
||||||
|
import {
|
||||||
|
type ChartOverlayToggleKey,
|
||||||
|
type ChartOverlayVisibility,
|
||||||
|
chartOverlayKeyForType,
|
||||||
|
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||||
|
listChartOverlayToggleItems,
|
||||||
|
} from '../utils/chartOverlayVisibility';
|
||||||
import { saveSlot } from '../utils/backendApi';
|
import { saveSlot } from '../utils/backendApi';
|
||||||
import type {
|
import type {
|
||||||
OHLCVBar, ChartType, Theme, Timeframe,
|
OHLCVBar, ChartType, Theme, Timeframe,
|
||||||
@@ -192,6 +199,8 @@ export interface ChartSlotProps {
|
|||||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||||
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */
|
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */
|
||||||
chartCrosshairInfoVisible?: boolean;
|
chartCrosshairInfoVisible?: boolean;
|
||||||
|
/** 차트 마우스오버 줌·이동 플로팅 툴바 */
|
||||||
|
chartHoverToolbarVisible?: boolean;
|
||||||
/** 업비트 실시간 티커 맵 (슬롯 심볼별 현재가·등락) */
|
/** 업비트 실시간 티커 맵 (슬롯 심볼별 현재가·등락) */
|
||||||
marketTickers?: Map<string, TickerData>;
|
marketTickers?: Map<string, TickerData>;
|
||||||
}
|
}
|
||||||
@@ -220,6 +229,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
chartVisible = true,
|
chartVisible = true,
|
||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
chartCrosshairInfoVisible = true,
|
chartCrosshairInfoVisible = true,
|
||||||
|
chartHoverToolbarVisible = true,
|
||||||
marketTickers,
|
marketTickers,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
// ── 전역 지표 파라미터 + 시각 설정 (DB 동기화) ────────────────────────
|
// ── 전역 지표 파라미터 + 시각 설정 (DB 동기화) ────────────────────────
|
||||||
@@ -649,6 +659,37 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
setFocusedIndicatorId(null);
|
setFocusedIndicatorId(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [overlayVisibility, setOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||||
|
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
||||||
|
);
|
||||||
|
const overlayVisibilityRef = useRef(overlayVisibility);
|
||||||
|
useEffect(() => {
|
||||||
|
overlayVisibilityRef.current = overlayVisibility;
|
||||||
|
}, [overlayVisibility]);
|
||||||
|
|
||||||
|
const candleOverlayToggles = useMemo(
|
||||||
|
() => listChartOverlayToggleItems(overlayVisibility),
|
||||||
|
[overlayVisibility],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
||||||
|
setOverlayVisibility(prev => {
|
||||||
|
const next = { ...prev, [key]: !prev[key] };
|
||||||
|
const mgr = managerRef.current;
|
||||||
|
mgr?.setChartOverlayGroupVisible(key, next[key]);
|
||||||
|
if (mgr) {
|
||||||
|
for (const info of mgr.getIndicatorPaneInfo()) {
|
||||||
|
if (chartOverlayKeyForType(info.type) !== key) continue;
|
||||||
|
const reactInd = indicatorsRef.current.find(i => i.id === info.id);
|
||||||
|
mgr.applyIndicatorStyle(reactInd
|
||||||
|
? { ...info.config, ...reactInd, id: info.id, type: info.type }
|
||||||
|
: info.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDuplicateIndicator = useCallback((sourceId: string) => {
|
const handleDuplicateIndicator = useCallback((sourceId: string) => {
|
||||||
setIndicators(prev => duplicateIndicatorInList(sourceId, prev, newIndId) ?? prev);
|
setIndicators(prev => duplicateIndicatorInList(sourceId, prev, newIndId) ?? prev);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -869,7 +910,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
logScale={false}
|
logScale={false}
|
||||||
drawingsLocked={compactMode}
|
drawingsLocked={compactMode}
|
||||||
drawingsVisible={!compactMode}
|
drawingsVisible={!compactMode}
|
||||||
showHoverToolbar={!compactMode}
|
showHoverToolbar={!compactMode && chartHoverToolbarVisible}
|
||||||
volumeVisible={chartVolumeVisible}
|
volumeVisible={chartVolumeVisible}
|
||||||
candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels}
|
candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels}
|
||||||
indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels}
|
indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels}
|
||||||
@@ -895,6 +936,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels);
|
mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels);
|
||||||
mgr.setVolumeVisible(chartVolumeVisible);
|
mgr.setVolumeVisible(chartVolumeVisible);
|
||||||
if (chartPaneSeparator) mgr.setPaneSeparatorOptions(chartPaneSeparator);
|
if (chartPaneSeparator) mgr.setPaneSeparatorOptions(chartPaneSeparator);
|
||||||
|
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||||
// Time sync: 가시 시간 범위 변화 구독
|
// Time sync: 가시 시간 범위 변화 구독
|
||||||
mgr.subscribeVisibleTimeRange(r => {
|
mgr.subscribeVisibleTimeRange(r => {
|
||||||
if (!r || isSyncingTimeRef.current) return;
|
if (!r || isSyncingTimeRef.current) return;
|
||||||
@@ -919,6 +961,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
// 첫 마운트 시 pending 상태였던 sync range 를 재적용한다
|
// 첫 마운트 시 pending 상태였던 sync range 를 재적용한다
|
||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
if (!mgr) return;
|
if (!mgr) return;
|
||||||
|
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||||
if (skipSyncApplyRef.current) {
|
if (skipSyncApplyRef.current) {
|
||||||
skipSyncApplyRef.current = false;
|
skipSyncApplyRef.current = false;
|
||||||
const fb = mgr.hasIchimoku() ? 28 : 0;
|
const fb = mgr.hasIchimoku() ? 28 : 0;
|
||||||
@@ -971,6 +1014,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
onToggleIndicatorHidden={handleToggleHidden}
|
onToggleIndicatorHidden={handleToggleHidden}
|
||||||
onOpenIndicatorSettings={id => setSettingsModalId(id)}
|
onOpenIndicatorSettings={id => setSettingsModalId(id)}
|
||||||
onFullView={onFullView ? () => onFullView(symbolRef.current) : undefined}
|
onFullView={onFullView ? () => onFullView(symbolRef.current) : undefined}
|
||||||
|
candleOverlayToggles={compactMode ? undefined : candleOverlayToggles}
|
||||||
|
onToggleCandleOverlay={compactMode ? undefined : handleToggleCandleOverlay}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 컨텍스트 툴바 */}
|
{/* 컨텍스트 툴바 */}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ interface SettingsPageProps {
|
|||||||
onChartLegendOptionsChange?: (patch: Partial<ChartLegendVisibility>) => void;
|
onChartLegendOptionsChange?: (patch: Partial<ChartLegendVisibility>) => void;
|
||||||
chartCrosshairInfoVisible?: boolean;
|
chartCrosshairInfoVisible?: boolean;
|
||||||
onChartCrosshairInfoVisible?: (v: boolean) => void;
|
onChartCrosshairInfoVisible?: (v: boolean) => void;
|
||||||
|
chartHoverToolbarVisible?: boolean;
|
||||||
|
onChartHoverToolbarVisible?: (v: boolean) => void;
|
||||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||||
onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void;
|
onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void;
|
||||||
paperTradingEnabled?: boolean;
|
paperTradingEnabled?: boolean;
|
||||||
@@ -783,6 +785,8 @@ interface ChartPanelProps {
|
|||||||
onChartLegendOptionsChange?: (patch: Partial<ChartLegendVisibility>) => void;
|
onChartLegendOptionsChange?: (patch: Partial<ChartLegendVisibility>) => void;
|
||||||
chartCrosshairInfoVisible?: boolean;
|
chartCrosshairInfoVisible?: boolean;
|
||||||
onChartCrosshairInfoVisible?: (v: boolean) => void;
|
onChartCrosshairInfoVisible?: (v: boolean) => void;
|
||||||
|
chartHoverToolbarVisible?: boolean;
|
||||||
|
onChartHoverToolbarVisible?: (v: boolean) => void;
|
||||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||||
onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void;
|
onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void;
|
||||||
chartTimeFormat?: string;
|
chartTimeFormat?: string;
|
||||||
@@ -807,6 +811,8 @@ const ChartPanel: React.FC<ChartPanelProps> = ({
|
|||||||
onChartLegendOptionsChange,
|
onChartLegendOptionsChange,
|
||||||
chartCrosshairInfoVisible = true,
|
chartCrosshairInfoVisible = true,
|
||||||
onChartCrosshairInfoVisible,
|
onChartCrosshairInfoVisible,
|
||||||
|
chartHoverToolbarVisible = true,
|
||||||
|
onChartHoverToolbarVisible,
|
||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
onChartPaneSeparatorChange,
|
onChartPaneSeparatorChange,
|
||||||
chartTimeFormat = 'MM-dd HH:mm',
|
chartTimeFormat = 'MM-dd HH:mm',
|
||||||
@@ -1054,6 +1060,22 @@ const ChartPanel: React.FC<ChartPanelProps> = ({
|
|||||||
</select>
|
</select>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
|
<SettingSection title="차트 조작 툴바">
|
||||||
|
<SettingRow
|
||||||
|
label="줌·이동 플로팅 툴바"
|
||||||
|
desc="켜면 차트에 마우스를 올렸을 때 하단에 축소(−)·확대(+)·전체 보기·좌우 이동(‹ ›) 버튼이 표시됩니다. 끄면 툴바가 나타나지 않습니다."
|
||||||
|
>
|
||||||
|
<label className="stg-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={chartHoverToolbarVisible}
|
||||||
|
onChange={e => onChartHoverToolbarVisible?.(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="stg-toggle-slider" />
|
||||||
|
</label>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1714,6 +1736,8 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
onChartLegendOptionsChange,
|
onChartLegendOptionsChange,
|
||||||
chartCrosshairInfoVisible = true,
|
chartCrosshairInfoVisible = true,
|
||||||
onChartCrosshairInfoVisible,
|
onChartCrosshairInfoVisible,
|
||||||
|
chartHoverToolbarVisible = true,
|
||||||
|
onChartHoverToolbarVisible,
|
||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
onChartPaneSeparatorChange,
|
onChartPaneSeparatorChange,
|
||||||
tradeAlertSoundEnabled = true,
|
tradeAlertSoundEnabled = true,
|
||||||
@@ -1828,6 +1852,8 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
onChartLegendOptionsChange={onChartLegendOptionsChange}
|
onChartLegendOptionsChange={onChartLegendOptionsChange}
|
||||||
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
||||||
onChartCrosshairInfoVisible={onChartCrosshairInfoVisible}
|
onChartCrosshairInfoVisible={onChartCrosshairInfoVisible}
|
||||||
|
chartHoverToolbarVisible={chartHoverToolbarVisible}
|
||||||
|
onChartHoverToolbarVisible={onChartHoverToolbarVisible}
|
||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
onChartPaneSeparatorChange={onChartPaneSeparatorChange}
|
onChartPaneSeparatorChange={onChartPaneSeparatorChange}
|
||||||
chartTimeFormat={chartTimeFormat}
|
chartTimeFormat={chartTimeFormat}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ interface TradingChartProps {
|
|||||||
showCandlePaneControls?: boolean;
|
showCandlePaneControls?: boolean;
|
||||||
/** 캔들 오버레이(이동평균·일목·볼린저) 표시 토글 — 우측 툴바 상단 */
|
/** 캔들 오버레이(이동평균·일목·볼린저) 표시 토글 — 우측 툴바 상단 */
|
||||||
candleOverlayToggles?: CandleOverlayToggleItem[];
|
candleOverlayToggles?: CandleOverlayToggleItem[];
|
||||||
onToggleCandleOverlay?: (key: string) => void;
|
onToggleCandleOverlay?: (key: import('../utils/chartOverlayVisibility').ChartOverlayToggleKey) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TradingChart: React.FC<TradingChartProps> = ({
|
const TradingChart: React.FC<TradingChartProps> = ({
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export function resolveAppDefaults(s: AppSettingsDto) {
|
|||||||
chartVolumeVisible: s.chartVolumeVisible ?? true,
|
chartVolumeVisible: s.chartVolumeVisible ?? true,
|
||||||
chartLiveReceiveHighlight: s.chartLiveReceiveHighlight ?? true,
|
chartLiveReceiveHighlight: s.chartLiveReceiveHighlight ?? true,
|
||||||
chartCrosshairInfoVisible: s.chartCrosshairInfoVisible ?? true,
|
chartCrosshairInfoVisible: s.chartCrosshairInfoVisible ?? true,
|
||||||
|
chartHoverToolbarVisible: s.chartHoverToolbarVisible ?? true,
|
||||||
chartLegendOptions: resolveChartLegendOptions(
|
chartLegendOptions: resolveChartLegendOptions(
|
||||||
s.chartLegendOptions as Partial<ChartLegendVisibility> | null | undefined,
|
s.chartLegendOptions as Partial<ChartLegendVisibility> | null | undefined,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ import {
|
|||||||
resolveIchimokuCloudColors,
|
resolveIchimokuCloudColors,
|
||||||
resolveIchimokuDisplacements,
|
resolveIchimokuDisplacements,
|
||||||
} from './ichimokuConfig';
|
} from './ichimokuConfig';
|
||||||
|
import {
|
||||||
|
type ChartOverlayToggleKey,
|
||||||
|
type ChartOverlayVisibility,
|
||||||
|
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||||
|
chartOverlayKeyForType,
|
||||||
|
} from './chartOverlayVisibility';
|
||||||
import type { PlotDef, HLineStyle } from './indicatorRegistry';
|
import type { PlotDef, HLineStyle } from './indicatorRegistry';
|
||||||
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils';
|
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils';
|
||||||
import {
|
import {
|
||||||
@@ -230,6 +236,10 @@ export class ChartManager {
|
|||||||
/** 실시간 전략 체크 마커 */
|
/** 실시간 전략 체크 마커 */
|
||||||
private liveStrategyMarkers: MarkerData[] = [];
|
private liveStrategyMarkers: MarkerData[] = [];
|
||||||
private compareSeriesMap = new Map<string, ISeriesApi<SeriesType>>();
|
private compareSeriesMap = new Map<string, ISeriesApi<SeriesType>>();
|
||||||
|
/** 캔들 pane 오버레이 그룹 표시 (config.hidden 과 별도 — in-place 토글용) */
|
||||||
|
private _chartOverlayVisibility: ChartOverlayVisibility = {
|
||||||
|
...DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||||
|
};
|
||||||
|
|
||||||
// ── 인디케이터 실시간 갱신 스로틀 ─────────────────────────────────────────
|
// ── 인디케이터 실시간 갱신 스로틀 ─────────────────────────────────────────
|
||||||
private _indUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
private _indUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -668,7 +678,7 @@ export class ChartManager {
|
|||||||
config = enrichIndicatorConfig(config);
|
config = enrichIndicatorConfig(config);
|
||||||
if (this.indicators.has(config.id) || this.rawBars.length === 0) return;
|
if (this.indicators.has(config.id) || this.rawBars.length === 0) return;
|
||||||
const dataGenAtStart = this._dataGeneration;
|
const dataGenAtStart = this._dataGeneration;
|
||||||
const indicatorHidden = config.hidden === true;
|
const indicatorHidden = !this._isIndicatorEffectivelyVisible(config);
|
||||||
const def = getIndicatorDef(config.type);
|
const def = getIndicatorDef(config.type);
|
||||||
const seriesList: ISeriesApi<SeriesType>[] = [];
|
const seriesList: ISeriesApi<SeriesType>[] = [];
|
||||||
const seriesMeta: IndicatorEntry['seriesMeta'] = [];
|
const seriesMeta: IndicatorEntry['seriesMeta'] = [];
|
||||||
@@ -855,6 +865,7 @@ export class ChartManager {
|
|||||||
if (configs.length > 0) {
|
if (configs.length > 0) {
|
||||||
this._finalizeIndicatorPaneLayout();
|
this._finalizeIndicatorPaneLayout();
|
||||||
this._refreshAllIchimokuClouds();
|
this._refreshAllIchimokuClouds();
|
||||||
|
this.syncChartOverlayVisibility();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,6 +941,7 @@ export class ChartManager {
|
|||||||
|
|
||||||
this._finalizeIndicatorPaneLayout();
|
this._finalizeIndicatorPaneLayout();
|
||||||
this._refreshAllIchimokuClouds();
|
this._refreshAllIchimokuClouds();
|
||||||
|
this.syncChartOverlayVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeIndicator(id: string): void {
|
removeIndicator(id: string): void {
|
||||||
@@ -1786,7 +1798,7 @@ export class ChartManager {
|
|||||||
plugin.setColors(colors.bullishColor, colors.bearishColor);
|
plugin.setColors(colors.bullishColor, colors.bearishColor);
|
||||||
plugin.setVisibility(effectiveBullish, effectiveBearish);
|
plugin.setVisibility(effectiveBullish, effectiveBearish);
|
||||||
|
|
||||||
if (config.hidden === true) {
|
if (!this._isIndicatorEffectivelyVisible(config)) {
|
||||||
plugin.setCloudData([]);
|
plugin.setCloudData([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2343,6 +2355,89 @@ export class ChartManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChartOverlayVisibility(): ChartOverlayVisibility {
|
||||||
|
return { ...this._chartOverlayVisibility };
|
||||||
|
}
|
||||||
|
|
||||||
|
setChartOverlayVisibility(visibility: ChartOverlayVisibility): void {
|
||||||
|
this._chartOverlayVisibility = { ...visibility };
|
||||||
|
this.syncChartOverlayVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
setChartOverlayGroupVisible(key: ChartOverlayToggleKey, visible: boolean): void {
|
||||||
|
this._chartOverlayVisibility = {
|
||||||
|
...this._chartOverlayVisibility,
|
||||||
|
[key]: visible,
|
||||||
|
};
|
||||||
|
this.syncChartOverlayVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사용자 hidden + 세션 오버레이 토글 — entry.config.hidden 은 사용자 의도만 반영 */
|
||||||
|
private _isIndicatorEffectivelyVisible(config: IndicatorConfig): boolean {
|
||||||
|
const overlayKey = chartOverlayKeyForType(config.type);
|
||||||
|
if (overlayKey && !this._chartOverlayVisibility[overlayKey]) return false;
|
||||||
|
return config.hidden !== true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 오버레이·plotVisibility 반영해 시리즈 visible 만 갱신 (플롯 정의 없어도 전체 시리즈 대상) */
|
||||||
|
private _applySeriesVisibilityToEntry(entry: IndicatorEntry): void {
|
||||||
|
const config = entry.config;
|
||||||
|
if (!config) return;
|
||||||
|
const indicatorVisible = this._isIndicatorEffectivelyVisible(config);
|
||||||
|
|
||||||
|
entry.seriesList.forEach((series, i) => {
|
||||||
|
const plotId = entry.seriesMeta[i]?.plotId;
|
||||||
|
const visible = indicatorVisible
|
||||||
|
&& (plotId == null || config.plotVisibility?.[plotId] !== false);
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
series.applyOptions({ visible } as any);
|
||||||
|
} catch { /* ok */ }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 오버레이 그룹·캔들 visible — 시리즈 재생성 없이 applyOptions 만 사용 */
|
||||||
|
syncChartOverlayVisibility(): void {
|
||||||
|
if (this.mainSeries) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(this.mainSeries as ISeriesApi<any>).applyOptions({
|
||||||
|
visible: this._chartOverlayVisibility.candle,
|
||||||
|
});
|
||||||
|
} catch { /* ok */ }
|
||||||
|
}
|
||||||
|
for (const entry of this.indicators.values()) {
|
||||||
|
const config = entry.config;
|
||||||
|
if (!config) continue;
|
||||||
|
const key = chartOverlayKeyForType(config.type);
|
||||||
|
if (!key) continue;
|
||||||
|
|
||||||
|
this._applySeriesVisibilityToEntry(entry);
|
||||||
|
|
||||||
|
if (entry.type === 'BollingerBands') {
|
||||||
|
detachBbBandFill(entry);
|
||||||
|
if (this._isIndicatorEffectivelyVisible(config)) {
|
||||||
|
attachBbBandFill(entry, config);
|
||||||
|
entry.bbFillPrimitive?.requestRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.type === 'IchimokuCloud' && entry.cloudPlugin) {
|
||||||
|
if (!this._isIndicatorEffectivelyVisible(config)) {
|
||||||
|
entry.cloudPlugin.setCloudData([]);
|
||||||
|
} else {
|
||||||
|
void calculateIndicator(
|
||||||
|
'IchimokuCloud',
|
||||||
|
this.rawBars,
|
||||||
|
config.params ?? {},
|
||||||
|
).then(({ plots }) => {
|
||||||
|
this._applyIchimokuCloudPlugin(entry.cloudPlugin!, plots, config);
|
||||||
|
}).catch(() => { /* ignore */ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 인디케이터 전체 가시성 토글 (시리즈 visible 옵션 변경) */
|
/** 인디케이터 전체 가시성 토글 (시리즈 visible 옵션 변경) */
|
||||||
toggleIndicatorVisible(id: string, visible: boolean): void {
|
toggleIndicatorVisible(id: string, visible: boolean): void {
|
||||||
const entry = this.indicators.get(id);
|
const entry = this.indicators.get(id);
|
||||||
@@ -2451,19 +2546,25 @@ export class ChartManager {
|
|||||||
config = enrichIndicatorConfig(config);
|
config = enrichIndicatorConfig(config);
|
||||||
const entry = this.indicators.get(config.id);
|
const entry = this.indicators.get(config.id);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
entry.config = config;
|
const prevUserHidden = entry.config?.hidden === true;
|
||||||
const def = getIndicatorDef(config.type);
|
const def = getIndicatorDef(config.type);
|
||||||
const plotDefs = config.plots ?? def?.plots ?? [];
|
const plotDefs = config.plots ?? def?.plots ?? [];
|
||||||
|
|
||||||
const plotById = Object.fromEntries(plotDefs.map((p: PlotDef) => [p.id, p]));
|
const plotById = Object.fromEntries(plotDefs.map((p: PlotDef) => [p.id, p]));
|
||||||
|
|
||||||
|
const indicatorVisible = this._isIndicatorEffectivelyVisible(config);
|
||||||
|
|
||||||
entry.seriesList.forEach((series, i) => {
|
entry.seriesList.forEach((series, i) => {
|
||||||
let plot: PlotDef | undefined;
|
let plot: PlotDef | undefined;
|
||||||
const pid = entry.seriesMeta[i]?.plotId;
|
const pid = entry.seriesMeta[i]?.plotId;
|
||||||
plot = (pid ? plotById[pid] : undefined) ?? plotDefs[i];
|
plot = (pid ? plotById[pid] : undefined) ?? plotDefs[i];
|
||||||
if (!plot) return;
|
const visible = indicatorVisible
|
||||||
const indicatorVisible = config.hidden !== true;
|
&& (pid == null || config.plotVisibility?.[pid] !== false);
|
||||||
const visible = indicatorVisible && config.plotVisibility?.[plot.id] !== false;
|
if (!plot) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
series.applyOptions({ visible } as any);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const paneIdx = entry.paneIndex ?? 0;
|
const paneIdx = entry.paneIndex ?? 0;
|
||||||
const indicatorPriceFormat = priceFormatForIndicatorPane(paneIdx);
|
const indicatorPriceFormat = priceFormatForIndicatorPane(paneIdx);
|
||||||
|
|
||||||
@@ -2516,7 +2617,7 @@ export class ChartManager {
|
|||||||
entry.fillPrimitive = undefined;
|
entry.fillPrimitive = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.hidden !== true) {
|
if (indicatorVisible) {
|
||||||
for (const hl of hlines) {
|
for (const hl of hlines) {
|
||||||
if (hl.visible === false) continue;
|
if (hl.visible === false) continue;
|
||||||
const pl = firstSeries.createPriceLine({
|
const pl = firstSeries.createPriceLine({
|
||||||
@@ -2544,7 +2645,7 @@ export class ChartManager {
|
|||||||
|
|
||||||
if (entry.type === 'BollingerBands') {
|
if (entry.type === 'BollingerBands') {
|
||||||
detachBbBandFill(entry);
|
detachBbBandFill(entry);
|
||||||
if (config.hidden !== true) {
|
if (indicatorVisible) {
|
||||||
attachBbBandFill(entry, config);
|
attachBbBandFill(entry, config);
|
||||||
entry.bbFillPrimitive?.requestRefresh();
|
entry.bbFillPrimitive?.requestRefresh();
|
||||||
}
|
}
|
||||||
@@ -2560,7 +2661,17 @@ export class ChartManager {
|
|||||||
}).catch(() => { /* ignore */ });
|
}).catch(() => { /* ignore */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.config = config;
|
const overlayKey = chartOverlayKeyForType(config.type);
|
||||||
|
const overlayOn = overlayKey == null || this._chartOverlayVisibility[overlayKey];
|
||||||
|
const toStore = { ...config };
|
||||||
|
if (overlayKey) {
|
||||||
|
if (overlayOn && config.hidden !== true) {
|
||||||
|
delete toStore.hidden;
|
||||||
|
} else if (!overlayOn && toStore.hidden === true && !prevUserHidden && config.hidden !== true) {
|
||||||
|
delete toStore.hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.config = toStore;
|
||||||
if (entry.seriesMeta.some(m => m.isHistogram)) {
|
if (entry.seriesMeta.some(m => m.isHistogram)) {
|
||||||
void this._repaintHistogramSeries(config.id, config);
|
void this._repaintHistogramSeries(config.id, config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,6 +423,8 @@ export interface AppSettingsDto {
|
|||||||
chartLiveReceiveHighlight?: boolean;
|
chartLiveReceiveHighlight?: boolean;
|
||||||
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */
|
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */
|
||||||
chartCrosshairInfoVisible?: boolean;
|
chartCrosshairInfoVisible?: boolean;
|
||||||
|
/** 차트 마우스오버 줌·이동 플로팅 툴바 표시 (기본 true) */
|
||||||
|
chartHoverToolbarVisible?: boolean;
|
||||||
/** 차트 상단 범례(tv-legend) 항목별 표시 옵션 */
|
/** 차트 상단 범례(tv-legend) 항목별 표시 옵션 */
|
||||||
chartLegendOptions?: Record<string, boolean> | null;
|
chartLegendOptions?: Record<string, boolean> | null;
|
||||||
/** 차트 pane 구분선 — mainToIndicator / indicatorToIndicator */
|
/** 차트 pane 구분선 — mainToIndicator / indicatorToIndicator */
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { IndicatorConfig } from '../types';
|
||||||
|
import { getIndicatorDef } from './indicatorRegistry';
|
||||||
|
|
||||||
|
/** 실시간 차트 — 캔들 pane 오버레이 표시/숨김 (세션만, DB 미저장) */
|
||||||
|
export type ChartOverlayToggleKey = 'ma' | 'bollinger' | 'ichimoku' | 'candle';
|
||||||
|
|
||||||
|
export type ChartOverlayVisibility = Record<ChartOverlayToggleKey, boolean>;
|
||||||
|
|
||||||
|
export const DEFAULT_CHART_OVERLAY_VISIBILITY: ChartOverlayVisibility = {
|
||||||
|
ma: true,
|
||||||
|
bollinger: true,
|
||||||
|
ichimoku: true,
|
||||||
|
candle: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CHART_OVERLAY_TOGGLE_ORDER: ChartOverlayToggleKey[] = [
|
||||||
|
'ma',
|
||||||
|
'bollinger',
|
||||||
|
'ichimoku',
|
||||||
|
'candle',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CHART_OVERLAY_LABELS: Record<ChartOverlayToggleKey, string> = {
|
||||||
|
ma: '이동평균선',
|
||||||
|
bollinger: '볼린저 밴드',
|
||||||
|
ichimoku: '일목균형표',
|
||||||
|
candle: '캔들차트',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 캔들 pane 오버레이 이동평균 그룹 (SMA·EMA·WMA·HMA 등 Moving Averages 카테고리) */
|
||||||
|
export function isMovingAverageOverlayType(type: string): boolean {
|
||||||
|
const def = getIndicatorDef(type);
|
||||||
|
return def?.category === 'Moving Averages' && def.overlay === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chartOverlayKeyForType(type: string): ChartOverlayToggleKey | null {
|
||||||
|
if (isMovingAverageOverlayType(type)) return 'ma';
|
||||||
|
if (type === 'IchimokuCloud') return 'ichimoku';
|
||||||
|
if (type === 'BollingerBands') return 'bollinger';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 오버레이 그룹 + 지표 자체 hidden 을 반영한 표시용 config (entry.config 는 변경하지 않음).
|
||||||
|
* ChartManager 는 _chartOverlayVisibility 로 직접 제어 — 이 헬퍼는 UI/필터용.
|
||||||
|
*/
|
||||||
|
export function effectiveConfigForOverlay(
|
||||||
|
config: IndicatorConfig,
|
||||||
|
visibility: ChartOverlayVisibility,
|
||||||
|
): IndicatorConfig {
|
||||||
|
const key = chartOverlayKeyForType(config.type);
|
||||||
|
if (!key) return config;
|
||||||
|
if (config.hidden === true) return config;
|
||||||
|
if (!visibility[key]) return { ...config, hidden: true };
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listChartOverlayToggleItems(
|
||||||
|
visibility: ChartOverlayVisibility,
|
||||||
|
): Array<{ key: ChartOverlayToggleKey; label: string; visible: boolean }> {
|
||||||
|
return CHART_OVERLAY_TOGGLE_ORDER.map(key => ({
|
||||||
|
key,
|
||||||
|
label: CHART_OVERLAY_LABELS[key],
|
||||||
|
visible: visibility[key],
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export const DEFAULT_PAPER_OVERLAY_VISIBILITY: PaperOverlayVisibility = {
|
|||||||
bollinger: true,
|
bollinger: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MA_TYPES = new Set(['SMA', 'EMA']);
|
import { isMovingAverageOverlayType } from './chartOverlayVisibility';
|
||||||
|
|
||||||
export const PAPER_OVERLAY_TOGGLE_ORDER: PaperOverlayToggleKey[] = [
|
export const PAPER_OVERLAY_TOGGLE_ORDER: PaperOverlayToggleKey[] = [
|
||||||
'ma',
|
'ma',
|
||||||
@@ -29,7 +29,7 @@ export const PAPER_OVERLAY_LABELS: Record<PaperOverlayToggleKey, string> = {
|
|||||||
export const PAPER_OVERLAY_MENU_TITLE = '표시 설정';
|
export const PAPER_OVERLAY_MENU_TITLE = '표시 설정';
|
||||||
|
|
||||||
export function paperOverlayKeyForType(type: string): PaperOverlayToggleKey | null {
|
export function paperOverlayKeyForType(type: string): PaperOverlayToggleKey | null {
|
||||||
if (MA_TYPES.has(type)) return 'ma';
|
if (isMovingAverageOverlayType(type)) return 'ma';
|
||||||
if (type === 'IchimokuCloud') return 'ichimoku';
|
if (type === 'IchimokuCloud') return 'ichimoku';
|
||||||
if (type === 'BollingerBands') return 'bollinger';
|
if (type === 'BollingerBands') return 'bollinger';
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user