obv 문제
This commit is contained in:
@@ -11,7 +11,9 @@ import type {
|
|||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { isUpbitMarket } from '../../utils/upbitApi';
|
import { isUpbitMarket } from '../../utils/upbitApi';
|
||||||
import { useChartRealtimeData } from '../../hooks/useChartRealtimeData';
|
import { useChartRealtimeData } from '../../hooks/useChartRealtimeData';
|
||||||
|
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||||
import { useHistoryLoader } from '../../hooks/useHistoryLoader';
|
import { useHistoryLoader } from '../../hooks/useHistoryLoader';
|
||||||
|
import { useAppSettings } from '../../hooks/useAppSettings';
|
||||||
import type { ChartManager } from '../../utils/ChartManager';
|
import type { ChartManager } from '../../utils/ChartManager';
|
||||||
import { pinCandleWatch } from '../../utils/backendApi';
|
import { pinCandleWatch } from '../../utils/backendApi';
|
||||||
import { timeframeToCandleType } from '../../utils/chartCandleType';
|
import { timeframeToCandleType } from '../../utils/chartCandleType';
|
||||||
@@ -51,6 +53,8 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
|||||||
signalMarker,
|
signalMarker,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||||
|
const { defaults: appDefaults } = useAppSettings();
|
||||||
|
const chartRealtimeSource = (appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP') as ChartRealtimeSource;
|
||||||
const managerRef = useRef<ChartManager | null>(null);
|
const managerRef = useRef<ChartManager | null>(null);
|
||||||
const canvasWrapRef = useRef<HTMLDivElement | null>(null);
|
const canvasWrapRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [chartReloadTick, setChartReloadTick] = useState(0);
|
const [chartReloadTick, setChartReloadTick] = useState(0);
|
||||||
@@ -147,9 +151,9 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
|||||||
onTickUpdate: handleTickUpdate,
|
onTickUpdate: handleTickUpdate,
|
||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: enabled && useUpbit,
|
enabled: enabled && useUpbit,
|
||||||
source: 'BACKEND_STOMP' as const,
|
source: chartRealtimeSource,
|
||||||
deepObvHistory: needsDeepObv,
|
deepObvHistory: needsDeepObv,
|
||||||
}), [handleTickUpdate, handleNewCandle, enabled, useUpbit, needsDeepObv]),
|
}), [handleTickUpdate, handleNewCandle, enabled, useUpbit, needsDeepObv, chartRealtimeSource]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isLoadingMore } = useHistoryLoader({
|
const { isLoadingMore } = useHistoryLoader({
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ function detachBbBandFill(entry: IndicatorEntry): void {
|
|||||||
entry.bbFillPrimitive = undefined;
|
entry.bbFillPrimitive = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** OBV·CCI·RSI·TRIX — 본선+신호선 이중 plot (누락 시리즈 보정 대상) */
|
||||||
|
const DUAL_LINE_INDICATOR_TYPES = new Set(['OBV', 'CCI', 'RSI', 'TRIX']);
|
||||||
|
|
||||||
export class ChartManager {
|
export class ChartManager {
|
||||||
private chart: IChartApi;
|
private chart: IChartApi;
|
||||||
private container: HTMLElement;
|
private container: HTMLElement;
|
||||||
@@ -722,6 +725,89 @@ export class ChartManager {
|
|||||||
return loadGen !== this._dataGeneration;
|
return loadGen !== this._dataGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 이중 plot 지표 — addIndicator 루프에서 누락된 plot 시리즈 추가 */
|
||||||
|
private _appendMissingPlotSeriesToLists(
|
||||||
|
seriesList: ISeriesApi<SeriesType>[],
|
||||||
|
seriesMeta: IndicatorEntry['seriesMeta'],
|
||||||
|
config: IndicatorConfig,
|
||||||
|
result: { plots: Record<string, PlotData> },
|
||||||
|
pane: number,
|
||||||
|
indicatorHidden: boolean,
|
||||||
|
def: ReturnType<typeof getIndicatorDef>,
|
||||||
|
): void {
|
||||||
|
const plotDefs = config.plots ?? def?.plots ?? [];
|
||||||
|
const existingIds = new Set(seriesMeta.map(m => m.plotId));
|
||||||
|
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||||
|
|
||||||
|
for (const plotDef of plotDefs) {
|
||||||
|
if (existingIds.has(plotDef.id)) continue;
|
||||||
|
if (config.plotVisibility?.[plotDef.id] === false) continue;
|
||||||
|
if (plotDef.type === 'histogram') continue;
|
||||||
|
|
||||||
|
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 ?? 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 스타일 갱신·초기 추가 후 OBV 등 본선(plot0) 시리즈 누락 보정 */
|
||||||
|
private async _ensureDualLinePlotSeriesForEntry(entry: IndicatorEntry): Promise<void> {
|
||||||
|
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) return;
|
||||||
|
const config = enrichIndicatorConfig(entry.config!);
|
||||||
|
const def = getIndicatorDef(config.type);
|
||||||
|
if (!def) return;
|
||||||
|
const plotDefs = config.plots ?? def.plots ?? [];
|
||||||
|
const expectedCount = plotDefs.filter(
|
||||||
|
p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false,
|
||||||
|
).length;
|
||||||
|
if (entry.seriesList.length >= expectedCount) return;
|
||||||
|
if (this.rawBars.length === 0) return;
|
||||||
|
|
||||||
|
const pane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? 2;
|
||||||
|
const indicatorHidden = !this._isIndicatorEffectivelyVisible(config);
|
||||||
|
try {
|
||||||
|
const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params);
|
||||||
|
this._appendMissingPlotSeriesToLists(
|
||||||
|
entry.seriesList,
|
||||||
|
entry.seriesMeta,
|
||||||
|
config,
|
||||||
|
result,
|
||||||
|
pane,
|
||||||
|
indicatorHidden,
|
||||||
|
def,
|
||||||
|
);
|
||||||
|
entry.config = config;
|
||||||
|
if (entry.seriesList.length > 0) {
|
||||||
|
const actualPane = this._readSeriesPaneIndex(entry.seriesList[0]);
|
||||||
|
if (actualPane >= 2) entry.paneIndex = actualPane;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Indicator] ${config.type} dual-line repair error:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Indicators ─────────────────────────────────────────────────────────
|
// ─── Indicators ─────────────────────────────────────────────────────────
|
||||||
async addIndicator(
|
async addIndicator(
|
||||||
config: IndicatorConfig,
|
config: IndicatorConfig,
|
||||||
@@ -807,6 +893,18 @@ 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
|
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
|
||||||
const hlineRefs: IPriceLine[] = [];
|
const hlineRefs: IPriceLine[] = [];
|
||||||
let fillPrimitive: IndicatorFillPrimitive | undefined;
|
let fillPrimitive: IndicatorFillPrimitive | undefined;
|
||||||
@@ -2849,6 +2947,10 @@ export class ChartManager {
|
|||||||
void this._repaintHistogramSeries(config.id, config);
|
void this._repaintHistogramSeries(config.id, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
||||||
|
void this._ensureDualLinePlotSeriesForEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
if (this._candleOnlyLayout && this._isSubPaneIndicator(entry)) {
|
if (this._candleOnlyLayout && this._isSubPaneIndicator(entry)) {
|
||||||
this._hideSubPaneIndicator(entry);
|
this._hideSubPaneIndicator(entry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ export function calcOBVWithMA(
|
|||||||
obv.push(0);
|
obv.push(0);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const vol = bars[i].volume;
|
const vol = Number(bars[i].volume) || 0;
|
||||||
if (bars[i].close > bars[i - 1].close) acc += vol;
|
if (bars[i].close > bars[i - 1].close) acc += vol;
|
||||||
else if (bars[i].close < bars[i - 1].close) acc -= vol;
|
else if (bars[i].close < bars[i - 1].close) acc -= vol;
|
||||||
obv.push(acc);
|
obv.push(acc);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
normalizeObvParams,
|
normalizeObvParams,
|
||||||
type HLineDef,
|
type HLineDef,
|
||||||
} from './indicatorRegistry';
|
} from './indicatorRegistry';
|
||||||
|
import { initializeIndicatorConfigForEditor } from './indicatorSettingsEditor';
|
||||||
import { buildMainIndicatorConfig } from './indicatorMainConfig';
|
import { buildMainIndicatorConfig } from './indicatorMainConfig';
|
||||||
import { normalizeSmaConfig, smaPeriodKey } from './smaConfig';
|
import { normalizeSmaConfig, smaPeriodKey } from './smaConfig';
|
||||||
import { normalizeIchimokuConfig } from './ichimokuConfig';
|
import { normalizeIchimokuConfig } from './ichimokuConfig';
|
||||||
@@ -227,7 +228,11 @@ function buildOneIndicator(
|
|||||||
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };
|
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return enrichIndicatorConfig(cfg);
|
return initializeIndicatorConfigForEditor(
|
||||||
|
cfg,
|
||||||
|
getParams,
|
||||||
|
(type, defaultPlots, defaultHlines) => getVisual(type, defaultPlots, defaultHlines ?? []),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 전략에 포함된 보조지표를 차트용 IndicatorConfig 로 변환 (중복 type 제거) */
|
/** 전략에 포함된 보조지표를 차트용 IndicatorConfig 로 변환 (중복 type 제거) */
|
||||||
|
|||||||
Reference in New Issue
Block a user