macd 수정

This commit is contained in:
Macbook
2026-06-16 01:36:42 +09:00
parent 52300379c4
commit aac0641c86
2 changed files with 134 additions and 14 deletions
+130 -14
View File
@@ -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> = {