캔들차트, 이동평균선, 일목균형표, 볼린저밴드 보이기 숨기기 기능적용

This commit is contained in:
Macbook
2026-06-03 00:19:44 +09:00
parent 08fb442d4a
commit 76e5b12d19
15 changed files with 427 additions and 69 deletions
+120 -9
View File
@@ -59,6 +59,12 @@ import {
resolveIchimokuCloudColors,
resolveIchimokuDisplacements,
} from './ichimokuConfig';
import {
type ChartOverlayToggleKey,
type ChartOverlayVisibility,
DEFAULT_CHART_OVERLAY_VISIBILITY,
chartOverlayKeyForType,
} from './chartOverlayVisibility';
import type { PlotDef, HLineStyle } from './indicatorRegistry';
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils';
import {
@@ -230,6 +236,10 @@ export class ChartManager {
/** 실시간 전략 체크 마커 */
private liveStrategyMarkers: MarkerData[] = [];
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;
@@ -668,7 +678,7 @@ export class ChartManager {
config = enrichIndicatorConfig(config);
if (this.indicators.has(config.id) || this.rawBars.length === 0) return;
const dataGenAtStart = this._dataGeneration;
const indicatorHidden = config.hidden === true;
const indicatorHidden = !this._isIndicatorEffectivelyVisible(config);
const def = getIndicatorDef(config.type);
const seriesList: ISeriesApi<SeriesType>[] = [];
const seriesMeta: IndicatorEntry['seriesMeta'] = [];
@@ -855,6 +865,7 @@ export class ChartManager {
if (configs.length > 0) {
this._finalizeIndicatorPaneLayout();
this._refreshAllIchimokuClouds();
this.syncChartOverlayVisibility();
}
}
@@ -930,6 +941,7 @@ export class ChartManager {
this._finalizeIndicatorPaneLayout();
this._refreshAllIchimokuClouds();
this.syncChartOverlayVisibility();
}
removeIndicator(id: string): void {
@@ -1786,7 +1798,7 @@ export class ChartManager {
plugin.setColors(colors.bullishColor, colors.bearishColor);
plugin.setVisibility(effectiveBullish, effectiveBearish);
if (config.hidden === true) {
if (!this._isIndicatorEffectivelyVisible(config)) {
plugin.setCloudData([]);
return;
}
@@ -2343,6 +2355,89 @@ export class ChartManager {
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 옵션 변경) */
toggleIndicatorVisible(id: string, visible: boolean): void {
const entry = this.indicators.get(id);
@@ -2451,19 +2546,25 @@ export class ChartManager {
config = enrichIndicatorConfig(config);
const entry = this.indicators.get(config.id);
if (!entry) return;
entry.config = config;
const prevUserHidden = entry.config?.hidden === true;
const def = getIndicatorDef(config.type);
const plotDefs = config.plots ?? def?.plots ?? [];
const plotById = Object.fromEntries(plotDefs.map((p: PlotDef) => [p.id, p]));
const indicatorVisible = this._isIndicatorEffectivelyVisible(config);
entry.seriesList.forEach((series, i) => {
let plot: PlotDef | undefined;
const pid = entry.seriesMeta[i]?.plotId;
plot = (pid ? plotById[pid] : undefined) ?? plotDefs[i];
if (!plot) return;
const indicatorVisible = config.hidden !== true;
const visible = indicatorVisible && config.plotVisibility?.[plot.id] !== false;
const visible = indicatorVisible
&& (pid == null || config.plotVisibility?.[pid] !== false);
if (!plot) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
series.applyOptions({ visible } as any);
return;
}
const paneIdx = entry.paneIndex ?? 0;
const indicatorPriceFormat = priceFormatForIndicatorPane(paneIdx);
@@ -2516,7 +2617,7 @@ export class ChartManager {
entry.fillPrimitive = undefined;
}
if (config.hidden !== true) {
if (indicatorVisible) {
for (const hl of hlines) {
if (hl.visible === false) continue;
const pl = firstSeries.createPriceLine({
@@ -2544,7 +2645,7 @@ export class ChartManager {
if (entry.type === 'BollingerBands') {
detachBbBandFill(entry);
if (config.hidden !== true) {
if (indicatorVisible) {
attachBbBandFill(entry, config);
entry.bbFillPrimitive?.requestRefresh();
}
@@ -2560,7 +2661,17 @@ export class ChartManager {
}).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)) {
void this._repaintHistogramSeries(config.id, config);
}
+2
View File
@@ -423,6 +423,8 @@ export interface AppSettingsDto {
chartLiveReceiveHighlight?: boolean;
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */
chartCrosshairInfoVisible?: boolean;
/** 차트 마우스오버 줌·이동 플로팅 툴바 표시 (기본 true) */
chartHoverToolbarVisible?: boolean;
/** 차트 상단 범례(tv-legend) 항목별 표시 옵션 */
chartLegendOptions?: Record<string, boolean> | null;
/** 차트 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,
};
const MA_TYPES = new Set(['SMA', 'EMA']);
import { isMovingAverageOverlayType } from './chartOverlayVisibility';
export const PAPER_OVERLAY_TOGGLE_ORDER: PaperOverlayToggleKey[] = [
'ma',
@@ -29,7 +29,7 @@ export const PAPER_OVERLAY_LABELS: Record<PaperOverlayToggleKey, string> = {
export const PAPER_OVERLAY_MENU_TITLE = '표시 설정';
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 === 'BollingerBands') return 'bollinger';
return null;