custom 툴발 적용

This commit is contained in:
Macbook
2026-06-03 01:09:06 +09:00
parent 76e5b12d19
commit 88fe92d8c3
8 changed files with 1110 additions and 22 deletions
+143 -20
View File
@@ -64,7 +64,15 @@ import {
type ChartOverlayVisibility,
DEFAULT_CHART_OVERLAY_VISIBILITY,
chartOverlayKeyForType,
isMovingAverageOverlayType,
} from './chartOverlayVisibility';
import {
type ChartCustomOverlaySelection,
type ChartCustomOverlaySlotId,
createDefaultChartCustomOverlaySelection,
isCustomIchimokuCloudVisible,
isCustomPlotSelected,
} from './chartCustomOverlay';
import type { PlotDef, HLineStyle } from './indicatorRegistry';
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils';
import {
@@ -171,8 +179,13 @@ function shouldShowBbBandFill(config: IndicatorConfig): boolean {
return true;
}
function attachBbBandFill(entry: IndicatorEntry, config: IndicatorConfig): void {
if (!shouldShowBbBandFill(config)) {
function attachBbBandFill(
entry: IndicatorEntry,
config: IndicatorConfig,
showFill?: boolean,
): void {
const show = showFill ?? shouldShowBbBandFill(config);
if (!show) {
detachBbBandFill(entry);
return;
}
@@ -240,6 +253,36 @@ export class ChartManager {
private _chartOverlayVisibility: ChartOverlayVisibility = {
...DEFAULT_CHART_OVERLAY_VISIBILITY,
};
private _customOverlaySlots: Record<
ChartCustomOverlaySlotId,
{ active: boolean; selection: ChartCustomOverlaySelection }
> = {
custom: {
active: false,
selection: createDefaultChartCustomOverlaySelection(),
},
custom1: {
active: false,
selection: createDefaultChartCustomOverlaySelection(),
},
};
private _appliedCustomOverlaySlot(): ChartCustomOverlaySlotId | null {
if (this._customOverlaySlots.custom1.active) return 'custom1';
if (this._customOverlaySlots.custom.active) return 'custom';
return null;
}
private _isCustomOverlayApplied(): boolean {
return this._appliedCustomOverlaySlot() !== null;
}
private _getAppliedCustomOverlaySelection(): ChartCustomOverlaySelection {
const slot = this._appliedCustomOverlaySlot();
return slot
? this._customOverlaySlots[slot].selection
: createDefaultChartCustomOverlaySelection();
}
// ── 인디케이터 실시간 갱신 스로틀 ─────────────────────────────────────────
private _indUpdateTimer: ReturnType<typeof setTimeout> | null = null;
@@ -731,7 +774,10 @@ export class ChartManager {
series.setData(mapHistogramSeriesData(filtered, plotDef));
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
} else {
const isPlotVisible = !indicatorHidden && config.plotVisibility?.[plotDef.id] !== false;
const isPlotVisible = !indicatorHidden
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
: config.plotVisibility?.[plotDef.id] !== false);
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
const showSeriesTitle = pane < 2 && showPriceLabel;
series = this.chart.addSeries(LineSeries, {
@@ -804,7 +850,7 @@ export class ChartManager {
this.indicators.set(config.id, entry);
this.applyIndicatorStyle(config);
if (config.type === 'BollingerBands' && !indicatorHidden) {
attachBbBandFill(entry, config);
attachBbBandFill(entry, config, this._shouldShowBbBandFillForConfig(config));
entry.bbFillPrimitive?.requestRefresh();
}
} catch (e) {
@@ -1790,13 +1836,21 @@ export class ChartManager {
// bullishVisible/bearishVisible 가 둘 다 false 이면 DB 잘못된 저장값으로 간주하고
// 기본값(표시)으로 복원한다. 하나만 false 인 경우는 사용자 의도이므로 유지.
const bullishVis = colors.bullishVisible !== false;
const bearishVis = colors.bearishVisible !== false;
const effectiveBullish = (!bullishVis && !bearishVis) ? true : bullishVis;
const effectiveBearish = (!bullishVis && !bearishVis) ? true : bearishVis;
let bullishVis = colors.bullishVisible !== false;
let bearishVis = colors.bearishVisible !== false;
if (this._isCustomOverlayApplied()) {
const sel = this._getAppliedCustomOverlaySelection();
bullishVis = isCustomIchimokuCloudVisible(sel, 'bullish');
bearishVis = isCustomIchimokuCloudVisible(sel, 'bearish');
} else {
const effectiveBullish = (!bullishVis && !bearishVis) ? true : bullishVis;
const effectiveBearish = (!bullishVis && !bearishVis) ? true : bearishVis;
bullishVis = effectiveBullish;
bearishVis = effectiveBearish;
}
plugin.setColors(colors.bullishColor, colors.bearishColor);
plugin.setVisibility(effectiveBullish, effectiveBearish);
plugin.setVisibility(bullishVis, bearishVis);
if (!this._isIndicatorEffectivelyVisible(config)) {
plugin.setCloudData([]);
@@ -2379,16 +2433,87 @@ export class ChartManager {
return config.hidden !== true;
}
getChartCustomOverlayActive(slot: ChartCustomOverlaySlotId = 'custom'): boolean {
return this._customOverlaySlots[slot].active;
}
getChartCustomOverlaySelection(slot: ChartCustomOverlaySlotId = 'custom'): ChartCustomOverlaySelection {
const sel = this._customOverlaySlots[slot].selection;
return {
smaPlots: { ...sel.smaPlots },
bollinger: { ...sel.bollinger },
ichimoku: { ...sel.ichimoku },
candle: sel.candle,
};
}
setChartCustomOverlaySelection(
selection: ChartCustomOverlaySelection,
slot: ChartCustomOverlaySlotId = 'custom',
): void {
this._customOverlaySlots[slot].selection = {
smaPlots: { ...selection.smaPlots },
bollinger: { ...selection.bollinger },
ichimoku: { ...selection.ichimoku },
candle: selection.candle,
};
if (this._customOverlaySlots[slot].active) {
this.syncChartOverlayVisibility();
}
}
/** 슬롯 적용 시 다른 슬롯은 자동 미적용 (Custom ↔ Custom1 상호 배타) */
setChartCustomOverlayActive(active: boolean, slot: ChartCustomOverlaySlotId = 'custom'): void {
if (this._customOverlaySlots[slot].active === active) return;
this._customOverlaySlots[slot].active = active;
if (active) {
const other: ChartCustomOverlaySlotId = slot === 'custom' ? 'custom1' : 'custom';
this._customOverlaySlots[other].active = false;
}
this.syncChartOverlayVisibility();
}
/** custom 미적용 시 지표 설정 plotVisibility, 적용 시 custom 선택만 반영 */
private _resolveSeriesVisible(config: IndicatorConfig, plotId: string | undefined): boolean {
if (!plotId || !this._isIndicatorEffectivelyVisible(config)) return false;
if (!this._isCustomOverlayApplied()) {
return config.plotVisibility?.[plotId] !== false;
}
if (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud') {
return isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotId);
}
if (isMovingAverageOverlayType(config.type)) {
return config.plotVisibility?.[plotId] !== false;
}
return config.plotVisibility?.[plotId] !== false;
}
private _resolveCandleVisible(): boolean {
if (!this._chartOverlayVisibility.candle) return false;
if (!this._isCustomOverlayApplied()) return true;
return this._getAppliedCustomOverlaySelection().candle;
}
private _shouldShowBbBandFillForConfig(config: IndicatorConfig): boolean {
if (!this._isIndicatorEffectivelyVisible(config)) return false;
if (!this._isCustomOverlayApplied()) {
const bg = resolveBbBandBackground(config.bandBackground);
if (!bg.visible) return false;
if (config.plotVisibility?.plot1 === false || config.plotVisibility?.plot2 === false) return false;
return true;
}
const b = this._getAppliedCustomOverlaySelection().bollinger;
return b.background && b.upper && b.lower;
}
/** 오버레이·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);
const visible = this._resolveSeriesVisible(config, plotId);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
series.applyOptions({ visible } as any);
@@ -2402,7 +2527,7 @@ export class ChartManager {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.mainSeries as ISeriesApi<any>).applyOptions({
visible: this._chartOverlayVisibility.candle,
visible: this._resolveCandleVisible(),
});
} catch { /* ok */ }
}
@@ -2416,8 +2541,8 @@ export class ChartManager {
if (entry.type === 'BollingerBands') {
detachBbBandFill(entry);
if (this._isIndicatorEffectivelyVisible(config)) {
attachBbBandFill(entry, config);
if (this._shouldShowBbBandFillForConfig(config)) {
attachBbBandFill(entry, config, true);
entry.bbFillPrimitive?.requestRefresh();
}
}
@@ -2551,15 +2676,13 @@ export class ChartManager {
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];
const visible = indicatorVisible
&& (pid == null || config.plotVisibility?.[pid] !== false);
const visible = this._resolveSeriesVisible(config, pid);
if (!plot) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
series.applyOptions({ visible } as any);
@@ -2645,8 +2768,8 @@ export class ChartManager {
if (entry.type === 'BollingerBands') {
detachBbBandFill(entry);
if (indicatorVisible) {
attachBbBandFill(entry, config);
if (this._shouldShowBbBandFillForConfig(config)) {
attachBbBandFill(entry, config, true);
entry.bbFillPrimitive?.requestRefresh();
}
}