최종이길
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·투명색으로 숨김 방지 */
|
/** OBV 본선(plot0) — DB plotVisibility·투명색으로 숨김 방지 */
|
||||||
function isDualLinePlotForcedVisible(type: string, plotId: string): boolean {
|
function isDualLinePlotForcedVisible(type: string, plotId: string): boolean {
|
||||||
return type === 'OBV' && plotId === 'plot0';
|
return type === 'OBV' && plotId === 'plot0';
|
||||||
@@ -738,63 +744,23 @@ export class ChartManager {
|
|||||||
return loadGen !== this._dataGeneration;
|
return loadGen !== this._dataGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 이중 plot 지표 — addIndicator 루프에서 누락된 plot 시리즈 추가 */
|
/** OBV — RSI/CCI와 동일한 공유 price scale, 이전 autoscale 오버라이드 제거 */
|
||||||
/**
|
private _resetObvSharedScale(entry: IndicatorEntry): void {
|
||||||
* OBV pane: plot0(본선)=하반부, plot1(신호선)=상반부로 분리.
|
if (entry.type !== 'OBV') return;
|
||||||
* 각 series의 autoscaleInfoProvider에서 priceRange를 오프셋 처리하여
|
for (const s of entry.seriesList) {
|
||||||
* 독립 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;
|
|
||||||
try {
|
try {
|
||||||
if (plotId === 'plot1') {
|
|
||||||
// 신호선: 독립 scale, 상반부 — priceRange 를 하방으로 pSpan 확장
|
|
||||||
// [pMin - pSpan, pMax] 에서 실제 데이터 [pMin, pMax] 가 상위 50% 차지
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(s as ISeriesApi<any>).applyOptions({
|
(s as ISeriesApi<any>).applyOptions({ autoscaleInfoProvider: undefined });
|
||||||
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 },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch { /* ok */ }
|
} 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(
|
private _appendMissingPlotSeriesToLists(
|
||||||
@@ -806,7 +772,7 @@ export class ChartManager {
|
|||||||
indicatorHidden: boolean,
|
indicatorHidden: boolean,
|
||||||
def: ReturnType<typeof getIndicatorDef>,
|
def: ReturnType<typeof getIndicatorDef>,
|
||||||
): void {
|
): 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 existingIds = new Set(seriesMeta.map(m => m.plotId));
|
||||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||||
|
|
||||||
@@ -828,9 +794,14 @@ export class ChartManager {
|
|||||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
||||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||||
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
|
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, {
|
const series = this.chart.addSeries(LineSeries, {
|
||||||
color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
|
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
||||||
lineWidth: Math.max(1, plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
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),
|
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
lastValueVisible: showPriceLabel,
|
lastValueVisible: showPriceLabel,
|
||||||
@@ -871,7 +842,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>, entry.seriesMeta);
|
this._resetObvSharedScale(entry);
|
||||||
}
|
}
|
||||||
entry.config = config;
|
entry.config = config;
|
||||||
if (entry.seriesList.length > 0) {
|
if (entry.seriesList.length > 0) {
|
||||||
@@ -928,7 +899,7 @@ export class ChartManager {
|
|||||||
|
|
||||||
const plotDefsRaw = config.plots ?? def?.plots ?? [];
|
const plotDefsRaw = config.plots ?? def?.plots ?? [];
|
||||||
const plotDefs = DUAL_LINE_INDICATOR_TYPES.has(config.type)
|
const plotDefs = DUAL_LINE_INDICATOR_TYPES.has(config.type)
|
||||||
? sortDualLinePlotDefs(plotDefsRaw)
|
? sortPlotDefsForSeriesAdd(config.type, plotDefsRaw)
|
||||||
: plotDefsRaw;
|
: plotDefsRaw;
|
||||||
|
|
||||||
for (const plotDef of plotDefs) {
|
for (const plotDef of plotDefs) {
|
||||||
@@ -958,11 +929,6 @@ 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'
|
||||||
@@ -974,7 +940,6 @@ 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 });
|
||||||
@@ -995,9 +960,12 @@ export class ChartManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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>, seriesMeta);
|
const obvEntry: IndicatorEntry = {
|
||||||
|
id: config.id, type: config.type, seriesList, seriesMeta,
|
||||||
|
paneIndex: pane, alertLines: [], hlineRefs: [], config,
|
||||||
|
};
|
||||||
|
this._resetObvSharedScale(obvEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
|
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
|
||||||
@@ -1115,12 +1083,8 @@ 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: 분리 스케일 재적용 (스타일 갱신 후 scaleMargins 덮어써질 수 있음)
|
if (entry.type === 'OBV' && entry.seriesList.length > 0) {
|
||||||
if (entry.type === 'OBV' && entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
this._resetObvSharedScale(entry);
|
||||||
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 */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3034,14 +2998,8 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OBV: 스타일 갱신 후 autoscaleInfoProvider 기반 상하 분리 재적용
|
if (entry.type === 'OBV' && entry.seriesList.length > 0) {
|
||||||
if (entry.type === 'OBV' && entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
this._resetObvSharedScale(entry);
|
||||||
(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 === 'BollingerBands') {
|
if (entry.type === 'BollingerBands') {
|
||||||
|
|||||||
Reference in New Issue
Block a user