obv 선 표시

This commit is contained in:
Macbook
2026-06-09 01:13:54 +09:00
parent 1f0671aa7a
commit 969b2fa6ad
7 changed files with 68 additions and 21 deletions
@@ -68,6 +68,11 @@ const TradeSignalMiniChart: React.FC<Props> = ({
[indicators, getParams, getVisualConfig], [indicators, getParams, getVisualConfig],
); );
const needsDeepObv = useMemo(
() => baseIndicators.some(i => i.type === 'OBV'),
[baseIndicators],
);
const { const {
indicators: chartIndicators, indicators: chartIndicators,
candleOverlayToggles, candleOverlayToggles,
@@ -143,7 +148,8 @@ const TradeSignalMiniChart: React.FC<Props> = ({
onNewCandle: handleNewCandle, onNewCandle: handleNewCandle,
enabled: enabled && useUpbit, enabled: enabled && useUpbit,
source: 'BACKEND_STOMP' as const, source: 'BACKEND_STOMP' as const,
}), [handleTickUpdate, handleNewCandle, enabled, useUpbit]), deepObvHistory: needsDeepObv,
}), [handleTickUpdate, handleNewCandle, enabled, useUpbit, needsDeepObv]),
); );
const { isLoadingMore } = useHistoryLoader({ const { isLoadingMore } = useHistoryLoader({
@@ -22,6 +22,7 @@ export function useChartRealtimeData(
onTickUpdate: opts.onTickUpdate, onTickUpdate: opts.onTickUpdate,
onNewCandle: opts.onNewCandle, onNewCandle: opts.onNewCandle,
enabled: opts.enabled !== false && useStomp, enabled: opts.enabled !== false && useStomp,
deepObvHistory: opts.deepObvHistory,
}); });
const upbit = useUpbitData(market, timeframe, { const upbit = useUpbitData(market, timeframe, {
onTickUpdate: opts.onTickUpdate, onTickUpdate: opts.onTickUpdate,
+10 -1
View File
@@ -22,7 +22,7 @@
*/ */
import { useEffect, useRef, useCallback, useState } from 'react'; import { useEffect, useRef, useCallback, useState } from 'react';
import { getIndicatorDef, mergePlotDefs, normalizeHLines, normalizeObvParams, PlotDef, HLineDef } from '../utils/indicatorRegistry'; import { getIndicatorDef, mergePlotDefs, normalizeHLines, normalizeObvParams, enrichIndicatorConfig, PlotDef, HLineDef } from '../utils/indicatorRegistry';
import { createDefaultSmaParams, createDefaultSmaPlots, normalizeSmaConfig } from '../utils/smaConfig'; import { createDefaultSmaParams, createDefaultSmaPlots, normalizeSmaConfig } from '../utils/smaConfig';
import { normalizeIchimokuConfig, mergeIchimokuPlots } from '../utils/ichimokuConfig'; import { normalizeIchimokuConfig, mergeIchimokuPlots } from '../utils/ichimokuConfig';
import { import {
@@ -296,6 +296,15 @@ export function useIndicatorSettings(sessionKey = 0) {
plots = mergeIchimokuPlots(plots, registryPlots); plots = mergeIchimokuPlots(plots, registryPlots);
} }
if (type === 'OBV') {
plots = enrichIndicatorConfig({
id: '',
type: 'OBV',
params: (def?.defaultParams ?? {}) as Record<string, number | string | boolean>,
plots,
}).plots ?? plots;
}
const cloudColors = type === 'IchimokuCloud' const cloudColors = type === 'IchimokuCloud'
? resolveIchimokuCloudColors(saved?.cloudColors ?? DEFAULT_ICHIMOKU_CLOUD_COLORS) ? resolveIchimokuCloudColors(saved?.cloudColors ?? DEFAULT_ICHIMOKU_CLOUD_COLORS)
: undefined; : undefined;
+7 -3
View File
@@ -7,7 +7,7 @@ import { parseStompJson } from '../utils/stompMessage';
import { subscribeStompTopic } from '../utils/stompChartBroker'; import { subscribeStompTopic } from '../utils/stompChartBroker';
import { timeframeToCandleType } from '../utils/chartCandleType'; import { timeframeToCandleType } from '../utils/chartCandleType';
import { getCandleStart, TF_SECONDS } from '../utils/upbitApi'; import { getCandleStart, TF_SECONDS } from '../utils/upbitApi';
import { DISPLAY_COUNT, WARMUP_COUNT } from './useUpbitData'; import { DISPLAY_COUNT, WARMUP_COUNT, OBV_DEEP_HISTORY } from './useUpbitData';
import type { WsStatus } from './useUpbitData'; import type { WsStatus } from './useUpbitData';
import { API_BASE } from '../utils/backendApi'; import { API_BASE } from '../utils/backendApi';
import { fetchUpbitCandlesCached } from '../utils/requestCache'; import { fetchUpbitCandlesCached } from '../utils/requestCache';
@@ -53,6 +53,8 @@ interface Options {
onTickUpdate: (bar: OHLCVBar) => void; onTickUpdate: (bar: OHLCVBar) => void;
onNewCandle: (bar: OHLCVBar) => void; onNewCandle: (bar: OHLCVBar) => void;
enabled?: boolean; enabled?: boolean;
/** OBV 누적 정확도 — 더 많은 초기 봉 로드 */
deepObvHistory?: boolean;
} }
interface Return { interface Return {
@@ -177,7 +179,9 @@ export function useStompChartData(
const marketChanged = prevMarketRef.current !== '' && prevMarketRef.current !== market; const marketChanged = prevMarketRef.current !== '' && prevMarketRef.current !== market;
prevMarketRef.current = market; prevMarketRef.current = market;
const count = DISPLAY_COUNT + WARMUP_COUNT; const count = opts.deepObvHistory
? OBV_DEEP_HISTORY
: DISPLAY_COUNT + WARMUP_COUNT;
loadInitialBars(market, timeframe, count, marketChanged) loadInitialBars(market, timeframe, count, marketChanged)
.then(newBars => { .then(newBars => {
if (cancelled) return; if (cancelled) return;
@@ -202,7 +206,7 @@ export function useStompChartData(
setIsLoading(false); setIsLoading(false);
}); });
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [market, candleType, timeframe, opts.enabled]); }, [market, candleType, timeframe, opts.enabled, opts.deepObvHistory]);
const syncBarsToReactIfNeeded = useCallback((marketId: string) => { const syncBarsToReactIfNeeded = useCallback((marketId: string) => {
if (!bootstrapFromStompRef.current) return; if (!bootstrapFromStompRef.current) return;
+6 -4
View File
@@ -76,7 +76,7 @@ import {
isCustomPlotSelected, isCustomPlotSelected,
} from './chartCustomOverlay'; } from './chartCustomOverlay';
import type { PlotDef, HLineStyle } from './indicatorRegistry'; import type { PlotDef, HLineStyle } from './indicatorRegistry';
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils'; import { mapHistogramSeriesData, histogramBarColor, resolvePlotLineColor } from './plotColorUtils';
import { import {
applyPaneSeparatorToChartOptions, applyPaneSeparatorToChartOptions,
cloneChartPaneSeparatorOptions, cloneChartPaneSeparatorOptions,
@@ -789,8 +789,9 @@ export class ChartManager {
: 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);
series = this.chart.addSeries(LineSeries, { series = this.chart.addSeries(LineSeries, {
color: plotDef.color, color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
lineWidth: Math.max(1, plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4, lineWidth: Math.max(1, plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4,
lineStyle: plotSeriesLineStyle(plotDef.lineStyle), lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
priceLineVisible: false, priceLineVisible: false,
@@ -2747,8 +2748,9 @@ export class ChartManager {
opts['priceFormat'] = indicatorPriceFormat; opts['priceFormat'] = indicatorPriceFormat;
} }
if (!entry.seriesMeta[i]?.isHistogram) { if (!entry.seriesMeta[i]?.isHistogram) {
opts['color'] = plot.color ?? '#aaa'; const regPlot = def?.plots?.find(p => p.id === pid);
if (plot.lineWidth != null) opts['lineWidth'] = plot.lineWidth; opts['color'] = resolvePlotLineColor(plot.color, regPlot?.color ?? plot.color ?? '#aaa');
opts['lineWidth'] = 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') {
+28 -12
View File
@@ -11,6 +11,7 @@ import {
mergeIchimokuPlots, mergeIchimokuPlots,
resolveIchimokuDisplacements, resolveIchimokuDisplacements,
} from './ichimokuConfig'; } from './ichimokuConfig';
import { resolvePlotLineColor } from './plotColorUtils';
import { import {
normalizePsychologicalParams, normalizePsychologicalParams,
resolvePsychologicalIndicatorType, resolvePsychologicalIndicatorType,
@@ -441,9 +442,21 @@ export function mergePlotDefs(saved: PlotDef[] | undefined, defaults: PlotDef[])
}); });
} }
/** OBV·CCI·RSI·TRIX — registry 신호선(plot1) 병합·표시 보장 (DB에 plot0만 저장된 경우 대비) */ /** OBV·CCI·RSI·TRIX — 이중 plot 병합·표시 보장 */
const SIGNAL_LINE_INDICATOR_TYPES = new Set(['OBV', 'CCI', 'RSI', 'TRIX']); const SIGNAL_LINE_INDICATOR_TYPES = new Set(['OBV', 'CCI', 'RSI', 'TRIX']);
function normalizeDualLinePlotDef(saved: PlotDef, reg: PlotDef): PlotDef {
return {
...reg,
...saved,
id: reg.id,
title: reg.title,
type: 'line',
lineWidth: Math.max(1, saved.lineWidth ?? reg.lineWidth ?? 1),
color: resolvePlotLineColor(saved.color, reg.color),
};
}
function normalizeSignalLineIndicatorPlots( function normalizeSignalLineIndicatorPlots(
type: string, type: string,
plots: PlotDef[], plots: PlotDef[],
@@ -455,24 +468,27 @@ function normalizeSignalLineIndicatorPlots(
return { plots, plotVisibility }; return { plots, plotVisibility };
} }
const merged = mergePlotDefs(plots, registryPlots); const merged = mergePlotDefs(plots, registryPlots);
if (type === 'OBV') {
const nextPlots = merged.map(p => {
const reg = registryPlots.find(r => r.id === p.id);
return reg ? normalizeDualLinePlotDef(p, reg) : p;
});
return {
plots: nextPlots,
plotVisibility: { ...plotVisibility, plot0: true, plot1: true },
};
}
const regSignal = registryPlots.find(p => p.id === 'plot1'); const regSignal = registryPlots.find(p => p.id === 'plot1');
const nextPlots = regSignal const nextPlots = regSignal
? merged.map(p => { ? merged.map(p => {
if (p.id !== 'plot1') return p; if (p.id !== 'plot1') return p;
return { return normalizeDualLinePlotDef(p, regSignal);
...regSignal,
...p,
id: 'plot1',
title: regSignal.title,
type: 'line' as const,
lineWidth: Math.max(1, p.lineWidth ?? regSignal.lineWidth ?? 1),
};
}) })
: merged; : merged;
const nextVis = { ...plotVisibility }; const nextVis = { ...plotVisibility };
if (type === 'OBV') { if ((type === 'CCI' || type === 'RSI') && params.maType === 'None') {
nextVis.plot1 = true;
} else if ((type === 'CCI' || type === 'RSI') && params.maType === 'None') {
nextVis.plot1 = false; nextVis.plot1 = false;
} else if (nextVis.plot1 !== false) { } else if (nextVis.plot1 !== false) {
nextVis.plot1 = true; nextVis.plot1 = true;
+9
View File
@@ -45,6 +45,15 @@ export function plotColorCss(value: string): string {
return `rgba(${r},${g},${b},${(alpha / 100).toFixed(2)})`; return `rgba(${r},${g},${b},${(alpha / 100).toFixed(2)})`;
} }
/** line 시리즈 색 — DB에 alpha 0·8자리 hex 저장 시 registry 기본색으로 복원 */
export function resolvePlotLineColor(color: string | undefined, fallback: string): string {
const raw = (color ?? fallback).trim();
const fb = fallback.length >= 7 && fallback.startsWith('#') ? fallback.slice(0, 7) : '#2962FF';
const { alpha } = parsePlotColor(raw);
if (alpha < 10) return fb;
return plotColorCss(raw);
}
export const LINE_WIDTH_OPTIONS = [1, 2, 3, 4] as const; export const LINE_WIDTH_OPTIONS = [1, 2, 3, 4] as const;
export const LINE_STYLE_OPTIONS: HLineStyle[] = ['solid', 'dashed', 'dotted']; export const LINE_STYLE_OPTIONS: HLineStyle[] = ['solid', 'dashed', 'dotted'];