최종이길
This commit is contained in:
@@ -225,6 +225,12 @@ function sortDualLinePlotDefs(plotDefs: PlotDef[]): PlotDef[] {
|
||||
);
|
||||
}
|
||||
|
||||
/** 이중 plot 지표 시리즈 추가 순서 — OBV는 plot1→plot0 순으로 본선이 위에 그려지게 */
|
||||
function sortPlotDefsForSeriesAdd(type: string, plotDefs: PlotDef[]): PlotDef[] {
|
||||
const sorted = sortDualLinePlotDefs(plotDefs);
|
||||
return type === 'OBV' ? [...sorted].reverse() : sorted;
|
||||
}
|
||||
|
||||
/** OBV 본선(plot0) — DB plotVisibility·투명색으로 숨김 방지 */
|
||||
function isDualLinePlotForcedVisible(type: string, plotId: string): boolean {
|
||||
return type === 'OBV' && plotId === 'plot0';
|
||||
@@ -738,63 +744,23 @@ export class ChartManager {
|
||||
return loadGen !== this._dataGeneration;
|
||||
}
|
||||
|
||||
/** 이중 plot 지표 — addIndicator 루프에서 누락된 plot 시리즈 추가 */
|
||||
/**
|
||||
* OBV pane: plot0(본선)=하반부, plot1(신호선)=상반부로 분리.
|
||||
* 각 series의 autoscaleInfoProvider에서 priceRange를 오프셋 처리하여
|
||||
* 독립 scale에서도 항상 pane의 서로 다른 절반에 표시되도록 보장.
|
||||
*
|
||||
* 원리: plot0는 [pMin, pMax + pSpan] 범위를 사용 → 실제 데이터가 하반부에 위치
|
||||
* plot1는 [pMin - pSpan, pMax] 범위를 사용 → 실제 데이터가 상반부에 위치
|
||||
*/
|
||||
private _applyObvTightScale(
|
||||
seriesList: ISeriesApi<SeriesType>[],
|
||||
plots: Record<string, PlotData>,
|
||||
seriesMeta?: IndicatorEntry['seriesMeta'],
|
||||
): void {
|
||||
const allPts: number[] = [];
|
||||
for (const key of ['plot0', 'plot1']) {
|
||||
const pts = (plots[key] ?? []).filter(p => p.value != null && isFinite(p.value as number));
|
||||
allPts.push(...pts.slice(-60).map(p => p.value as number));
|
||||
}
|
||||
if (allPts.length === 0) return;
|
||||
|
||||
const minV = Math.min(...allPts);
|
||||
const maxV = Math.max(...allPts);
|
||||
const span = Math.max(1, maxV - minV);
|
||||
const pad = span * 0.12;
|
||||
const pMin = minV - pad;
|
||||
const pMax = maxV + pad;
|
||||
const pSpan = pMax - pMin;
|
||||
|
||||
seriesList.forEach((s, i) => {
|
||||
const plotId = seriesMeta?.[i]?.plotId;
|
||||
/** OBV — RSI/CCI와 동일한 공유 price scale, 이전 autoscale 오버라이드 제거 */
|
||||
private _resetObvSharedScale(entry: IndicatorEntry): void {
|
||||
if (entry.type !== 'OBV') return;
|
||||
for (const s of entry.seriesList) {
|
||||
try {
|
||||
if (plotId === 'plot1') {
|
||||
// 신호선: 독립 scale, 상반부 — priceRange 를 하방으로 pSpan 확장
|
||||
// [pMin - pSpan, pMax] 에서 실제 데이터 [pMin, pMax] 가 상위 50% 차지
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(s as ISeriesApi<any>).applyOptions({
|
||||
autoscaleInfoProvider: () => ({
|
||||
priceRange: { minValue: pMin - pSpan, maxValue: pMax },
|
||||
margins: { above: 0.04, below: 0 },
|
||||
}),
|
||||
});
|
||||
// 신호선 scale axis 숨김
|
||||
try { s.priceScale().applyOptions({ visible: false }); } catch { /* ok */ }
|
||||
} else {
|
||||
// OBV 본선: 하반부 — priceRange 를 상방으로 pSpan 확장
|
||||
// [pMin, pMax + pSpan] 에서 실제 데이터 [pMin, pMax] 가 하위 50% 차지
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(s as ISeriesApi<any>).applyOptions({
|
||||
autoscaleInfoProvider: () => ({
|
||||
priceRange: { minValue: pMin, maxValue: pMax + pSpan },
|
||||
margins: { above: 0, below: 0.04 },
|
||||
}),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(s as ISeriesApi<any>).applyOptions({ autoscaleInfoProvider: undefined });
|
||||
} catch { /* ok */ }
|
||||
});
|
||||
}
|
||||
const anchor = entry.seriesList[0];
|
||||
if (!anchor) return;
|
||||
try {
|
||||
anchor.priceScale().applyOptions({
|
||||
visible: true,
|
||||
scaleMargins: { top: 0.1, bottom: 0.1 },
|
||||
});
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
|
||||
private _appendMissingPlotSeriesToLists(
|
||||
@@ -806,7 +772,7 @@ export class ChartManager {
|
||||
indicatorHidden: boolean,
|
||||
def: ReturnType<typeof getIndicatorDef>,
|
||||
): void {
|
||||
const plotDefs = config.plots ?? def?.plots ?? [];
|
||||
const plotDefs = sortPlotDefsForSeriesAdd(config.type, config.plots ?? def?.plots ?? []);
|
||||
const existingIds = new Set(seriesMeta.map(m => m.plotId));
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
|
||||
@@ -828,9 +794,14 @@ export class ChartManager {
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
|
||||
const fallbackColor = config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
? (regPlot?.color ?? '#2196F3')
|
||||
: (regPlot?.color ?? plotDef.color ?? '#2962FF');
|
||||
const series = this.chart.addSeries(LineSeries, {
|
||||
color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
|
||||
lineWidth: Math.max(1, plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
||||
lineWidth: (config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
? Math.max(2, plotDef.lineWidth ?? regPlot?.lineWidth ?? 2)
|
||||
: Math.max(1, plotDef.lineWidth ?? 1)) as 1 | 2 | 3 | 4,
|
||||
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: showPriceLabel,
|
||||
@@ -871,7 +842,7 @@ export class ChartManager {
|
||||
def,
|
||||
);
|
||||
if (config.type === 'OBV' && !indicatorHidden && entry.seriesList.length > 0) {
|
||||
this._applyObvTightScale(entry.seriesList, result.plots as Record<string, PlotData>, entry.seriesMeta);
|
||||
this._resetObvSharedScale(entry);
|
||||
}
|
||||
entry.config = config;
|
||||
if (entry.seriesList.length > 0) {
|
||||
@@ -928,7 +899,7 @@ export class ChartManager {
|
||||
|
||||
const plotDefsRaw = config.plots ?? def?.plots ?? [];
|
||||
const plotDefs = DUAL_LINE_INDICATOR_TYPES.has(config.type)
|
||||
? sortDualLinePlotDefs(plotDefsRaw)
|
||||
? sortPlotDefsForSeriesAdd(config.type, plotDefsRaw)
|
||||
: plotDefsRaw;
|
||||
|
||||
for (const plotDef of plotDefs) {
|
||||
@@ -958,11 +929,6 @@ export class ChartManager {
|
||||
const fallbackColor = config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
? (regPlot?.color ?? '#2196F3')
|
||||
: (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, {
|
||||
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
||||
lineWidth: (config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
@@ -974,7 +940,6 @@ export class ChartManager {
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
...(seriesPriceScaleId ? { priceScaleId: seriesPriceScaleId } : {}),
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
@@ -995,9 +960,12 @@ export class ChartManager {
|
||||
);
|
||||
}
|
||||
|
||||
// OBV: plot0=하반부, plot1=상반부로 분리 → 언제나 두 선이 시각적으로 구분됨
|
||||
if (config.type === 'OBV' && !indicatorHidden && seriesList.length > 0) {
|
||||
this._applyObvTightScale(seriesList, result.plots as Record<string, PlotData>, seriesMeta);
|
||||
const obvEntry: IndicatorEntry = {
|
||||
id: config.id, type: config.type, seriesList, seriesMeta,
|
||||
paneIndex: pane, alertLines: [], hlineRefs: [], config,
|
||||
};
|
||||
this._resetObvSharedScale(obvEntry);
|
||||
}
|
||||
|
||||
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
|
||||
@@ -1115,12 +1083,8 @@ export class ChartManager {
|
||||
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue;
|
||||
await this._ensureDualLinePlotSeriesForEntry(entry);
|
||||
if (entry.config) this.applyIndicatorStyle(entry.config);
|
||||
// OBV: 분리 스케일 재적용 (스타일 갱신 후 scaleMargins 덮어써질 수 있음)
|
||||
if (entry.type === 'OBV' && entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
||||
try {
|
||||
const result = await calculateIndicator('OBV', this.rawBars.slice(), entry.config?.params ?? {});
|
||||
this._applyObvTightScale(entry.seriesList, result.plots as Record<string, PlotData>, entry.seriesMeta);
|
||||
} catch { /* ok */ }
|
||||
if (entry.type === 'OBV' && entry.seriesList.length > 0) {
|
||||
this._resetObvSharedScale(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3034,14 +2998,8 @@ export class ChartManager {
|
||||
}
|
||||
}
|
||||
|
||||
// OBV: 스타일 갱신 후 autoscaleInfoProvider 기반 상하 분리 재적용
|
||||
if (entry.type === 'OBV' && entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
||||
(async () => {
|
||||
try {
|
||||
const result = await calculateIndicator('OBV', this.rawBars.slice(), entry.config?.params ?? {});
|
||||
this._applyObvTightScale(entry.seriesList, result.plots as Record<string, PlotData>, entry.seriesMeta);
|
||||
} catch { /* ok */ }
|
||||
})();
|
||||
if (entry.type === 'OBV' && entry.seriesList.length > 0) {
|
||||
this._resetObvSharedScale(entry);
|
||||
}
|
||||
|
||||
if (entry.type === 'BollingerBands') {
|
||||
|
||||
Reference in New Issue
Block a user