추가수정
This commit is contained in:
@@ -103,6 +103,26 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
|||||||
applyNotificationSignalMarkers(mgr, signalMarkerRef.current, false);
|
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 syncChartLayout = useCallback(() => {
|
||||||
const wrap = canvasWrapRef.current;
|
const wrap = canvasWrapRef.current;
|
||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
@@ -125,21 +145,23 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
|||||||
syncChartLayout();
|
syncChartLayout();
|
||||||
applySignalMarkers();
|
applySignalMarkers();
|
||||||
if (!mgr) return;
|
if (!mgr) return;
|
||||||
|
void ensureObvDualLine();
|
||||||
[0, 100, 500, 1200].forEach(delay => {
|
[0, 100, 500, 1200].forEach(delay => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
void managerRef.current?.repairDualLineSeries();
|
void managerRef.current?.repairDualLineSeries();
|
||||||
|
void ensureObvDualLine();
|
||||||
}, delay);
|
}, delay);
|
||||||
});
|
});
|
||||||
}, [syncChartLayout, applySignalMarkers]);
|
}, [syncChartLayout, applySignalMarkers, ensureObvDualLine]);
|
||||||
|
|
||||||
const handleManagerReady = useCallback((mgr: ChartManager) => {
|
const handleManagerReady = useCallback((mgr: ChartManager) => {
|
||||||
managerRef.current = mgr;
|
managerRef.current = mgr;
|
||||||
syncToManager(mgr);
|
syncToManager(mgr);
|
||||||
applySignalMarkers();
|
applySignalMarkers();
|
||||||
if (needsDeepObv) {
|
if (needsDeepObv) {
|
||||||
void mgr.repairDualLineSeries();
|
void mgr.repairDualLineSeries().then(() => ensureObvDualLine());
|
||||||
}
|
}
|
||||||
}, [syncToManager, applySignalMarkers, needsDeepObv]);
|
}, [syncToManager, applySignalMarkers, needsDeepObv, ensureObvDualLine]);
|
||||||
|
|
||||||
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
|
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
|
||||||
if (barsMarketRef.current !== marketRef.current) return;
|
if (barsMarketRef.current !== marketRef.current) return;
|
||||||
@@ -199,10 +221,13 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
|||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
if (!mgr?.hasMainSeries()) return;
|
if (!mgr?.hasMainSeries()) return;
|
||||||
const timers = [200, 800, 1600].map(delay =>
|
const timers = [200, 800, 1600].map(delay =>
|
||||||
setTimeout(() => void mgr.repairDualLineSeries(), delay),
|
setTimeout(() => {
|
||||||
|
void mgr.repairDualLineSeries();
|
||||||
|
void ensureObvDualLine();
|
||||||
|
}, delay),
|
||||||
);
|
);
|
||||||
return () => timers.forEach(clearTimeout);
|
return () => timers.forEach(clearTimeout);
|
||||||
}, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision]);
|
}, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision, ensureObvDualLine]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
chartLiveReadyRef.current = false;
|
chartLiveReadyRef.current = false;
|
||||||
|
|||||||
@@ -748,18 +748,72 @@ export class ChartManager {
|
|||||||
return loadGen !== this._dataGeneration;
|
return loadGen !== this._dataGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 이중 plot 지표에서 누락된 plotId 목록 (series 개수만으로는 plot0 누락 감지 불가) */
|
/** 이중 plot 지표에서 표시해야 할 plotId 목록 */
|
||||||
private _dualLineMissingPlotIds(
|
private _expectedDualLinePlotIds(
|
||||||
entry: IndicatorEntry,
|
|
||||||
config: IndicatorConfig,
|
config: IndicatorConfig,
|
||||||
def: ReturnType<typeof getIndicatorDef> | undefined,
|
def: ReturnType<typeof getIndicatorDef> | undefined,
|
||||||
): string[] {
|
): string[] {
|
||||||
const plotDefs = config.plots ?? def?.plots ?? [];
|
const plotDefs = config.plots ?? def?.plots ?? [];
|
||||||
const existing = new Set(entry.seriesMeta.map(m => m.plotId));
|
|
||||||
return plotDefs
|
return plotDefs
|
||||||
.filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false)
|
.filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false)
|
||||||
.map(p => p.id)
|
.map(p => p.id);
|
||||||
.filter(id => !existing.has(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(
|
private _appendMissingPlotSeriesToLists(
|
||||||
@@ -831,7 +885,7 @@ export class ChartManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params);
|
const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params);
|
||||||
this._appendMissingPlotSeriesToLists(
|
this._addDualLinePlotSeries(
|
||||||
entry.seriesList,
|
entry.seriesList,
|
||||||
entry.seriesMeta,
|
entry.seriesMeta,
|
||||||
config,
|
config,
|
||||||
@@ -861,8 +915,7 @@ export class ChartManager {
|
|||||||
if (!def) return;
|
if (!def) return;
|
||||||
if (this.rawBars.length === 0) return;
|
if (this.rawBars.length === 0) return;
|
||||||
|
|
||||||
const missing = this._dualLineMissingPlotIds(entry, config, def);
|
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||||
if (missing.length > 0) {
|
|
||||||
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
|
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -921,11 +974,19 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const plotDefsRaw = config.plots ?? def?.plots ?? [];
|
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) {
|
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
||||||
|
this._addDualLinePlotSeries(
|
||||||
|
seriesList,
|
||||||
|
seriesMeta,
|
||||||
|
config,
|
||||||
|
result,
|
||||||
|
pane,
|
||||||
|
indicatorHidden,
|
||||||
|
def,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for (const plotDef of plotDefsRaw) {
|
||||||
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
||||||
if (!plotData || plotData.length === 0) continue;
|
if (!plotData || plotData.length === 0) continue;
|
||||||
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
|
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
|
||||||
@@ -964,17 +1025,6 @@ export class ChartManager {
|
|||||||
|
|
||||||
seriesList.push(series);
|
seriesList.push(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
|
||||||
this._appendMissingPlotSeriesToLists(
|
|
||||||
seriesList,
|
|
||||||
seriesMeta,
|
|
||||||
config,
|
|
||||||
result,
|
|
||||||
pane,
|
|
||||||
indicatorHidden,
|
|
||||||
def,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) {
|
if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) {
|
||||||
@@ -1105,8 +1155,7 @@ export class ChartManager {
|
|||||||
const def = getIndicatorDef(entry.type);
|
const def = getIndicatorDef(entry.type);
|
||||||
if (!def) continue;
|
if (!def) continue;
|
||||||
|
|
||||||
const missing = this._dualLineMissingPlotIds(entry, config, def);
|
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||||
if (missing.length > 0) {
|
|
||||||
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
|
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
|
||||||
} else if (entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
} else if (entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -1923,7 +1972,7 @@ export class ChartManager {
|
|||||||
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
|
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
|
||||||
const config = enrichIndicatorConfig(entry.config);
|
const config = enrichIndicatorConfig(entry.config);
|
||||||
const def = getIndicatorDef(entry.type);
|
const def = getIndicatorDef(entry.type);
|
||||||
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) {
|
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||||
await this._ensureDualLinePlotSeriesForEntry(entry);
|
await this._ensureDualLinePlotSeriesForEntry(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2155,7 +2204,7 @@ export class ChartManager {
|
|||||||
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
|
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
|
||||||
const config = enrichIndicatorConfig(entry.config);
|
const config = enrichIndicatorConfig(entry.config);
|
||||||
const def = getIndicatorDef(entry.type);
|
const def = getIndicatorDef(entry.type);
|
||||||
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) {
|
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
|
||||||
await this._ensureDualLinePlotSeriesForEntry(entry);
|
await this._ensureDualLinePlotSeriesForEntry(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3857,6 +3906,11 @@ export class ChartManager {
|
|||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 지표에 붙은 plotId 목록 (OBV plot0/plot1 누락 진단용) */
|
||||||
|
getIndicatorSeriesPlotIds(id: string): string[] {
|
||||||
|
return this.indicators.get(id)?.seriesMeta.map(m => m.plotId) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
/** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */
|
/** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */
|
||||||
getRawBarsLength(): number {
|
getRawBarsLength(): number {
|
||||||
return this.rawBars.length;
|
return this.rawBars.length;
|
||||||
|
|||||||
Reference in New Issue
Block a user