obv 선 표시
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user