macd 수정
This commit is contained in:
@@ -59,8 +59,10 @@ const INDICATOR_PRICE_FORMAT = {
|
||||
formatter: formatIndicatorAxisPrice,
|
||||
};
|
||||
|
||||
function priceFormatForIndicatorPane(paneIndex: number) {
|
||||
return paneIndex >= 2 ? INDICATOR_PRICE_FORMAT : undefined;
|
||||
/** 서브 pane 보조지표 — 볼륨 숨김(pane 1)에서도 지표 전용 포맷 사용 */
|
||||
function priceFormatForIndicatorSeries(def: ReturnType<typeof getIndicatorDef> | undefined) {
|
||||
if (!def || def.overlay || def.returnsMarkers) return undefined;
|
||||
return INDICATOR_PRICE_FORMAT;
|
||||
}
|
||||
import {
|
||||
getIchimokuPlotTitle,
|
||||
@@ -162,6 +164,10 @@ interface IndicatorEntry {
|
||||
bbFillPrimitive?: BollingerBandFillPrimitive;
|
||||
config: IndicatorConfig;
|
||||
cloudPlugin?: IchimokuCloudPlugin; // 일목균형표 구름 플러그인
|
||||
/** pane 'right' 축 오염 방지용 전용 priceScaleId (OBV 제외 서브 pane 지표) */
|
||||
isolatedScaleId?: string;
|
||||
/** isolatedScale autoscaleInfoProvider 초기 min/max */
|
||||
plotAutoscaleBounds?: { min: number; max: number };
|
||||
}
|
||||
|
||||
interface AlertEntry { price: number; label: string; line: IPriceLine; }
|
||||
@@ -181,9 +187,11 @@ function seriesByPlotId(
|
||||
return i >= 0 ? entry.seriesList[i] : undefined;
|
||||
}
|
||||
|
||||
/** hline·음영 프리미티브 부착 대상 — histogram+line 지표(MACD 등)는 line 시리즈에 부착 */
|
||||
/** hline·음영 프리미티브 부착 — histogram 이 있으면 0선과 동일 스케일·기준선에 부착 */
|
||||
function hlineAnchorSeries(entry: Pick<IndicatorEntry, 'seriesList' | 'seriesMeta'>): ISeriesApi<SeriesType> | undefined {
|
||||
if (entry.seriesList.length === 0) return undefined;
|
||||
const histIdx = entry.seriesMeta.findIndex(m => m.isHistogram);
|
||||
if (histIdx >= 0) return entry.seriesList[histIdx];
|
||||
const lineIdx = entry.seriesMeta.findIndex(m => !m.isHistogram);
|
||||
return entry.seriesList[lineIdx >= 0 ? lineIdx : 0];
|
||||
}
|
||||
@@ -654,7 +662,7 @@ export class ChartManager {
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
for (const entry of this.indicators.values()) {
|
||||
const fmt = priceFormatForIndicatorPane(entry.paneIndex ?? 0);
|
||||
const fmt = priceFormatForIndicatorSeries(getIndicatorDef(entry.type));
|
||||
if (!fmt) continue;
|
||||
for (const series of entry.seriesList) {
|
||||
try {
|
||||
@@ -858,7 +866,7 @@ export class ChartManager {
|
||||
const plotById = Object.fromEntries(
|
||||
sortPlotDefsForSeriesAdd('OBV', enriched.plots ?? def?.plots ?? []).map(p => [p.id, p]),
|
||||
);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorSeries(def);
|
||||
|
||||
// 본선(plot0)·신호선(plot1) 을 같은 가격 범위로 고정.
|
||||
// 재렌더·pane 재배치 과정에서 스케일이 오염되면 본선이 납작하게 눌릴 수 있어,
|
||||
@@ -969,9 +977,12 @@ export class ChartManager {
|
||||
pane: number,
|
||||
indicatorHidden: boolean,
|
||||
def: ReturnType<typeof getIndicatorDef>,
|
||||
scaleOpts: ReturnType<ChartManager['_isolatedScaleSeriesOptions']> = null,
|
||||
): void {
|
||||
const plotDefs = this._dualLinePlotDefsForRender(config, def);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorSeries(def);
|
||||
const scaleExtra = scaleOpts ?? {};
|
||||
let targetPane = pane;
|
||||
|
||||
for (const plotDef of plotDefs) {
|
||||
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
||||
@@ -994,8 +1005,13 @@ export class ChartManager {
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...scaleExtra,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
}, targetPane);
|
||||
if (seriesList.length === 0) {
|
||||
const actual = this._readSeriesPaneIndex(series);
|
||||
if (actual >= 0) targetPane = actual;
|
||||
}
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesList.push(series);
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
@@ -1013,7 +1029,7 @@ export class ChartManager {
|
||||
): void {
|
||||
const plotDefs = this._dualLinePlotDefsForRender(config, def);
|
||||
const existingIds = new Set(seriesMeta.map(m => m.plotId));
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorSeries(def);
|
||||
|
||||
for (const plotDef of plotDefs) {
|
||||
if (existingIds.has(plotDef.id)) continue;
|
||||
@@ -1159,6 +1175,9 @@ export class ChartManager {
|
||||
}
|
||||
|
||||
const plotDefsRaw = config.plots ?? def?.plots ?? [];
|
||||
const plotRange = this._computePlotsValueRange(result.plots as Record<string, PlotData>);
|
||||
const scaleOpts = this._isolatedScaleSeriesOptions(config, plotRange);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorSeries(def);
|
||||
|
||||
if (config.type === 'OBV') {
|
||||
this._addObvIndicatorSeries(
|
||||
@@ -1179,8 +1198,10 @@ export class ChartManager {
|
||||
pane,
|
||||
indicatorHidden,
|
||||
def,
|
||||
scaleOpts,
|
||||
);
|
||||
} else {
|
||||
let targetPane = pane;
|
||||
for (const plotDef of plotDefsRaw) {
|
||||
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
||||
if (!plotData || plotData.length === 0) continue;
|
||||
@@ -1188,12 +1209,14 @@ export class ChartManager {
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
let series: ISeriesApi<SeriesType>;
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
const scaleExtra = scaleOpts ?? {};
|
||||
if (plotDef.type === 'histogram') {
|
||||
series = this.chart.addSeries(HistogramSeries, {
|
||||
color: plotDef.color,
|
||||
base: 0,
|
||||
...scaleExtra,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
}, targetPane);
|
||||
series.setData(mapHistogramSeriesData(filtered, plotDef));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
|
||||
} else {
|
||||
@@ -1212,12 +1235,17 @@ export class ChartManager {
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...scaleExtra,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
}, targetPane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
}
|
||||
|
||||
if (seriesList.length === 0) {
|
||||
const actual = this._readSeriesPaneIndex(series);
|
||||
if (actual >= 0) targetPane = actual;
|
||||
}
|
||||
seriesList.push(series);
|
||||
}
|
||||
}
|
||||
@@ -1275,9 +1303,15 @@ export class ChartManager {
|
||||
const entry: IndicatorEntry = {
|
||||
id: config.id, type: config.type, seriesList, seriesMeta,
|
||||
paneIndex: pane, alertLines: [], hlineRefs, fillPrimitive, config,
|
||||
isolatedScaleId: scaleOpts?.priceScaleId,
|
||||
plotAutoscaleBounds: plotRange ?? undefined,
|
||||
};
|
||||
const actualPane = this._readSeriesPaneIndex(seriesList[0]);
|
||||
if (actualPane >= 2) entry.paneIndex = actualPane;
|
||||
const minSub = this._minIndicatorSubPane();
|
||||
if (actualPane >= minSub) entry.paneIndex = actualPane;
|
||||
if (entry.isolatedScaleId != null && entry.paneIndex != null && entry.paneIndex >= minSub) {
|
||||
this._applyIsolatedScalePaneOptions(entry.isolatedScaleId, entry.paneIndex);
|
||||
}
|
||||
if (this._indicatorLoadStale(dataGenAtStart) || this.indicators.has(config.id)) {
|
||||
this._discardIndicatorSeries(seriesList, { fillPrimitive });
|
||||
return;
|
||||
@@ -2145,6 +2179,86 @@ export class ChartManager {
|
||||
}
|
||||
}
|
||||
|
||||
private _indicatorScaleId(configId: string): string {
|
||||
return `ind-${configId}`;
|
||||
}
|
||||
|
||||
/** plot 데이터 min/max — 전용 가격축 autoscale 용 */
|
||||
private _computePlotsValueRange(
|
||||
plots: Record<string, PlotData>,
|
||||
plotIds?: string[],
|
||||
): { min: number; max: number } | null {
|
||||
let lo = Infinity;
|
||||
let hi = -Infinity;
|
||||
const ids = plotIds ?? Object.keys(plots);
|
||||
for (const id of ids) {
|
||||
for (const p of plots[id] ?? []) {
|
||||
if (Number.isFinite(p.value)) {
|
||||
lo = Math.min(lo, p.value);
|
||||
hi = Math.max(hi, p.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(lo) || !Number.isFinite(hi)) return null;
|
||||
return { min: lo, max: hi };
|
||||
}
|
||||
|
||||
/** OBV 제외 서브 pane 지표 — pane 공유 'right' 축 오염(종가·타 지표 값 혼입) 방지 */
|
||||
private _usesIsolatedPriceScale(type: string): boolean {
|
||||
if (type === 'OBV') return false;
|
||||
const def = getIndicatorDef(type);
|
||||
return !!(def && !def.overlay && !def.returnsMarkers);
|
||||
}
|
||||
|
||||
private _makeIsolatedAutoscaleProvider(lo: number, hi: number) {
|
||||
return (baseImpl: () => { priceRange: { minValue: number; maxValue: number } } | null) => {
|
||||
let minV = lo;
|
||||
let maxV = hi;
|
||||
const base = baseImpl?.();
|
||||
if (base?.priceRange) {
|
||||
minV = Math.min(minV, base.priceRange.minValue);
|
||||
maxV = Math.max(maxV, base.priceRange.maxValue);
|
||||
}
|
||||
minV = Math.min(minV, 0);
|
||||
maxV = Math.max(maxV, 0);
|
||||
const pad = (maxV - minV) * 0.08 || Math.abs(maxV) * 0.08 || 1;
|
||||
return { priceRange: { minValue: minV - pad, maxValue: maxV + pad } };
|
||||
};
|
||||
}
|
||||
|
||||
private _isolatedScaleSeriesOptions(
|
||||
config: IndicatorConfig,
|
||||
plotRange: { min: number; max: number } | null,
|
||||
): {
|
||||
priceScaleId: string;
|
||||
autoscaleInfoProvider?: ReturnType<ChartManager['_makeIsolatedAutoscaleProvider']>;
|
||||
} | null {
|
||||
if (!this._usesIsolatedPriceScale(config.type)) return null;
|
||||
const scaleId = this._indicatorScaleId(config.id);
|
||||
if (!plotRange || plotRange.max <= plotRange.min) {
|
||||
return { priceScaleId: scaleId };
|
||||
}
|
||||
let { min, max } = plotRange;
|
||||
const def = getIndicatorDef(config.type);
|
||||
if (def?.hlines?.some(h => h.price === 0)) {
|
||||
min = Math.min(min, 0);
|
||||
max = Math.max(max, 0);
|
||||
}
|
||||
return {
|
||||
priceScaleId: scaleId,
|
||||
autoscaleInfoProvider: this._makeIsolatedAutoscaleProvider(min, max),
|
||||
};
|
||||
}
|
||||
|
||||
private _applyIsolatedScalePaneOptions(scaleId: string, pane: number): void {
|
||||
try {
|
||||
this.chart.priceScale(scaleId, pane).applyOptions({
|
||||
visible: true,
|
||||
scaleMargins: { top: 0.12, bottom: 0.12 },
|
||||
});
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
|
||||
/** 볼륨 pane(1)이 실제로 표시되는지 여부 */
|
||||
private _volumePaneShown(): boolean {
|
||||
return this._volumePaneEnabled && this._volumeVisible && !this._candleOnlyLayout;
|
||||
@@ -2269,7 +2383,7 @@ export class ChartManager {
|
||||
if (!moved && entry.paneIndex === target) continue;
|
||||
entry.paneIndex = target;
|
||||
|
||||
// 통일된 pane 에서 OBV 전용 축 옵션 재확정 (이동 중 손실 방지)
|
||||
// 통일된 pane 에서 OBV·전용축 지표 옵션 재확정 (이동 중 손실 방지)
|
||||
if (entry.type === 'OBV' && entry.config) {
|
||||
try {
|
||||
this.chart.priceScale(`obv-${entry.config.id}`, target).applyOptions({
|
||||
@@ -2277,6 +2391,8 @@ export class ChartManager {
|
||||
scaleMargins: { top: 0.12, bottom: 0.12 },
|
||||
});
|
||||
} catch { /* ok */ }
|
||||
} else if (entry.isolatedScaleId) {
|
||||
this._applyIsolatedScalePaneOptions(entry.isolatedScaleId, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3494,7 +3610,7 @@ export class ChartManager {
|
||||
return;
|
||||
}
|
||||
const paneIdx = entry.paneIndex ?? 0;
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(paneIdx);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorSeries(def);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const opts: Record<string, unknown> = {
|
||||
|
||||
Reference in New Issue
Block a user