custom 툴발 적용
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { SMA_MA_COUNT, smaPlotId } from './smaConfig';
|
||||
|
||||
/** Custom / Custom1 — 동일 기능, 설정만 분리 (세션만, DB 미저장) */
|
||||
export type ChartCustomOverlaySlotId = 'custom' | 'custom1';
|
||||
|
||||
export const CHART_CUSTOM_OVERLAY_SLOTS: ChartCustomOverlaySlotId[] = ['custom', 'custom1'];
|
||||
|
||||
/** Custom 적용 시 표시할 항목 (세션만, DB 미저장) */
|
||||
export type ChartCustomOverlaySelection = {
|
||||
smaPlots: Record<string, boolean>;
|
||||
bollinger: {
|
||||
basis: boolean;
|
||||
upper: boolean;
|
||||
lower: boolean;
|
||||
background: boolean;
|
||||
};
|
||||
ichimoku: {
|
||||
conversion: boolean;
|
||||
base: boolean;
|
||||
lagging: boolean;
|
||||
spanA: boolean;
|
||||
spanB: boolean;
|
||||
bullishCloud: boolean;
|
||||
bearishCloud: boolean;
|
||||
};
|
||||
candle: boolean;
|
||||
};
|
||||
|
||||
export const BB_CUSTOM_PLOT_IDS = ['plot0', 'plot1', 'plot2'] as const;
|
||||
export const BB_CUSTOM_LABELS: Record<(typeof BB_CUSTOM_PLOT_IDS)[number], string> = {
|
||||
plot0: '중앙값',
|
||||
plot1: '어퍼',
|
||||
plot2: '로우어',
|
||||
};
|
||||
|
||||
export function createDefaultChartCustomOverlaySelection(): ChartCustomOverlaySelection {
|
||||
const smaPlots: Record<string, boolean> = {};
|
||||
for (let i = 0; i < SMA_MA_COUNT; i++) {
|
||||
smaPlots[smaPlotId(i)] = true;
|
||||
}
|
||||
return {
|
||||
smaPlots,
|
||||
bollinger: {
|
||||
basis: true,
|
||||
upper: true,
|
||||
lower: true,
|
||||
background: true,
|
||||
},
|
||||
ichimoku: {
|
||||
conversion: true,
|
||||
base: true,
|
||||
lagging: true,
|
||||
spanA: true,
|
||||
spanB: true,
|
||||
bullishCloud: true,
|
||||
bearishCloud: true,
|
||||
},
|
||||
candle: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function cloneChartCustomOverlaySelection(
|
||||
sel: ChartCustomOverlaySelection,
|
||||
): ChartCustomOverlaySelection {
|
||||
return {
|
||||
smaPlots: { ...sel.smaPlots },
|
||||
bollinger: { ...sel.bollinger },
|
||||
ichimoku: { ...sel.ichimoku },
|
||||
candle: sel.candle,
|
||||
};
|
||||
}
|
||||
|
||||
export function smaMaLabel(plotIndex: number): string {
|
||||
return `MA${plotIndex + 1}`;
|
||||
}
|
||||
|
||||
export const ICHIMOKU_CUSTOM_ROWS: Array<{
|
||||
key: keyof ChartCustomOverlaySelection['ichimoku'];
|
||||
plotId?: string;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: 'conversion', plotId: 'plot0', label: '전환선' },
|
||||
{ key: 'base', plotId: 'plot1', label: '기준선' },
|
||||
{ key: 'lagging', plotId: 'plot2', label: '후행스팬' },
|
||||
{ key: 'spanA', plotId: 'plot3', label: '선행스팬1' },
|
||||
{ key: 'spanB', plotId: 'plot4', label: '선행스팬2' },
|
||||
{ key: 'bullishCloud', label: '상승 구름' },
|
||||
{ key: 'bearishCloud', label: '하락 구름' },
|
||||
];
|
||||
|
||||
/** 지표 설정 plotVisibility 와 무관 — custom 적용 시 이 맵만 사용 */
|
||||
export function isCustomPlotSelected(
|
||||
sel: ChartCustomOverlaySelection,
|
||||
type: string,
|
||||
plotId: string,
|
||||
): boolean {
|
||||
if (type === 'SMA') {
|
||||
return sel.smaPlots[plotId] === true;
|
||||
}
|
||||
if (type === 'BollingerBands') {
|
||||
if (plotId === 'plot0') return sel.bollinger.basis;
|
||||
if (plotId === 'plot1') return sel.bollinger.upper;
|
||||
if (plotId === 'plot2') return sel.bollinger.lower;
|
||||
return false;
|
||||
}
|
||||
if (type === 'IchimokuCloud') {
|
||||
const row = ICHIMOKU_CUSTOM_ROWS.find(r => r.plotId === plotId);
|
||||
if (!row) return false;
|
||||
return sel.ichimoku[row.key] === true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isCustomIchimokuCloudVisible(
|
||||
sel: ChartCustomOverlaySelection,
|
||||
which: 'bullish' | 'bearish',
|
||||
): boolean {
|
||||
return which === 'bullish'
|
||||
? sel.ichimoku.bullishCloud
|
||||
: sel.ichimoku.bearishCloud;
|
||||
}
|
||||
|
||||
export type CustomOverlaySectionId = 'sma' | 'bollinger' | 'ichimoku' | 'candle';
|
||||
|
||||
export function isCustomSectionAllSelected(
|
||||
sel: ChartCustomOverlaySelection,
|
||||
section: CustomOverlaySectionId,
|
||||
): boolean {
|
||||
switch (section) {
|
||||
case 'sma':
|
||||
return Array.from({ length: SMA_MA_COUNT }, (_, i) =>
|
||||
sel.smaPlots[smaPlotId(i)] !== false,
|
||||
).every(Boolean);
|
||||
case 'bollinger':
|
||||
return sel.bollinger.basis
|
||||
&& sel.bollinger.upper
|
||||
&& sel.bollinger.lower
|
||||
&& sel.bollinger.background;
|
||||
case 'ichimoku':
|
||||
return ICHIMOKU_CUSTOM_ROWS.every(r => sel.ichimoku[r.key]);
|
||||
case 'candle':
|
||||
return sel.candle;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function setCustomSectionAllSelected(
|
||||
sel: ChartCustomOverlaySelection,
|
||||
section: CustomOverlaySectionId,
|
||||
on: boolean,
|
||||
): void {
|
||||
switch (section) {
|
||||
case 'sma':
|
||||
for (let i = 0; i < SMA_MA_COUNT; i++) {
|
||||
sel.smaPlots[smaPlotId(i)] = on;
|
||||
}
|
||||
break;
|
||||
case 'bollinger':
|
||||
sel.bollinger.basis = on;
|
||||
sel.bollinger.upper = on;
|
||||
sel.bollinger.lower = on;
|
||||
sel.bollinger.background = on;
|
||||
break;
|
||||
case 'ichimoku':
|
||||
for (const row of ICHIMOKU_CUSTOM_ROWS) {
|
||||
sel.ichimoku[row.key] = on;
|
||||
}
|
||||
break;
|
||||
case 'candle':
|
||||
sel.candle = on;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export type ChartCustomOverlaySlotState = {
|
||||
active: boolean;
|
||||
selection: ChartCustomOverlaySelection;
|
||||
};
|
||||
|
||||
/** UI·Manager 공통: 한 번에 하나의 슬롯만 적용 (다른 슬롯 적용 시 기존 슬롯 자동 해제) */
|
||||
export function isCustomOverlaySlotActive(
|
||||
activeSlot: ChartCustomOverlaySlotId | null,
|
||||
slot: ChartCustomOverlaySlotId,
|
||||
): boolean {
|
||||
return activeSlot === slot;
|
||||
}
|
||||
|
||||
export function resolveActiveCustomOverlaySlot(
|
||||
slot: ChartCustomOverlaySlotId,
|
||||
turnOn: boolean,
|
||||
current: ChartCustomOverlaySlotId | null,
|
||||
): ChartCustomOverlaySlotId | null {
|
||||
if (turnOn) return slot;
|
||||
return current === slot ? null : current;
|
||||
}
|
||||
|
||||
type CustomOverlayManagerApi = {
|
||||
setChartCustomOverlaySelection(
|
||||
selection: ChartCustomOverlaySelection,
|
||||
slot: ChartCustomOverlaySlotId,
|
||||
): void;
|
||||
setChartCustomOverlayActive(active: boolean, slot: ChartCustomOverlaySlotId): void;
|
||||
};
|
||||
|
||||
/** React → ChartManager: 두 슬롯 선택값 동기화 후 적용 중인 슬롯만 활성화 */
|
||||
export function syncChartManagerCustomOverlays(
|
||||
mgr: CustomOverlayManagerApi,
|
||||
custom: ChartCustomOverlaySlotState,
|
||||
custom1: ChartCustomOverlaySlotState,
|
||||
): void {
|
||||
mgr.setChartCustomOverlaySelection(custom.selection, 'custom');
|
||||
mgr.setChartCustomOverlaySelection(custom1.selection, 'custom1');
|
||||
mgr.setChartCustomOverlayActive(false, 'custom');
|
||||
mgr.setChartCustomOverlayActive(false, 'custom1');
|
||||
if (custom1.active) {
|
||||
mgr.setChartCustomOverlayActive(true, 'custom1');
|
||||
} else if (custom.active) {
|
||||
mgr.setChartCustomOverlayActive(true, 'custom');
|
||||
}
|
||||
}
|
||||
|
||||
/** 적용 슬롯 하나만 — Custom 적용 중 Custom1 적용 시 Custom 자동 미적용 */
|
||||
export function syncChartManagerCustomOverlaysByActiveSlot(
|
||||
mgr: CustomOverlayManagerApi,
|
||||
activeSlot: ChartCustomOverlaySlotId | null,
|
||||
customSelection: ChartCustomOverlaySelection,
|
||||
custom1Selection: ChartCustomOverlaySelection,
|
||||
): void {
|
||||
syncChartManagerCustomOverlays(
|
||||
mgr,
|
||||
{ active: activeSlot === 'custom', selection: customSelection },
|
||||
{ active: activeSlot === 'custom1', selection: custom1Selection },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user