obv 수정
This commit is contained in:
@@ -740,37 +740,49 @@ export class ChartManager {
|
|||||||
|
|
||||||
/** 이중 plot 지표 — addIndicator 루프에서 누락된 plot 시리즈 추가 */
|
/** 이중 plot 지표 — addIndicator 루프에서 누락된 plot 시리즈 추가 */
|
||||||
/**
|
/**
|
||||||
* OBV pane y축 스케일을 최근 N봉 실제 값 기준으로 강제 고정.
|
* OBV pane: plot0(본선)=하반부, plot1(신호선)=상반부로 scale 분리.
|
||||||
* 과거에 OBV가 큰 값을 기록했어도 현재 구간이 y축 하단에 압축되지 않도록 방지.
|
* 두 선이 언제나 pane의 서로 다른 절반에 표시되어 항상 시각적으로 구분됨.
|
||||||
*/
|
*/
|
||||||
private _applyObvTightScale(
|
private _applyObvTightScale(
|
||||||
seriesList: ISeriesApi<SeriesType>[],
|
seriesList: ISeriesApi<SeriesType>[],
|
||||||
plots: Record<string, PlotData>,
|
plots: Record<string, PlotData>,
|
||||||
recentCount = 60,
|
seriesMeta?: IndicatorEntry['seriesMeta'],
|
||||||
): void {
|
): void {
|
||||||
const allRecent: number[] = [];
|
seriesList.forEach((s, i) => {
|
||||||
|
const plotId = seriesMeta?.[i]?.plotId;
|
||||||
|
try {
|
||||||
|
const ps = s.priceScale();
|
||||||
|
if (plotId === 'plot1') {
|
||||||
|
// 신호선: 상반부 (하단 55% 여백)
|
||||||
|
ps.applyOptions({ scaleMargins: { top: 0.03, bottom: 0.55 }, visible: false });
|
||||||
|
} else {
|
||||||
|
// 본선(plot0): 하반부 (상단 55% 여백)
|
||||||
|
ps.applyOptions({ scaleMargins: { top: 0.55, bottom: 0.03 } });
|
||||||
|
}
|
||||||
|
} catch { /* ok */ }
|
||||||
|
});
|
||||||
|
// plot0에 전체 값 범위 기준 autoscaleInfoProvider (과거 극값에 의한 압축 방지)
|
||||||
|
const allPts: number[] = [];
|
||||||
for (const key of ['plot0', 'plot1']) {
|
for (const key of ['plot0', 'plot1']) {
|
||||||
const pts = (plots[key] ?? []).filter(p => p.value != null && isFinite(p.value as number));
|
const pts = (plots[key] ?? []).filter(p => p.value != null && isFinite(p.value as number));
|
||||||
const slice = pts.slice(-recentCount).map(p => p.value as number);
|
allPts.push(...pts.slice(-60).map(p => p.value as number));
|
||||||
allRecent.push(...slice);
|
|
||||||
}
|
}
|
||||||
if (allRecent.length === 0) return;
|
if (allPts.length > 0) {
|
||||||
const minV = Math.min(...allRecent);
|
const minV = Math.min(...allPts);
|
||||||
const maxV = Math.max(...allRecent);
|
const maxV = Math.max(...allPts);
|
||||||
const span = Math.max(1, maxV - minV);
|
const span = Math.max(1, maxV - minV);
|
||||||
const pad = span * 0.3;
|
const pad = span * 0.15;
|
||||||
const tMin = minV - pad;
|
seriesList.forEach(s => {
|
||||||
const tMax = maxV + pad;
|
try {
|
||||||
for (const s of seriesList) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
try {
|
(s as ISeriesApi<any>).applyOptions({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
autoscaleInfoProvider: () => ({
|
||||||
(s as ISeriesApi<any>).applyOptions({
|
priceRange: { minValue: minV - pad, maxValue: maxV + pad },
|
||||||
autoscaleInfoProvider: () => ({
|
margins: { above: 0, below: 0 },
|
||||||
priceRange: { minValue: tMin, maxValue: tMax },
|
}),
|
||||||
margins: { above: 0, below: 0 },
|
});
|
||||||
}),
|
} catch { /* ok */ }
|
||||||
});
|
});
|
||||||
} catch { /* ok */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,7 +860,7 @@ export class ChartManager {
|
|||||||
def,
|
def,
|
||||||
);
|
);
|
||||||
if (config.type === 'OBV' && !indicatorHidden && entry.seriesList.length > 0) {
|
if (config.type === 'OBV' && !indicatorHidden && entry.seriesList.length > 0) {
|
||||||
this._applyObvTightScale(entry.seriesList, result.plots as Record<string, PlotData>);
|
this._applyObvTightScale(entry.seriesList, result.plots as Record<string, PlotData>, entry.seriesMeta);
|
||||||
}
|
}
|
||||||
entry.config = config;
|
entry.config = config;
|
||||||
if (entry.seriesList.length > 0) {
|
if (entry.seriesList.length > 0) {
|
||||||
@@ -935,6 +947,11 @@ export class ChartManager {
|
|||||||
const fallbackColor = config.type === 'OBV' && plotDef.id === 'plot0'
|
const fallbackColor = config.type === 'OBV' && plotDef.id === 'plot0'
|
||||||
? (regPlot?.color ?? '#2196F3')
|
? (regPlot?.color ?? '#2196F3')
|
||||||
: (regPlot?.color ?? plotDef.color ?? '#2962FF');
|
: (regPlot?.color ?? plotDef.color ?? '#2962FF');
|
||||||
|
// OBV: plot1(신호선)은 별도 priceScaleId → 상반부 / plot0(본선)은 하반부
|
||||||
|
const obvSignalScaleId = `obv-sig-${config.id}`;
|
||||||
|
const seriesPriceScaleId = (config.type === 'OBV' && plotDef.id === 'plot1')
|
||||||
|
? obvSignalScaleId
|
||||||
|
: undefined;
|
||||||
series = this.chart.addSeries(LineSeries, {
|
series = this.chart.addSeries(LineSeries, {
|
||||||
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
||||||
lineWidth: (config.type === 'OBV' && plotDef.id === 'plot0'
|
lineWidth: (config.type === 'OBV' && plotDef.id === 'plot0'
|
||||||
@@ -946,6 +963,7 @@ export class ChartManager {
|
|||||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||||
visible: isPlotVisible,
|
visible: isPlotVisible,
|
||||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||||
|
...(seriesPriceScaleId ? { priceScaleId: seriesPriceScaleId } : {}),
|
||||||
}, pane);
|
}, pane);
|
||||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||||
@@ -966,9 +984,9 @@ export class ChartManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// OBV: 최근 50봉 값 기준으로 y축 고정 → 과거 큰 값에 의한 스케일 압축 방지
|
// OBV: plot0=하반부, plot1=상반부로 분리 → 언제나 두 선이 시각적으로 구분됨
|
||||||
if (config.type === 'OBV' && !indicatorHidden && seriesList.length > 0) {
|
if (config.type === 'OBV' && !indicatorHidden && seriesList.length > 0) {
|
||||||
this._applyObvTightScale(seriesList, result.plots as Record<string, PlotData>);
|
this._applyObvTightScale(seriesList, result.plots as Record<string, PlotData>, seriesMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
|
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
|
||||||
@@ -1086,11 +1104,11 @@ export class ChartManager {
|
|||||||
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue;
|
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue;
|
||||||
await this._ensureDualLinePlotSeriesForEntry(entry);
|
await this._ensureDualLinePlotSeriesForEntry(entry);
|
||||||
if (entry.config) this.applyIndicatorStyle(entry.config);
|
if (entry.config) this.applyIndicatorStyle(entry.config);
|
||||||
// OBV: tight scale 재적용 (스타일 갱신 후 autoscaleInfoProvider 덮어써질 수 있음)
|
// OBV: 분리 스케일 재적용 (스타일 갱신 후 scaleMargins 덮어써질 수 있음)
|
||||||
if (entry.type === 'OBV' && entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
if (entry.type === 'OBV' && entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
||||||
try {
|
try {
|
||||||
const result = await calculateIndicator('OBV', this.rawBars.slice(), entry.config?.params ?? {});
|
const result = await calculateIndicator('OBV', this.rawBars.slice(), entry.config?.params ?? {});
|
||||||
this._applyObvTightScale(entry.seriesList, result.plots as Record<string, PlotData>);
|
this._applyObvTightScale(entry.seriesList, result.plots as Record<string, PlotData>, entry.seriesMeta);
|
||||||
} catch { /* ok */ }
|
} catch { /* ok */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3005,6 +3023,21 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OBV: 스타일 갱신 후 pane 분리 scaleMargins 재적용
|
||||||
|
if (entry.type === 'OBV' && entry.seriesList.length > 0) {
|
||||||
|
entry.seriesList.forEach((s, i) => {
|
||||||
|
const pid = entry.seriesMeta[i]?.plotId;
|
||||||
|
try {
|
||||||
|
const ps = s.priceScale();
|
||||||
|
if (pid === 'plot1') {
|
||||||
|
ps.applyOptions({ scaleMargins: { top: 0.03, bottom: 0.55 }, visible: false });
|
||||||
|
} else {
|
||||||
|
ps.applyOptions({ scaleMargins: { top: 0.55, bottom: 0.03 } });
|
||||||
|
}
|
||||||
|
} catch { /* ok */ }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.type === 'BollingerBands') {
|
if (entry.type === 'BollingerBands') {
|
||||||
detachBbBandFill(entry);
|
detachBbBandFill(entry);
|
||||||
if (this._shouldShowBbBandFillForConfig(config)) {
|
if (this._shouldShowBbBandFillForConfig(config)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user