obv 그래프 수정

This commit is contained in:
Macbook
2026-06-09 02:57:58 +09:00
parent 7c1cc2ee4b
commit 133c20cc25
3 changed files with 70 additions and 16 deletions
@@ -21,6 +21,7 @@ import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings'; import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
import { useSessionCandleOverlayControls } from '../../hooks/useSessionCandleOverlayControls'; import { useSessionCandleOverlayControls } from '../../hooks/useSessionCandleOverlayControls';
import { ensurePaperChartOverlays } from '../../utils/strategyToChartIndicators'; import { ensurePaperChartOverlays } from '../../utils/strategyToChartIndicators';
import { enrichIndicatorConfig } from '../../utils/indicatorRegistry';
import { import {
applyNotificationSignalMarkers, applyNotificationSignalMarkers,
type TradeSignalChartMarker, type TradeSignalChartMarker,
@@ -68,7 +69,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
signalMarkerRef.current = signalMarker; signalMarkerRef.current = signalMarker;
const baseIndicators = useMemo( const baseIndicators = useMemo(
() => ensurePaperChartOverlays(indicators, getParams, getVisualConfig), () => ensurePaperChartOverlays(indicators, getParams, getVisualConfig).map(enrichIndicatorConfig),
[indicators, getParams, getVisualConfig], [indicators, getParams, getVisualConfig],
); );
@@ -117,6 +118,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
} }
syncChartLayout(); syncChartLayout();
applySignalMarkers(); applySignalMarkers();
void mgr?.repairDualLineSeries();
}, [syncChartLayout, applySignalMarkers]); }, [syncChartLayout, applySignalMarkers]);
const handleManagerReady = useCallback((mgr: ChartManager) => { const handleManagerReady = useCallback((mgr: ChartManager) => {
@@ -162,7 +164,8 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const { isLoadingMore } = useHistoryLoader({ const { isLoadingMore } = useHistoryLoader({
symbol: market, symbol: market,
timeframe, timeframe,
isUpbit: useUpbit && enabled, /** OBV — 백엔드 history volume=0 봉 prepend 시 본선 왜곡 방지 */
isUpbit: useUpbit && enabled && !needsDeepObv,
managerRef, managerRef,
}); });
+51 -10
View File
@@ -217,6 +217,19 @@ function detachBbBandFill(entry: IndicatorEntry): void {
/** OBV·CCI·RSI·TRIX — 본선+신호선 이중 plot (누락 시리즈 보정 대상) */ /** OBV·CCI·RSI·TRIX — 본선+신호선 이중 plot (누락 시리즈 보정 대상) */
const DUAL_LINE_INDICATOR_TYPES = new Set(['OBV', 'CCI', 'RSI', 'TRIX']); const DUAL_LINE_INDICATOR_TYPES = new Set(['OBV', 'CCI', 'RSI', 'TRIX']);
const DUAL_LINE_PLOT_ORDER = ['plot0', 'plot1', 'plot2', 'plot3'];
function sortDualLinePlotDefs(plotDefs: PlotDef[]): PlotDef[] {
return [...plotDefs].sort(
(a, b) => DUAL_LINE_PLOT_ORDER.indexOf(a.id) - DUAL_LINE_PLOT_ORDER.indexOf(b.id),
);
}
/** OBV 본선(plot0) — DB plotVisibility·투명색으로 숨김 방지 */
function isDualLinePlotForcedVisible(type: string, plotId: string): boolean {
return type === 'OBV' && plotId === 'plot0';
}
export class ChartManager { export class ChartManager {
private chart: IChartApi; private chart: IChartApi;
private container: HTMLElement; private container: HTMLElement;
@@ -741,7 +754,7 @@ export class ChartManager {
for (const plotDef of plotDefs) { for (const plotDef of plotDefs) {
if (existingIds.has(plotDef.id)) continue; if (existingIds.has(plotDef.id)) continue;
if (config.plotVisibility?.[plotDef.id] === false) continue; if (config.plotVisibility?.[plotDef.id] === false && !isDualLinePlotForcedVisible(config.type, plotDef.id)) continue;
if (plotDef.type === 'histogram') continue; if (plotDef.type === 'histogram') continue;
const plotData = result.plots[plotDef.id] as PlotData | undefined; const plotData = result.plots[plotDef.id] as PlotData | undefined;
@@ -750,9 +763,10 @@ export class ChartManager {
if (filtered.length === 0) continue; if (filtered.length === 0) continue;
const isPlotVisible = !indicatorHidden const isPlotVisible = !indicatorHidden
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud') && (isDualLinePlotForcedVisible(config.type, plotDef.id)
|| (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id) ? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
: config.plotVisibility?.[plotDef.id] !== false); : config.plotVisibility?.[plotDef.id] !== false));
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);
@@ -851,7 +865,10 @@ export class ChartManager {
return; return;
} }
const plotDefs = config.plots ?? def?.plots ?? []; const plotDefsRaw = config.plots ?? def?.plots ?? [];
const plotDefs = DUAL_LINE_INDICATOR_TYPES.has(config.type)
? sortDualLinePlotDefs(plotDefsRaw)
: plotDefsRaw;
for (const plotDef of plotDefs) { for (const plotDef of plotDefs) {
const plotData = result.plots[plotDef.id] as PlotData | undefined; const plotData = result.plots[plotDef.id] as PlotData | undefined;
@@ -870,15 +887,21 @@ export class ChartManager {
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color }); seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
} else { } else {
const isPlotVisible = !indicatorHidden const isPlotVisible = !indicatorHidden
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud') && (isDualLinePlotForcedVisible(config.type, plotDef.id)
|| (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id) ? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
: config.plotVisibility?.[plotDef.id] !== false); : config.plotVisibility?.[plotDef.id] !== false));
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');
series = this.chart.addSeries(LineSeries, { 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,
@@ -956,6 +979,9 @@ export class ChartManager {
return; return;
} }
this.indicators.set(config.id, entry); this.indicators.set(config.id, entry);
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
await this._ensureDualLinePlotSeriesForEntry(entry);
}
this.applyIndicatorStyle(config); this.applyIndicatorStyle(config);
if (config.type === 'BollingerBands' && !indicatorHidden) { if (config.type === 'BollingerBands' && !indicatorHidden) {
attachBbBandFill(entry, config, this._shouldShowBbBandFillForConfig(config)); attachBbBandFill(entry, config, this._shouldShowBbBandFillForConfig(config));
@@ -1011,6 +1037,15 @@ export class ChartManager {
this._notifyPaneLayout(); this._notifyPaneLayout();
} }
/** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 (미니 차트·스타일 갱신 후) */
async repairDualLineSeries(): Promise<void> {
for (const entry of this.indicators.values()) {
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue;
await this._ensureDualLinePlotSeriesForEntry(entry);
if (entry.config) this.applyIndicatorStyle(entry.config);
}
}
/** 여러 지표 추가 후 pane 레이아웃을 한 번만 갱신 */ /** 여러 지표 추가 후 pane 레이아웃을 한 번만 갱신 */
async addIndicatorsBatch(configs: IndicatorConfig[]): Promise<void> { async addIndicatorsBatch(configs: IndicatorConfig[]): Promise<void> {
for (const config of configs) { for (const config of configs) {
@@ -2617,6 +2652,7 @@ export class ChartManager {
/** custom 미적용 시 지표 설정 plotVisibility, 적용 시 custom 선택만 반영 */ /** custom 미적용 시 지표 설정 plotVisibility, 적용 시 custom 선택만 반영 */
private _resolveSeriesVisible(config: IndicatorConfig, plotId: string | undefined): boolean { private _resolveSeriesVisible(config: IndicatorConfig, plotId: string | undefined): boolean {
if (!plotId || !this._isIndicatorEffectivelyVisible(config)) return false; if (!plotId || !this._isIndicatorEffectivelyVisible(config)) return false;
if (isDualLinePlotForcedVisible(config.type, plotId)) return true;
if (!this._isCustomOverlayApplied()) { if (!this._isCustomOverlayApplied()) {
return config.plotVisibility?.[plotId] !== false; return config.plotVisibility?.[plotId] !== false;
} }
@@ -2847,8 +2883,13 @@ export class ChartManager {
} }
if (!entry.seriesMeta[i]?.isHistogram) { if (!entry.seriesMeta[i]?.isHistogram) {
const regPlot = def?.plots?.find(p => p.id === pid); const regPlot = def?.plots?.find(p => p.id === pid);
opts['color'] = resolvePlotLineColor(plot.color, regPlot?.color ?? plot.color ?? '#aaa'); const fallbackColor = entry.type === 'OBV' && pid === 'plot0'
opts['lineWidth'] = Math.max(1, plot.lineWidth ?? regPlot?.lineWidth ?? 1); ? (regPlot?.color ?? '#2196F3')
: (regPlot?.color ?? plot.color ?? '#aaa');
opts['color'] = resolvePlotLineColor(plot.color, fallbackColor);
opts['lineWidth'] = entry.type === 'OBV' && pid === 'plot0'
? Math.max(2, plot.lineWidth ?? regPlot?.lineWidth ?? 2)
: Math.max(1, plot.lineWidth ?? regPlot?.lineWidth ?? 1);
opts['lineStyle'] = plotSeriesLineStyle(plot.lineStyle); opts['lineStyle'] = plotSeriesLineStyle(plot.lineStyle);
let plotAllows = true; let plotAllows = true;
if (entry.type === 'IchimokuCloud') { if (entry.type === 'IchimokuCloud') {
+12 -2
View File
@@ -474,12 +474,22 @@ function normalizeSignalLineIndicatorPlots(
const reg = registryPlots.find(r => r.id === p.id); const reg = registryPlots.find(r => r.id === p.id);
if (!reg) return p; if (!reg) return p;
const normalized = normalizeDualLinePlotDef(p, reg); const normalized = normalizeDualLinePlotDef(p, reg);
// OBV 본선 — DB 저장색(투명·신호선과 동일) 무시, registry 기본색 유지 // OBV 본선 — DB 저장색(투명·신호선과 동일) 무시, registry 기본색·굵기 유지
if (p.id === 'plot0') { if (p.id === 'plot0') {
return { ...normalized, color: reg.color }; return {
...normalized,
type: 'line' as const,
color: reg.color,
lineWidth: Math.max(2, normalized.lineWidth ?? reg.lineWidth ?? 2),
};
} }
return normalized; return normalized;
}); });
// plot0(본선) → plot1(신호선) 순서 보장
nextPlots.sort((a, b) => {
const order = ['plot0', 'plot1'];
return order.indexOf(a.id) - order.indexOf(b.id);
});
return { return {
plots: nextPlots, plots: nextPlots,
plotVisibility: { ...plotVisibility, plot0: true, plot1: true }, plotVisibility: { ...plotVisibility, plot0: true, plot1: true },