추가수정
This commit is contained in:
@@ -103,6 +103,26 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
applyNotificationSignalMarkers(mgr, signalMarkerRef.current, false);
|
||||
}, []);
|
||||
|
||||
/** OBV plot0(본선) 누락 시 refreshIndicators 로 강제 재생성 */
|
||||
const ensureObvDualLine = useCallback(async (attempt = 0) => {
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr?.hasMainSeries() || !needsDeepObv) return;
|
||||
const obvCfg = chartIndicators.find(i => i.type === 'OBV');
|
||||
if (!obvCfg) return;
|
||||
|
||||
await mgr.repairDualLineSeries();
|
||||
const plotIds = mgr.getIndicatorSeriesPlotIds(obvCfg.id);
|
||||
const needsBoth = plotIds.length < 2 || !plotIds.includes('plot0');
|
||||
if (needsBoth && attempt < 4) {
|
||||
await mgr.refreshIndicators([obvCfg]);
|
||||
await mgr.repairDualLineSeries();
|
||||
const after = mgr.getIndicatorSeriesPlotIds(obvCfg.id);
|
||||
if ((after.length < 2 || !after.includes('plot0')) && attempt < 3) {
|
||||
setTimeout(() => void ensureObvDualLine(attempt + 1), 500 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
}, [needsDeepObv, chartIndicators]);
|
||||
|
||||
const syncChartLayout = useCallback(() => {
|
||||
const wrap = canvasWrapRef.current;
|
||||
const mgr = managerRef.current;
|
||||
@@ -125,21 +145,23 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
syncChartLayout();
|
||||
applySignalMarkers();
|
||||
if (!mgr) return;
|
||||
void ensureObvDualLine();
|
||||
[0, 100, 500, 1200].forEach(delay => {
|
||||
setTimeout(() => {
|
||||
void managerRef.current?.repairDualLineSeries();
|
||||
void ensureObvDualLine();
|
||||
}, delay);
|
||||
});
|
||||
}, [syncChartLayout, applySignalMarkers]);
|
||||
}, [syncChartLayout, applySignalMarkers, ensureObvDualLine]);
|
||||
|
||||
const handleManagerReady = useCallback((mgr: ChartManager) => {
|
||||
managerRef.current = mgr;
|
||||
syncToManager(mgr);
|
||||
applySignalMarkers();
|
||||
if (needsDeepObv) {
|
||||
void mgr.repairDualLineSeries();
|
||||
void mgr.repairDualLineSeries().then(() => ensureObvDualLine());
|
||||
}
|
||||
}, [syncToManager, applySignalMarkers, needsDeepObv]);
|
||||
}, [syncToManager, applySignalMarkers, needsDeepObv, ensureObvDualLine]);
|
||||
|
||||
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
|
||||
if (barsMarketRef.current !== marketRef.current) return;
|
||||
@@ -199,10 +221,13 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr?.hasMainSeries()) return;
|
||||
const timers = [200, 800, 1600].map(delay =>
|
||||
setTimeout(() => void mgr.repairDualLineSeries(), delay),
|
||||
setTimeout(() => {
|
||||
void mgr.repairDualLineSeries();
|
||||
void ensureObvDualLine();
|
||||
}, delay),
|
||||
);
|
||||
return () => timers.forEach(clearTimeout);
|
||||
}, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision]);
|
||||
}, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision, ensureObvDualLine]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chartLiveReadyRef.current = false;
|
||||
|
||||
@@ -748,18 +748,72 @@ export class ChartManager {
|
||||
return loadGen !== this._dataGeneration;
|
||||
}
|
||||
|
||||
/** 이중 plot 지표에서 누락된 plotId 목록 (series 개수만으로는 plot0 누락 감지 불가) */
|
||||
private _dualLineMissingPlotIds(
|
||||
entry: IndicatorEntry,
|
||||
/** 이중 plot 지표에서 표시해야 할 plotId 목록 */
|
||||
private _expectedDualLinePlotIds(
|
||||
config: IndicatorConfig,
|
||||
def: ReturnType<typeof getIndicatorDef> | undefined,
|
||||
): string[] {
|
||||
const plotDefs = config.plots ?? def?.plots ?? [];
|
||||
const existing = new Set(entry.seriesMeta.map(m => m.plotId));
|
||||
return plotDefs
|
||||
.filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false)
|
||||
.map(p => p.id)
|
||||
.filter(id => !existing.has(id));
|
||||
.map(p => p.id);
|
||||
}
|
||||
|
||||
/** seriesMeta·seriesList 불일치 또는 plot0 누락 */
|
||||
private _dualLineSeriesOutOfSync(
|
||||
entry: IndicatorEntry,
|
||||
config: IndicatorConfig,
|
||||
def: ReturnType<typeof getIndicatorDef> | undefined,
|
||||
): boolean {
|
||||
const expected = this._expectedDualLinePlotIds(config, def);
|
||||
if (expected.length === 0) return false;
|
||||
if (entry.seriesList.length !== entry.seriesMeta.length) return true;
|
||||
if (entry.seriesMeta.length !== expected.length) return true;
|
||||
const existing = new Set(entry.seriesMeta.map(m => m.plotId));
|
||||
return expected.some(id => !existing.has(id));
|
||||
}
|
||||
|
||||
/** OBV·RSI 등 — plot0→plot1 순서로 시리즈를 한 번에 생성 (append 누락·순서 어긋남 방지) */
|
||||
private _addDualLinePlotSeries(
|
||||
seriesList: ISeriesApi<SeriesType>[],
|
||||
seriesMeta: IndicatorEntry['seriesMeta'],
|
||||
config: IndicatorConfig,
|
||||
result: { plots: Record<string, PlotData> },
|
||||
pane: number,
|
||||
indicatorHidden: boolean,
|
||||
def: ReturnType<typeof getIndicatorDef>,
|
||||
): void {
|
||||
const plotDefs = sortDualLinePlotDefs(config.plots ?? def?.plots ?? [])
|
||||
.filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
|
||||
for (const plotDef of plotDefs) {
|
||||
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
||||
if (!plotData?.length) continue;
|
||||
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
const isPlotVisible = !indicatorHidden
|
||||
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
|
||||
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
|
||||
: config.plotVisibility?.[plotDef.id] !== false);
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
|
||||
const series = this.chart.addSeries(LineSeries, {
|
||||
color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
|
||||
lineWidth: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
private _appendMissingPlotSeriesToLists(
|
||||
@@ -831,7 +885,7 @@ export class ChartManager {
|
||||
|
||||
try {
|
||||
const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params);
|
||||
this._appendMissingPlotSeriesToLists(
|
||||
this._addDualLinePlotSeries(
|
||||
entry.seriesList,
|
||||
entry.seriesMeta,
|
||||
config,
|
||||
@@ -861,8 +915,7 @@ export class ChartManager {
|
||||
if (!def) return;
|
||||
if (this.rawBars.length === 0) return;
|
||||
|
||||
const missing = this._dualLineMissingPlotIds(entry, config, def);
|
||||
if (missing.length > 0) {
|
||||
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
|
||||
return;
|
||||
}
|
||||
@@ -921,52 +974,9 @@ export class ChartManager {
|
||||
}
|
||||
|
||||
const plotDefsRaw = config.plots ?? def?.plots ?? [];
|
||||
const plotDefs = DUAL_LINE_INDICATOR_TYPES.has(config.type)
|
||||
? sortPlotDefsForSeriesAdd(config.type, plotDefsRaw)
|
||||
: plotDefsRaw;
|
||||
|
||||
for (const plotDef of plotDefs) {
|
||||
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
||||
if (!plotData || plotData.length === 0) continue;
|
||||
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
let series: ISeriesApi<SeriesType>;
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
if (plotDef.type === 'histogram') {
|
||||
series = this.chart.addSeries(HistogramSeries, {
|
||||
color: plotDef.color,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
series.setData(mapHistogramSeriesData(filtered, plotDef));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
|
||||
} else {
|
||||
const isPlotVisible = !indicatorHidden
|
||||
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
|
||||
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
|
||||
: config.plotVisibility?.[plotDef.id] !== false);
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
|
||||
series = this.chart.addSeries(LineSeries, {
|
||||
color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
|
||||
lineWidth: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
}
|
||||
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
||||
this._appendMissingPlotSeriesToLists(
|
||||
this._addDualLinePlotSeries(
|
||||
seriesList,
|
||||
seriesMeta,
|
||||
config,
|
||||
@@ -975,6 +985,46 @@ export class ChartManager {
|
||||
indicatorHidden,
|
||||
def,
|
||||
);
|
||||
} else {
|
||||
for (const plotDef of plotDefsRaw) {
|
||||
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
||||
if (!plotData || plotData.length === 0) continue;
|
||||
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
let series: ISeriesApi<SeriesType>;
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
if (plotDef.type === 'histogram') {
|
||||
series = this.chart.addSeries(HistogramSeries, {
|
||||
color: plotDef.color,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
series.setData(mapHistogramSeriesData(filtered, plotDef));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
|
||||
} else {
|
||||
const isPlotVisible = !indicatorHidden
|
||||
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
|
||||
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
|
||||
: config.plotVisibility?.[plotDef.id] !== false);
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
|
||||
series = this.chart.addSeries(LineSeries, {
|
||||
color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
|
||||
lineWidth: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
}
|
||||
|
||||
seriesList.push(series);
|
||||
}
|
||||
}
|
||||
|
||||
if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) {
|
||||
@@ -1105,8 +1155,7 @@ export class ChartManager {
|
||||
const def = getIndicatorDef(entry.type);
|
||||
if (!def) continue;
|
||||
|
||||
const missing = this._dualLineMissingPlotIds(entry, config, def);
|
||||
if (missing.length > 0) {
|
||||
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
|
||||
} else if (entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
||||
try {
|
||||
@@ -1923,7 +1972,7 @@ export class ChartManager {
|
||||
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
|
||||
const config = enrichIndicatorConfig(entry.config);
|
||||
const def = getIndicatorDef(entry.type);
|
||||
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) {
|
||||
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||
await this._ensureDualLinePlotSeriesForEntry(entry);
|
||||
}
|
||||
}
|
||||
@@ -2155,7 +2204,7 @@ export class ChartManager {
|
||||
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
|
||||
const config = enrichIndicatorConfig(entry.config);
|
||||
const def = getIndicatorDef(entry.type);
|
||||
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) {
|
||||
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||
await this._ensureDualLinePlotSeriesForEntry(entry);
|
||||
}
|
||||
}
|
||||
@@ -3857,6 +3906,11 @@ export class ChartManager {
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** 지표에 붙은 plotId 목록 (OBV plot0/plot1 누락 진단용) */
|
||||
getIndicatorSeriesPlotIds(id: string): string[] {
|
||||
return this.indicators.get(id)?.seriesMeta.map(m => m.plotId) ?? [];
|
||||
}
|
||||
|
||||
/** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */
|
||||
getRawBarsLength(): number {
|
||||
return this.rawBars.length;
|
||||
|
||||
Reference in New Issue
Block a user