캔들차트, 이동평균선, 일목균형표, 볼린저밴드 보이기 숨기기 기능적용
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user