b34bdda073
- 전략 DSL 시간봉(5m)과 실행 시간봉(3m) 불일치 시 자동 동기화 - 업비트 캔들 200봉 제한 → 페이지네이션으로 800봉까지 조회 - 백테스트 차트는 실행 타임프레임 기준으로 표시 (시그널·지표 정렬) Co-authored-by: Cursor <cursoragent@cursor.com>
497 lines
19 KiB
TypeScript
497 lines
19 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from 'react';
|
|
import TradingChart from '../TradingChart';
|
|
import type { BacktestSignal, StrategyDto } from '../../utils/backendApi';
|
|
import { loadStrategyForNotification } from '../../utils/backendApi';
|
|
import { isStrategyMissingOnServer } from '../../utils/strategyMissingCache';
|
|
import type { ChartType, IndicatorConfig, OHLCVBar, Theme, Timeframe } from '../../types';
|
|
import { loadAnalysisCandles } from '../../utils/analysisChartData';
|
|
import type { ChartManager } from '../../utils/ChartManager';
|
|
import { useHistoryLoader, LOAD_MORE_TRIGGER } from '../../hooks/useHistoryLoader';
|
|
import { isUpbitMarket } from '../../utils/upbitApi';
|
|
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
|
import {
|
|
formatChartTypeKo,
|
|
formatTimeframeKo,
|
|
normalizeChartTimeframe,
|
|
timeframeBarSeconds,
|
|
} from '../../utils/backtestUiUtils';
|
|
import { getKoreanName } from '../../utils/marketNameCache';
|
|
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
|
import { useAppSettings } from '../../hooks/useAppSettings';
|
|
import { getIndicatorDef } from '../../utils/indicatorRegistry';
|
|
import {
|
|
chartPaneFlexRatio,
|
|
countNonOverlayIndicatorPanes,
|
|
} from '../../utils/strategyOscillatorSeries';
|
|
import { buildChartIndicatorConfig } from '../../utils/indicatorPaneMerge';
|
|
import {
|
|
applyPaperOverlayVisibility,
|
|
DEFAULT_PAPER_OVERLAY_VISIBILITY,
|
|
listPaperOverlayToggleItems,
|
|
paperOverlayVisibilityToChart,
|
|
type PaperOverlayToggleKey,
|
|
type PaperOverlayVisibility,
|
|
} from '../../utils/paperChartOverlayVisibility';
|
|
import {
|
|
buildVirtualTradingChartIndicators,
|
|
ensurePaperChartOverlays,
|
|
resolveStrategyPrimaryTimeframe,
|
|
} from '../../utils/strategyToChartIndicators';
|
|
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
|
|
|
interface Props {
|
|
symbol: string;
|
|
timeframe: string;
|
|
toTimeSec: number;
|
|
fromTimeSec?: number;
|
|
barCount: number;
|
|
signals: BacktestSignal[];
|
|
strategyId?: number | null;
|
|
theme?: Theme;
|
|
/** 과거 캔들 추가 로드 후 (차트에 반영된 전체 봉) */
|
|
onHistoryBarsLoaded?: (bars: OHLCVBar[]) => void;
|
|
/** 과거 로드·시그널 재계산 중 UI */
|
|
onHistoryLoadingChange?: (loading: boolean) => void;
|
|
/** 투자관리 차트 탭 — 캔들 오버레이 표시/숨김 (세션만, DB 미저장) */
|
|
overlayVisibility?: PaperOverlayVisibility;
|
|
onToggleOverlay?: (key: PaperOverlayToggleKey) => void;
|
|
/** 분석 레포트 팝업 (백테스팅 상단 툴바) */
|
|
onOpenReport?: () => void;
|
|
reportDisabled?: boolean;
|
|
/** 거래 로그 클릭 시 해당 시점으로 차트 이동 */
|
|
focusTimeSec?: number | null;
|
|
}
|
|
|
|
const noop = () => {};
|
|
|
|
let _indSeq = 0;
|
|
function newIndId() {
|
|
_indSeq += 1;
|
|
return `btd_${_indSeq}_${Date.now()}`;
|
|
}
|
|
|
|
const ALL_TF_VISIBLE = { '1m': true, '3m': true, '5m': true, '10m': true, '15m': true, '30m': true, '1h': true, '4h': true, '1D': true, '1W': true, '1M': true };
|
|
|
|
function defaultOverlayIndicators(
|
|
getParams: (t: string, d?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
|
|
getVisualConfig: ReturnType<typeof useIndicatorSettings>['getVisualConfig'],
|
|
): IndicatorConfig[] {
|
|
const cfg = buildChartIndicatorConfig('SMA', newIndId(), getParams, getVisualConfig, {
|
|
timeframeVisibility: ALL_TF_VISIBLE,
|
|
});
|
|
return cfg ? [cfg] : [];
|
|
}
|
|
|
|
/** 전략 미선택 시 기본 오실레이터(RSI, MACD)를 TradingChart sub-pane 으로 추가 */
|
|
function defaultOscillatorIndicators(
|
|
getParams: (t: string, d?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
|
|
getVisualConfig: ReturnType<typeof useIndicatorSettings>['getVisualConfig'],
|
|
): IndicatorConfig[] {
|
|
return ['RSI', 'MACD']
|
|
.map(type => buildChartIndicatorConfig(type, newIndId(), getParams, getVisualConfig, {
|
|
timeframeVisibility: ALL_TF_VISIBLE,
|
|
}))
|
|
.filter((c): c is IndicatorConfig => c != null);
|
|
}
|
|
|
|
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
|
|
|
|
/**
|
|
* 백테스트·투자관리 차트 — TradingChart 단일 엔진(실시간 차트와 동일 크로스헤어·시간축·우측 가격)
|
|
*/
|
|
const BacktestAnalysisChart: React.FC<Props> = ({
|
|
symbol,
|
|
timeframe: timeframeRaw,
|
|
toTimeSec,
|
|
fromTimeSec,
|
|
barCount,
|
|
signals,
|
|
strategyId,
|
|
theme = 'dark',
|
|
onHistoryBarsLoaded,
|
|
onHistoryLoadingChange,
|
|
overlayVisibility,
|
|
onToggleOverlay,
|
|
onOpenReport,
|
|
reportDisabled = false,
|
|
focusTimeSec = null,
|
|
}) => {
|
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
|
const { defaults: appDefaults } = useAppSettings();
|
|
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [chartType] = useState<ChartType>('candlestick');
|
|
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
|
const [strategyLoading, setStrategyLoading] = useState(false);
|
|
/** 개별 지표 숨김 ID 집합 (실시간 차트와 동일한 per-indicator 숨김 지원) */
|
|
const [hiddenIndicatorIds, setHiddenIndicatorIds] = useState<ReadonlySet<string>>(new Set());
|
|
/** 백테스트·분석 레포트 — 세션 전용 오버레이 on/off (투자관리 탭은 부모 props 사용) */
|
|
const [internalOverlayVisibility, setInternalOverlayVisibility] = useState<PaperOverlayVisibility>(
|
|
() => ({ ...DEFAULT_PAPER_OVERLAY_VISIBILITY }),
|
|
);
|
|
const effectiveOverlayVisibility = overlayVisibility ?? internalOverlayVisibility;
|
|
const managerRef = useRef<ChartManager | null>(null);
|
|
const overlayVisibilityRef = useRef(effectiveOverlayVisibility);
|
|
const logicalRangeUnsubRef = useRef<(() => void) | null>(null);
|
|
const viewportUnsubRef = useRef<(() => void) | null>(null);
|
|
const loadMoreRefRef = useRef<MutableRefObject<() => void>>({ current: () => {} });
|
|
overlayVisibilityRef.current = effectiveOverlayVisibility;
|
|
const signalsRef = useRef(signals);
|
|
signalsRef.current = signals;
|
|
const onHistoryBarsLoadedRef = useRef(onHistoryBarsLoaded);
|
|
onHistoryBarsLoadedRef.current = onHistoryBarsLoaded;
|
|
const [loadedBarCount, setLoadedBarCount] = useState<number | null>(null);
|
|
const market = symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
|
|
|
|
const chartTimeframe = useMemo((): Timeframe => {
|
|
// 백테스트 결과: 실행 타임프레임 우선 (전략 DSL TF와 실행 TF 불일치 시 차트·시그널 어긋남 방지)
|
|
const tf = timeframeRaw || (strategy ? resolveStrategyPrimaryTimeframe(strategy) : '1m');
|
|
return normalizeChartTimeframe(tf) as Timeframe;
|
|
}, [strategy, timeframeRaw]);
|
|
|
|
useEffect(() => {
|
|
if (!strategyId || isStrategyMissingOnServer(strategyId)) {
|
|
setStrategy(undefined);
|
|
setStrategyLoading(false);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
setStrategyLoading(true);
|
|
void loadStrategyForNotification(strategyId).then(s => {
|
|
if (!cancelled) {
|
|
setStrategy(s ?? undefined);
|
|
setStrategyLoading(false);
|
|
}
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [strategyId]);
|
|
|
|
/**
|
|
* 백테스트 화면(overlayVisibility 없음)에서 보조지표를 TradingChart sub-pane 으로 통합.
|
|
* - 기존: 오실레이터를 SVG 기반 StrategyOscillatorPanes 에서 별도 렌더링
|
|
* → 캔들 차트와 시간 축(x축) 불일치, 우측 가격 라벨 없음
|
|
* - 변경: 모든 지표를 TradingChart sub-pane 으로 통합
|
|
* → 시간 축 완전 동기화, 우측 가격 라벨 표시
|
|
*/
|
|
const showOscillatorPanel = !overlayVisibility;
|
|
|
|
const baseIndicators = useMemo(() => {
|
|
if (strategyId && strategyLoading) return [];
|
|
|
|
let inds: IndicatorConfig[];
|
|
if (strategy) {
|
|
inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
|
|
inds = inds.filter(i => i.timeframeVisibility?.[chartTimeframe] !== false);
|
|
if (showOscillatorPanel && inds.every(i => isOverlayType(i.type))) {
|
|
inds = [...inds, ...defaultOscillatorIndicators(getParams, getVisualConfig)];
|
|
}
|
|
} else {
|
|
inds = defaultOverlayIndicators(getParams, getVisualConfig);
|
|
if (showOscillatorPanel) {
|
|
inds = [...inds, ...defaultOscillatorIndicators(getParams, getVisualConfig)];
|
|
}
|
|
}
|
|
inds = ensurePaperChartOverlays(inds, getParams, getVisualConfig);
|
|
return inds;
|
|
}, [strategy, strategyId, strategyLoading, getParams, getVisualConfig, chartTimeframe, overlayVisibility, showOscillatorPanel]);
|
|
|
|
const indicators = useMemo(() => {
|
|
let inds = applyPaperOverlayVisibility(baseIndicators, effectiveOverlayVisibility);
|
|
// 개별 지표 숨김 상태 반영 (per-indicator hidden toggle)
|
|
if (hiddenIndicatorIds.size > 0) {
|
|
inds = inds.map(ind =>
|
|
hiddenIndicatorIds.has(ind.id) ? { ...ind, hidden: true } : ind,
|
|
);
|
|
}
|
|
return inds;
|
|
}, [baseIndicators, effectiveOverlayVisibility, hiddenIndicatorIds]);
|
|
|
|
const candleOverlayToggles = useMemo(
|
|
() => listPaperOverlayToggleItems(baseIndicators, effectiveOverlayVisibility),
|
|
[baseIndicators, effectiveOverlayVisibility],
|
|
);
|
|
|
|
/**
|
|
* 캔들 오버레이 그룹 토글 — 실시간 차트와 동일하게 ChartManager 를 즉시 호출한 뒤
|
|
* 부모 state 도 업데이트한다.
|
|
*/
|
|
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
|
const pKey = key as PaperOverlayToggleKey;
|
|
const prev = overlayVisibilityRef.current;
|
|
if (pKey in prev) {
|
|
const newVisible = !prev[pKey];
|
|
managerRef.current?.setChartOverlayGroupVisible(key, newVisible);
|
|
}
|
|
if (onToggleOverlay) {
|
|
onToggleOverlay(pKey);
|
|
} else {
|
|
setInternalOverlayVisibility(prev => ({ ...prev, [pKey]: !prev[pKey] }));
|
|
}
|
|
}, [onToggleOverlay]);
|
|
|
|
/**
|
|
* 개별 보조지표 숨김/표시 — 실시간 차트와 동일한 per-indicator 토글
|
|
*/
|
|
const handleToggleIndicatorHidden = useCallback((id: string) => {
|
|
setHiddenIndicatorIds(prev => {
|
|
const next = new Set(prev);
|
|
const willHide = !next.has(id);
|
|
if (willHide) {
|
|
next.add(id);
|
|
} else {
|
|
next.delete(id);
|
|
}
|
|
// ChartManager 즉시 반영
|
|
managerRef.current?.toggleIndicatorVisible(id, !willHide);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const auxPaneCount = useMemo(
|
|
() => countNonOverlayIndicatorPanes(indicators, isOverlayType),
|
|
[indicators],
|
|
);
|
|
|
|
const paneAreaRatio = useMemo(() => {
|
|
if (auxPaneCount <= 0) return null;
|
|
const flex = chartPaneFlexRatio(auxPaneCount);
|
|
return { candle: flex.candle, aux: flex.aux };
|
|
}, [auxPaneCount]);
|
|
|
|
const effectiveBarCount = useMemo(() => {
|
|
const barSec = timeframeBarSeconds(timeframeRaw);
|
|
if (fromTimeSec && toTimeSec > fromTimeSec) {
|
|
return Math.max(barCount, Math.min(500, Math.ceil((toTimeSec - fromTimeSec) / barSec) + 50));
|
|
}
|
|
return barCount;
|
|
}, [barCount, fromTimeSec, toTimeSec, timeframeRaw]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError(null);
|
|
setLoadedBarCount(null);
|
|
void loadAnalysisCandles(market, chartTimeframe, toTimeSec, effectiveBarCount)
|
|
.then(data => {
|
|
if (cancelled) return;
|
|
setBars(data);
|
|
})
|
|
.catch(e => {
|
|
if (cancelled) return;
|
|
setError(e instanceof Error ? e.message : '차트 로드 실패');
|
|
setBars([]);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [market, chartTimeframe, toTimeSec, effectiveBarCount]);
|
|
|
|
const applyMarkers = useCallback((attempt = 0) => {
|
|
const mgr = managerRef.current;
|
|
if (!mgr?.hasMainSeries()) {
|
|
if (attempt < 80) requestAnimationFrame(() => applyMarkers(attempt + 1));
|
|
return;
|
|
}
|
|
mgr.setBacktestMarkers(signalsRef.current, true);
|
|
}, []);
|
|
|
|
const applyPaneRatio = useCallback((mgr: ChartManager) => {
|
|
if (paneAreaRatio && paneAreaRatio.aux > 0) {
|
|
mgr.setPaneAreaRatio(paneAreaRatio.candle, paneAreaRatio.aux);
|
|
} else {
|
|
mgr.setPaneAreaRatio(0, 0);
|
|
}
|
|
}, [paneAreaRatio]);
|
|
|
|
const handleBarsPrepended = useCallback((_added: number, total: number) => {
|
|
setLoadedBarCount(total);
|
|
const mgr = managerRef.current;
|
|
if (!mgr) return;
|
|
onHistoryBarsLoadedRef.current?.(mgr.getRawBars());
|
|
// 왼쪽 끝에 붙어 있으면 연속 로드
|
|
requestAnimationFrame(() => {
|
|
const lr = mgr.getVisibleLogicalRange();
|
|
if (lr && lr.from < LOAD_MORE_TRIGGER) {
|
|
loadMoreRefRef.current.current();
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
const { isLoadingMore, loadMoreRef } = useHistoryLoader({
|
|
symbol: market,
|
|
timeframe: chartTimeframe,
|
|
isUpbit: isUpbitMarket(market),
|
|
managerRef,
|
|
onBarsPrepended: handleBarsPrepended,
|
|
});
|
|
|
|
loadMoreRefRef.current = loadMoreRef;
|
|
|
|
const setupHistoryScroll = useCallback((mgr: ChartManager) => {
|
|
logicalRangeUnsubRef.current?.();
|
|
viewportUnsubRef.current?.();
|
|
|
|
const tryLoadMore = () => {
|
|
if (!mgr.hasMainSeries()) return;
|
|
const lr = mgr.getVisibleLogicalRange();
|
|
if (lr && lr.from < LOAD_MORE_TRIGGER) {
|
|
loadMoreRefRef.current.current();
|
|
}
|
|
};
|
|
|
|
logicalRangeUnsubRef.current = mgr.subscribeVisibleLogicalRange(r => {
|
|
if (r && r.from < LOAD_MORE_TRIGGER) {
|
|
loadMoreRefRef.current.current();
|
|
}
|
|
});
|
|
// 커스텀 패닝은 LWC 논리범위 이벤트를 안 낼 수 있어 뷰포트 알림에도 연결
|
|
viewportUnsubRef.current = mgr.subscribeViewport(tryLoadMore);
|
|
requestAnimationFrame(tryLoadMore);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
onHistoryLoadingChange?.(isLoadingMore);
|
|
}, [isLoadingMore, onHistoryLoadingChange]);
|
|
|
|
useEffect(() => () => {
|
|
logicalRangeUnsubRef.current?.();
|
|
logicalRangeUnsubRef.current = null;
|
|
viewportUnsubRef.current?.();
|
|
viewportUnsubRef.current = null;
|
|
}, []);
|
|
|
|
const onManagerReady = useCallback((mgr: ChartManager) => {
|
|
managerRef.current = mgr;
|
|
// 실시간 차트와 동일하게: 마운트 시 오버레이 가시성 ChartManager 에 동기화
|
|
if (overlayVisibilityRef.current) {
|
|
mgr.setChartOverlayVisibility(paperOverlayVisibilityToChart(overlayVisibilityRef.current));
|
|
}
|
|
setupHistoryScroll(mgr);
|
|
setLoadedBarCount(mgr.getRawBarsLength());
|
|
applyPaneRatio(mgr);
|
|
applyMarkers();
|
|
}, [applyMarkers, applyPaneRatio, setupHistoryScroll]);
|
|
|
|
const onCandlesReady = useCallback(() => {
|
|
const mgr = managerRef.current;
|
|
if (!mgr) return;
|
|
setupHistoryScroll(mgr);
|
|
applyPaneRatio(mgr);
|
|
applyMarkers();
|
|
setLoadedBarCount(mgr.getRawBarsLength());
|
|
}, [applyMarkers, applyPaneRatio, setupHistoryScroll]);
|
|
|
|
useEffect(() => {
|
|
applyMarkers();
|
|
}, [signals, bars, applyMarkers]);
|
|
|
|
useEffect(() => {
|
|
if (focusTimeSec == null) return;
|
|
const mgr = managerRef.current;
|
|
if (!mgr) return;
|
|
const barSec = timeframeBarSeconds(timeframeRaw);
|
|
const pad = Math.max(barSec * 48, 3600);
|
|
mgr.applyVisibleTimeRange(focusTimeSec - pad, focusTimeSec + pad);
|
|
}, [focusTimeSec, timeframeRaw]);
|
|
|
|
useEffect(() => {
|
|
const mgr = managerRef.current;
|
|
if (mgr) applyPaneRatio(mgr);
|
|
}, [paneAreaRatio, applyPaneRatio]);
|
|
|
|
const koName = getKoreanName(market);
|
|
const tfKo = formatTimeframeKo(chartTimeframe);
|
|
const chartTypeKo = formatChartTypeKo(chartType);
|
|
const stratTf = chartTimeframe;
|
|
|
|
const priceLabels = appDefaults.chartSeriesPriceLabels !== false;
|
|
|
|
return (
|
|
<div className="btd-analysis-chart">
|
|
<div className="btd-analysis-toolbar">
|
|
<div className="btd-analysis-toolbar-left">
|
|
<span className="btd-analysis-select btd-analysis-select--static">{koName}</span>
|
|
<span className="btd-analysis-select btd-analysis-select--static">{tfKo}</span>
|
|
<span className="btd-analysis-select btd-analysis-select--static">{chartTypeKo}</span>
|
|
</div>
|
|
<div className="btd-analysis-toolbar-right">
|
|
{onOpenReport && (
|
|
<button
|
|
type="button"
|
|
className="btd-analysis-tool btd-analysis-tool--report"
|
|
title={reportDisabled ? '분석 결과를 선택하세요' : '투자분석 상세 레포트 (인쇄·PDF)'}
|
|
aria-label="분석 레포트"
|
|
disabled={reportDisabled}
|
|
onClick={onOpenReport}
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
<polyline points="14 2 14 8 20 8" />
|
|
<line x1="16" y1="13" x2="8" y2="13" />
|
|
<line x1="16" y1="17" x2="8" y2="17" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
<span className="btd-analysis-tool" title="자석 모드">⊕</span>
|
|
<span className="btd-analysis-tool" title="드로잉">✎</span>
|
|
<span className="btd-analysis-tool" title="삭제">⌫</span>
|
|
<span className="btd-analysis-count">
|
|
{(loadedBarCount ?? effectiveBarCount).toLocaleString()}봉
|
|
{isLoadingMore && ' · 과거 로드…'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="btd-analysis-main">
|
|
<div className="btd-analysis-canvas-wrap btd-analysis-canvas-wrap--live">
|
|
{loading && <div className="btd-analysis-loading">차트 로딩…</div>}
|
|
{error && !loading && <div className="btd-analysis-error">{error}</div>}
|
|
{!loading && !error && bars.length > 0 && (
|
|
<TradingChart
|
|
key={`${market}-${chartTimeframe}-${toTimeSec}-${effectiveBarCount}-${indicators.map(i => i.id).join(',')}`}
|
|
bars={bars}
|
|
barsMarket={market}
|
|
market={market}
|
|
timeframe={stratTf}
|
|
chartType={chartType}
|
|
theme={theme}
|
|
mode="chart"
|
|
indicators={indicators}
|
|
drawingTool="cursor"
|
|
drawings={[]}
|
|
logScale={false}
|
|
onCrosshair={noop}
|
|
onManagerReady={onManagerReady}
|
|
onCandlesReady={onCandlesReady}
|
|
onAddDrawing={noop}
|
|
displayTimezone={DEFAULT_DISPLAY_TIMEZONE}
|
|
volumeVisible={false}
|
|
paneSeparatorOptions={appDefaults.chartPaneSeparator}
|
|
paneLayoutClamp
|
|
showPaneResizeHandle={auxPaneCount > 0}
|
|
paneAreaRatio={paneAreaRatio}
|
|
showPaneLegend
|
|
crosshairInfoVisible
|
|
candleAreaPriceLabelsEnabled={priceLabels}
|
|
indicatorAreaPriceLabelsEnabled={priceLabels}
|
|
seriesPriceLabelsEnabled={priceLabels}
|
|
showHoverToolbar
|
|
candleOverlayToggles={candleOverlayToggles}
|
|
onToggleCandleOverlay={handleToggleCandleOverlay}
|
|
showCandleOverlayControls
|
|
onToggleIndicatorHidden={handleToggleIndicatorHidden}
|
|
/>
|
|
)}
|
|
{!loading && !error && bars.length === 0 && (
|
|
<div className="btd-analysis-error">표시할 캔들 데이터가 없습니다.</div>
|
|
)}
|
|
</div>
|
|
{/* 오실레이터 pane 은 TradingChart 내부 sub-pane 으로 통합 (시간 축 동기화) */}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BacktestAnalysisChart;
|