투자관리 화면 수정
This commit is contained in:
@@ -1082,6 +1082,11 @@ html.theme-blue {
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 캔들 pane 상단 — 표시 설정 그룹 (⋯ 메뉴, 투자관리 차트 탭) */
|
||||||
|
.chart-right-toolbar-cluster--overlay {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
/* 우측 툴바 플라이아웃 — 버튼 왼쪽으로 펼침 */
|
/* 우측 툴바 플라이아웃 — 버튼 왼쪽으로 펼침 */
|
||||||
.tv-side-flyout--right-anchor {
|
.tv-side-flyout--right-anchor {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
|
|||||||
@@ -7,12 +7,25 @@ import type { IndicatorConfig } from '../types';
|
|||||||
import { ChartManager } from '../utils/ChartManager';
|
import { ChartManager } from '../utils/ChartManager';
|
||||||
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
||||||
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
|
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
|
||||||
|
import { PAPER_OVERLAY_MENU_TITLE } from '../utils/paperChartOverlayVisibility';
|
||||||
|
|
||||||
|
/** 캔들 오버레이 표시 설정 그룹 메뉴 id */
|
||||||
|
export const CANDLE_OVERLAY_MENU_ID = '__candle_overlay_visibility__';
|
||||||
|
|
||||||
|
export interface CandleOverlayToggleItem {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChartRightToolbarProps {
|
export interface ChartRightToolbarProps {
|
||||||
manager: ChartManager;
|
manager: ChartManager;
|
||||||
indicators: IndicatorConfig[];
|
indicators: IndicatorConfig[];
|
||||||
chartHeight: number;
|
chartHeight: number;
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
|
/** 캔들 pane 상단 — 이동평균·일목·볼린저 표시/숨김 (투자관리 차트 탭) */
|
||||||
|
candleOverlayToggles?: CandleOverlayToggleItem[];
|
||||||
|
onToggleCandleOverlay?: (key: string) => 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;
|
||||||
@@ -210,6 +223,8 @@ function buildMenuItems(
|
|||||||
|
|
||||||
const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||||
manager, indicators, chartHeight, paused = false,
|
manager, indicators, chartHeight, paused = false,
|
||||||
|
candleOverlayToggles,
|
||||||
|
onToggleCandleOverlay,
|
||||||
onSplit, onRemove, onDuplicate,
|
onSplit, onRemove, onDuplicate,
|
||||||
onExpand, onRestore, focusedId,
|
onExpand, onRestore, focusedId,
|
||||||
onDragStart, onToggleHidden, onSettings,
|
onDragStart, onToggleHidden, onSettings,
|
||||||
@@ -279,16 +294,50 @@ 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
|
const openItem = openMenuId && openMenuId !== CANDLE_OVERLAY_MENU_ID
|
||||||
? paneItems.find(p => p.id === openMenuId)
|
? paneItems.find(p => p.id === openMenuId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const candlePane = paneLayouts.find(l => l.paneIndex === 0);
|
||||||
|
const showOverlayMenu = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
||||||
|
const overlayMenuOpen = openMenuId === CANDLE_OVERLAY_MENU_ID;
|
||||||
|
|
||||||
|
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
|
||||||
className="chart-right-toolbar-inner"
|
className="chart-right-toolbar-inner"
|
||||||
style={{ height: Math.max(innerHeight, chartHeight) }}
|
style={{ height: Math.max(innerHeight, chartHeight) }}
|
||||||
>
|
>
|
||||||
|
{showOverlayMenu && (
|
||||||
|
<div
|
||||||
|
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay"
|
||||||
|
style={{ top: candlePane!.topY + 4 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--menu${overlayMenuOpen ? ' active' : ''}`}
|
||||||
|
title="캔들 지표 표시 설정"
|
||||||
|
onMouseEnter={e => openFlyout(CANDLE_OVERLAY_MENU_ID, e.currentTarget)}
|
||||||
|
onMouseLeave={scheduleClose}
|
||||||
|
>
|
||||||
|
<IconMore />
|
||||||
|
<span className="tv-side-expand-arrow" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{paneLayouts.map(layout => (
|
{paneLayouts.map(layout => (
|
||||||
<div
|
<div
|
||||||
key={layout.paneIndex}
|
key={layout.paneIndex}
|
||||||
@@ -348,6 +397,28 @@ 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,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function canApplyChartBars(
|
|||||||
}
|
}
|
||||||
import DrawingCanvas, { hitTestDrawing } from './DrawingCanvas';
|
import DrawingCanvas, { hitTestDrawing } from './DrawingCanvas';
|
||||||
import PaneLegend, { type PaneLegendProps } from './PaneLegend';
|
import PaneLegend, { type PaneLegendProps } from './PaneLegend';
|
||||||
import ChartRightToolbar from './ChartRightToolbar';
|
import ChartRightToolbar, { type CandleOverlayToggleItem } from './ChartRightToolbar';
|
||||||
import CandlePaneTimeAxis from './CandlePaneTimeAxis';
|
import CandlePaneTimeAxis from './CandlePaneTimeAxis';
|
||||||
import ChartHoverToolbar from './ChartHoverToolbar';
|
import ChartHoverToolbar from './ChartHoverToolbar';
|
||||||
import ChartMagnifier from './ChartMagnifier';
|
import ChartMagnifier from './ChartMagnifier';
|
||||||
@@ -161,6 +161,9 @@ interface TradingChartProps {
|
|||||||
defaultCandleOnly?: boolean;
|
defaultCandleOnly?: boolean;
|
||||||
/** 캔들 pane 좌하단 전체보기/복원 버튼 (기본 true) */
|
/** 캔들 pane 좌하단 전체보기/복원 버튼 (기본 true) */
|
||||||
showCandlePaneControls?: boolean;
|
showCandlePaneControls?: boolean;
|
||||||
|
/** 캔들 오버레이(이동평균·일목·볼린저) 표시 토글 — 우측 툴바 상단 */
|
||||||
|
candleOverlayToggles?: CandleOverlayToggleItem[];
|
||||||
|
onToggleCandleOverlay?: (key: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TradingChart: React.FC<TradingChartProps> = ({
|
const TradingChart: React.FC<TradingChartProps> = ({
|
||||||
@@ -203,6 +206,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
dataLoading = false,
|
dataLoading = false,
|
||||||
defaultCandleOnly = false,
|
defaultCandleOnly = false,
|
||||||
showCandlePaneControls = true,
|
showCandlePaneControls = true,
|
||||||
|
candleOverlayToggles,
|
||||||
|
onToggleCandleOverlay,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
||||||
@@ -401,21 +406,24 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
required = mgr.resetPaneHeights(wrapperH);
|
required = mgr.resetPaneHeights(wrapperH);
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = wrapper.clientWidth;
|
// LWC는 chart-container에 마운트 — 우측 툴바(44px) 제외 너비로 맞춰 가격 라벨이 가리지 않음
|
||||||
|
const plotW = Math.max(0, container.clientWidth);
|
||||||
|
const plotH = Math.max(0, container.clientHeight > 0 ? container.clientHeight : wrapperH);
|
||||||
|
|
||||||
if (candleOnly || paneLayoutClamp) {
|
if (candleOnly || paneLayoutClamp) {
|
||||||
// 전체보기·미니차트: 래퍼 높이에 고정 resize (autoSize 피드백 루프·무한 스크롤 방지)
|
// 전체보기·미니차트: 래퍼 높이에 고정 resize (autoSize 피드백 루프·무한 스크롤 방지)
|
||||||
try {
|
|
||||||
if (w > 0 && wrapperH > 0) mgr.resize(w, wrapperH);
|
|
||||||
} catch { /* ok */ }
|
|
||||||
container.style.height = `${wrapperH}px`;
|
container.style.height = `${wrapperH}px`;
|
||||||
|
const clampH = wrapperH > 0 ? wrapperH : plotH;
|
||||||
|
try {
|
||||||
|
if (plotW > 0 && clampH > 0) mgr.resize(plotW, clampH);
|
||||||
|
} catch { /* ok */ }
|
||||||
if (candleOnly) wrapper.scrollTop = 0;
|
if (candleOnly) wrapper.scrollTop = 0;
|
||||||
} else if (required > wrapperH + 4) {
|
} else if (required > wrapperH + 4) {
|
||||||
container.style.height = `${required}px`;
|
container.style.height = `${required}px`;
|
||||||
} else {
|
} else {
|
||||||
container.style.height = '';
|
container.style.height = '';
|
||||||
try {
|
try {
|
||||||
if (w > 0 && wrapperH > 0) mgr.resize(w, wrapperH);
|
if (plotW > 0 && plotH > 0) mgr.resize(plotW, plotH);
|
||||||
} catch { /* ok */ }
|
} catch { /* ok */ }
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -1560,6 +1568,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
indicators={indicators}
|
indicators={indicators}
|
||||||
chartHeight={chartBodyHeight}
|
chartHeight={chartBodyHeight}
|
||||||
paused={paneLegendPaused}
|
paused={paneLegendPaused}
|
||||||
|
candleOverlayToggles={candleOverlayToggles}
|
||||||
|
onToggleCandleOverlay={onToggleCandleOverlay}
|
||||||
onSplit={onSplitIndicatorPane}
|
onSplit={onSplitIndicatorPane}
|
||||||
onRemove={onRemoveIndicator}
|
onRemove={onRemoveIndicator}
|
||||||
onDuplicate={onDuplicateIndicator}
|
onDuplicate={onDuplicateIndicator}
|
||||||
|
|||||||
@@ -17,8 +17,15 @@ import {
|
|||||||
countNonOverlayIndicatorPanes,
|
countNonOverlayIndicatorPanes,
|
||||||
} from '../../utils/strategyOscillatorSeries';
|
} from '../../utils/strategyOscillatorSeries';
|
||||||
import { normalizeSmaConfig } from '../../utils/smaConfig';
|
import { normalizeSmaConfig } from '../../utils/smaConfig';
|
||||||
|
import {
|
||||||
|
applyPaperOverlayVisibility,
|
||||||
|
listPaperOverlayToggleItems,
|
||||||
|
type PaperOverlayToggleKey,
|
||||||
|
type PaperOverlayVisibility,
|
||||||
|
} from '../../utils/paperChartOverlayVisibility';
|
||||||
import {
|
import {
|
||||||
buildVirtualTradingChartIndicators,
|
buildVirtualTradingChartIndicators,
|
||||||
|
ensurePaperChartOverlays,
|
||||||
resolveStrategyPrimaryTimeframe,
|
resolveStrategyPrimaryTimeframe,
|
||||||
} from '../../utils/strategyToChartIndicators';
|
} from '../../utils/strategyToChartIndicators';
|
||||||
|
|
||||||
@@ -35,6 +42,9 @@ interface Props {
|
|||||||
onHistoryBarsLoaded?: (bars: OHLCVBar[]) => void;
|
onHistoryBarsLoaded?: (bars: OHLCVBar[]) => void;
|
||||||
/** 과거 로드·시그널 재계산 중 UI */
|
/** 과거 로드·시그널 재계산 중 UI */
|
||||||
onHistoryLoadingChange?: (loading: boolean) => void;
|
onHistoryLoadingChange?: (loading: boolean) => void;
|
||||||
|
/** 투자관리 차트 탭 — 캔들 오버레이 표시/숨김 (세션만, DB 미저장) */
|
||||||
|
overlayVisibility?: PaperOverlayVisibility;
|
||||||
|
onToggleOverlay?: (key: PaperOverlayToggleKey) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -82,6 +92,8 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
theme = 'dark',
|
theme = 'dark',
|
||||||
onHistoryBarsLoaded,
|
onHistoryBarsLoaded,
|
||||||
onHistoryLoadingChange,
|
onHistoryLoadingChange,
|
||||||
|
overlayVisibility,
|
||||||
|
onToggleOverlay,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||||
const { defaults: appDefaults } = useAppSettings();
|
const { defaults: appDefaults } = useAppSettings();
|
||||||
@@ -118,13 +130,29 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [strategyId]);
|
}, [strategyId]);
|
||||||
|
|
||||||
const indicators = useMemo(() => {
|
const baseIndicators = useMemo(() => {
|
||||||
|
let inds: IndicatorConfig[];
|
||||||
if (strategy) {
|
if (strategy) {
|
||||||
const inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
|
inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
|
||||||
if (inds.length) return inds.filter(i => i.timeframeVisibility?.[chartTimeframe] !== false);
|
inds = inds.filter(i => i.timeframeVisibility?.[chartTimeframe] !== false);
|
||||||
|
} else {
|
||||||
|
inds = defaultOverlayIndicators(getParams);
|
||||||
}
|
}
|
||||||
return defaultOverlayIndicators(getParams);
|
if (overlayVisibility) {
|
||||||
}, [strategy, getParams, getVisualConfig, chartTimeframe]);
|
inds = ensurePaperChartOverlays(inds, getParams, getVisualConfig);
|
||||||
|
}
|
||||||
|
return inds;
|
||||||
|
}, [strategy, getParams, getVisualConfig, chartTimeframe, overlayVisibility]);
|
||||||
|
|
||||||
|
const indicators = useMemo(() => {
|
||||||
|
if (!overlayVisibility) return baseIndicators;
|
||||||
|
return applyPaperOverlayVisibility(baseIndicators, overlayVisibility);
|
||||||
|
}, [baseIndicators, overlayVisibility]);
|
||||||
|
|
||||||
|
const candleOverlayToggles = useMemo(() => {
|
||||||
|
if (!overlayVisibility || !onToggleOverlay) return undefined;
|
||||||
|
return listPaperOverlayToggleItems(baseIndicators, overlayVisibility);
|
||||||
|
}, [baseIndicators, overlayVisibility, onToggleOverlay]);
|
||||||
|
|
||||||
const auxPaneCount = useMemo(
|
const auxPaneCount = useMemo(
|
||||||
() => countNonOverlayIndicatorPanes(indicators, isOverlayType),
|
() => countNonOverlayIndicatorPanes(indicators, isOverlayType),
|
||||||
@@ -321,6 +349,10 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
indicatorAreaPriceLabelsEnabled={priceLabels}
|
indicatorAreaPriceLabelsEnabled={priceLabels}
|
||||||
seriesPriceLabelsEnabled={priceLabels}
|
seriesPriceLabelsEnabled={priceLabels}
|
||||||
showHoverToolbar
|
showHoverToolbar
|
||||||
|
candleOverlayToggles={candleOverlayToggles}
|
||||||
|
onToggleCandleOverlay={onToggleOverlay
|
||||||
|
? (key) => onToggleOverlay(key as PaperOverlayToggleKey)
|
||||||
|
: undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!loading && !error && bars.length === 0 && (
|
{!loading && !error && bars.length === 0 && (
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import { normalizeChartTimeframe } from '../../utils/backtestUiUtils';
|
|||||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||||
import type { OHLCVBar, Theme } from '../../types';
|
import type { OHLCVBar, Theme } from '../../types';
|
||||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||||
|
import {
|
||||||
|
DEFAULT_PAPER_OVERLAY_VISIBILITY,
|
||||||
|
type PaperOverlayToggleKey,
|
||||||
|
type PaperOverlayVisibility,
|
||||||
|
} from '../../utils/paperChartOverlayVisibility';
|
||||||
|
|
||||||
const BAR_COUNT = 300;
|
const BAR_COUNT = 300;
|
||||||
const HISTORY_BACKTEST_DEBOUNCE_MS = 450;
|
const HISTORY_BACKTEST_DEBOUNCE_MS = 450;
|
||||||
@@ -34,6 +39,10 @@ const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark
|
|||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [historyLoading, setHistoryLoading] = useState(false);
|
const [historyLoading, setHistoryLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
/** 종목 변경해도 유지 — 투자관리 차트 탭 세션 전용 (DB·설정 저장 없음) */
|
||||||
|
const [overlayVisibility, setOverlayVisibility] = useState<PaperOverlayVisibility>(
|
||||||
|
() => ({ ...DEFAULT_PAPER_OVERLAY_VISIBILITY }),
|
||||||
|
);
|
||||||
const toTimeSec = useMemo(() => Math.floor(Date.now() / 1000), [market, strategyId]);
|
const toTimeSec = useMemo(() => Math.floor(Date.now() / 1000), [market, strategyId]);
|
||||||
const backtestGenRef = useRef(0);
|
const backtestGenRef = useRef(0);
|
||||||
const historyDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const historyDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -141,6 +150,10 @@ const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark
|
|||||||
};
|
};
|
||||||
}, [market, strategyId, strategy, toTimeSec, runSignalsForBars]);
|
}, [market, strategyId, strategy, toTimeSec, runSignalsForBars]);
|
||||||
|
|
||||||
|
const handleToggleOverlay = useCallback((key: PaperOverlayToggleKey) => {
|
||||||
|
setOverlayVisibility(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleHistoryBarsLoaded = useCallback((bars: OHLCVBar[]) => {
|
const handleHistoryBarsLoaded = useCallback((bars: OHLCVBar[]) => {
|
||||||
const strat = strategyRef.current;
|
const strat = strategyRef.current;
|
||||||
const sid = strategyIdRef.current;
|
const sid = strategyIdRef.current;
|
||||||
@@ -189,6 +202,8 @@ const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark
|
|||||||
theme={theme}
|
theme={theme}
|
||||||
onHistoryBarsLoaded={handleHistoryBarsLoaded}
|
onHistoryBarsLoaded={handleHistoryBarsLoaded}
|
||||||
onHistoryLoadingChange={setHistoryLoading}
|
onHistoryLoadingChange={setHistoryLoading}
|
||||||
|
overlayVisibility={overlayVisibility}
|
||||||
|
onToggleOverlay={handleToggleOverlay}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -113,8 +113,9 @@ export function areIchimokuSpanLinesVisible(config: IndicatorConfig): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 구름 영역을 그릴 수 있는지 (선행스팬 + 상승/하락 구름 중 하나 이상 on) */
|
/** 구름 영역을 그릴 수 있는지 (지표 표시 on + 선행스팬 + 구름 색상 설정) */
|
||||||
export function isIchimokuCloudVisible(config: IndicatorConfig): boolean {
|
export function isIchimokuCloudVisible(config: IndicatorConfig): boolean {
|
||||||
|
if (config.hidden === true) return false;
|
||||||
if (!areIchimokuSpanLinesVisible(config)) return false;
|
if (!areIchimokuSpanLinesVisible(config)) return false;
|
||||||
const colors = resolveIchimokuCloudColors(config.cloudColors);
|
const colors = resolveIchimokuCloudColors(config.cloudColors);
|
||||||
return colors.bullishVisible !== false || colors.bearishVisible !== false;
|
return colors.bullishVisible !== false || colors.bearishVisible !== false;
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { IndicatorConfig } from '../types';
|
||||||
|
|
||||||
|
/** 투자관리 차트 탭 — 캔들 오버레이 표시/숨김 (저장 없음, 화면 세션만) */
|
||||||
|
export type PaperOverlayToggleKey = 'ma' | 'ichimoku' | 'bollinger';
|
||||||
|
|
||||||
|
export type PaperOverlayVisibility = Record<PaperOverlayToggleKey, boolean>;
|
||||||
|
|
||||||
|
export const DEFAULT_PAPER_OVERLAY_VISIBILITY: PaperOverlayVisibility = {
|
||||||
|
ma: true,
|
||||||
|
ichimoku: true,
|
||||||
|
bollinger: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MA_TYPES = new Set(['SMA', 'EMA']);
|
||||||
|
|
||||||
|
export const PAPER_OVERLAY_TOGGLE_ORDER: PaperOverlayToggleKey[] = [
|
||||||
|
'ma',
|
||||||
|
'ichimoku',
|
||||||
|
'bollinger',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PAPER_OVERLAY_LABELS: Record<PaperOverlayToggleKey, string> = {
|
||||||
|
ma: '이동평균선',
|
||||||
|
ichimoku: '일목균형표',
|
||||||
|
bollinger: '볼린저 밴드',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 우측 툴바 표시 설정 그룹 메뉴 헤더 */
|
||||||
|
export const PAPER_OVERLAY_MENU_TITLE = '표시 설정';
|
||||||
|
|
||||||
|
export function paperOverlayKeyForType(type: string): PaperOverlayToggleKey | null {
|
||||||
|
if (MA_TYPES.has(type)) return 'ma';
|
||||||
|
if (type === 'IchimokuCloud') return 'ichimoku';
|
||||||
|
if (type === 'BollingerBands') return 'bollinger';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyPaperOverlayVisibility(
|
||||||
|
indicators: IndicatorConfig[],
|
||||||
|
visibility: PaperOverlayVisibility,
|
||||||
|
): IndicatorConfig[] {
|
||||||
|
return indicators.map(ind => {
|
||||||
|
const key = paperOverlayKeyForType(ind.type);
|
||||||
|
if (!key) return ind;
|
||||||
|
const visible = visibility[key];
|
||||||
|
return { ...ind, hidden: !visible };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 투자관리 차트 — 표시 설정 메뉴에 항상 노출할 오버레이 (지표 인스턴스 유무와 무관) */
|
||||||
|
const PAPER_OVERLAY_ALWAYS_MENU: PaperOverlayToggleKey[] = ['ma', 'bollinger'];
|
||||||
|
|
||||||
|
export function listPaperOverlayToggleItems(
|
||||||
|
indicators: IndicatorConfig[],
|
||||||
|
visibility: PaperOverlayVisibility,
|
||||||
|
): Array<{ key: PaperOverlayToggleKey; label: string; visible: boolean }> {
|
||||||
|
const present = new Set<PaperOverlayToggleKey>(PAPER_OVERLAY_ALWAYS_MENU);
|
||||||
|
for (const ind of indicators) {
|
||||||
|
const key = paperOverlayKeyForType(ind.type);
|
||||||
|
if (key) present.add(key);
|
||||||
|
}
|
||||||
|
return PAPER_OVERLAY_TOGGLE_ORDER
|
||||||
|
.filter(key => present.has(key))
|
||||||
|
.map(key => ({
|
||||||
|
key,
|
||||||
|
label: PAPER_OVERLAY_LABELS[key],
|
||||||
|
visible: visibility[key],
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -248,6 +248,41 @@ export function buildStrategyChartIndicators(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MA_OVERLAY_TYPES = new Set(['SMA', 'EMA']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 투자관리 차트 탭 — 캔들 영역에 이동평균(SMA)·볼린저밴드를 기본 포함
|
||||||
|
* (전략 조건에 없어도 표시 설정 메뉴로 표시/숨김 가능)
|
||||||
|
*/
|
||||||
|
export function ensurePaperChartOverlays(
|
||||||
|
indicators: IndicatorConfig[],
|
||||||
|
getParams: GetParams,
|
||||||
|
getVisual: GetVisual,
|
||||||
|
): IndicatorConfig[] {
|
||||||
|
const hasMa = indicators.some(i => MA_OVERLAY_TYPES.has(i.type));
|
||||||
|
const hasBb = indicators.some(i => i.type === 'BollingerBands');
|
||||||
|
const prepend: IndicatorConfig[] = [];
|
||||||
|
|
||||||
|
if (!hasMa) {
|
||||||
|
const sma = buildOneIndicator(
|
||||||
|
{ dslType: 'MA', registryType: 'SMA' },
|
||||||
|
getParams,
|
||||||
|
getVisual,
|
||||||
|
);
|
||||||
|
if (sma) prepend.push(sma);
|
||||||
|
}
|
||||||
|
if (!hasBb) {
|
||||||
|
const bb = buildOneIndicator(
|
||||||
|
{ dslType: 'BOLLINGER', registryType: 'BollingerBands' },
|
||||||
|
getParams,
|
||||||
|
getVisual,
|
||||||
|
);
|
||||||
|
if (bb) prepend.push(bb);
|
||||||
|
}
|
||||||
|
if (prepend.length === 0) return indicators;
|
||||||
|
return [...prepend, ...indicators];
|
||||||
|
}
|
||||||
|
|
||||||
/** 가상투자 차트 — 전략 지표 + 기본 일목균형표 */
|
/** 가상투자 차트 — 전략 지표 + 기본 일목균형표 */
|
||||||
export function buildVirtualTradingChartIndicators(
|
export function buildVirtualTradingChartIndicators(
|
||||||
strategy: StrategyDto | undefined,
|
strategy: StrategyDto | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user