실시간 차트 캔들차트 높이 조절

This commit is contained in:
Macbook
2026-06-05 21:28:11 +09:00
parent 8c82c029e6
commit a0c8600607
4 changed files with 174 additions and 19 deletions
+95 -1
View File
@@ -250,6 +250,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
const candleOnlyModeRef = useRef(false);
const paneAreaRatioRef = useRef(paneAreaRatio);
paneAreaRatioRef.current = paneAreaRatio;
/** 사용자 드래그로 설정한 pane 비율 — prop 변경 시 초기화 */
const userDraggedRatioRef = useRef<{ candle: number; aux: number } | null>(null);
const lastCrosshairPriceRef = useRef<number | null>(null);
const onTradeOrderRequestRef = useRef(onTradeOrderRequest);
onTradeOrderRequestRef.current = onTradeOrderRequest;
@@ -276,6 +278,13 @@ const TradingChart: React.FC<TradingChartProps> = ({
const indicatorSyncInFlightRef = useRef(false);
const [paneLegendPaused, setPaneLegendPaused] = useState(false);
const [chartBodyHeight, setChartBodyHeight] = useState(0);
/** 캔들 pane 하단 Y 위치 (드래그 핸들용) — null 이면 보조지표 없음 */
const [candlePaneBottomY, setCandlePaneBottomY] = useState<number | null>(null);
const paneResizeDragRef = useRef<{
startClientY: number;
startCandleH: number;
wrapperH: number;
} | null>(null);
const pendingIndicatorResyncRef = useRef(false);
const indicatorReloadGenRef = useRef(0);
const prevMarket = useRef<string>(market);
@@ -324,6 +333,74 @@ const TradingChart: React.FC<TradingChartProps> = ({
return () => ro.disconnect();
}, [chartMgr]);
/** 캔들 pane 하단 Y 추적 — 보조지표 있을 때만 드래그 핸들 표시 */
useEffect(() => {
const mgr = managerRef.current;
if (!mgr || !chartMgr) return;
const updateHandle = () => {
const layouts = mgr.getPaneLayouts();
const candle = layouts.find(l => l.paneIndex === 0);
const hasAux = layouts.some(l => l.paneIndex >= 2 && l.height > 20);
if (candle && candle.height > 20 && hasAux) {
setCandlePaneBottomY(candle.topY + candle.height);
} else {
setCandlePaneBottomY(null);
}
};
updateHandle();
return mgr.subscribePaneLayout(updateHandle);
}, [chartMgr]);
/** 캔들/보조지표 영역 분리선 드래그 핸들러 */
const onPaneResizePointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
const mgr = managerRef.current;
const wrapper = wrapperRef.current;
if (!mgr || !wrapper) return;
e.preventDefault();
e.stopPropagation();
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
const layouts = mgr.getPaneLayouts();
const candle = layouts.find(l => l.paneIndex === 0);
paneResizeDragRef.current = {
startClientY: e.clientY,
startCandleH: candle?.height ?? 200,
wrapperH: wrapper.clientHeight,
};
}, []);
const onPaneResizePointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
const drag = paneResizeDragRef.current;
const mgr = managerRef.current;
const wrapper = wrapperRef.current;
const container = containerRef.current;
if (!drag || !mgr || !wrapper || !container) return;
const dy = e.clientY - drag.startClientY;
const H = drag.wrapperH;
const MIN_CANDLE = 100;
const MIN_AUX = 60;
const newCandleH = Math.max(MIN_CANDLE, Math.min(H - MIN_AUX, drag.startCandleH + dy));
const newCandle = newCandleH / H;
const newAux = 1 - newCandle;
mgr.setPaneAreaRatio(newCandle, newAux);
// applyPaneLayout 이 ResizeObserver 로 재실행돼도 이 비율 유지
userDraggedRatioRef.current = { candle: newCandle, aux: newAux };
const required = mgr.resetPaneHeights(H);
// 스크롤 영역이 필요한 경우 컨테이너 확장
const newContainerH = required > H + 4 ? required : H;
if (container.style.height !== `${newContainerH}px`) {
container.style.height = `${newContainerH}px`;
}
}, []);
const onPaneResizePointerUp = useCallback(() => {
paneResizeDragRef.current = null;
}, []);
const toggleCandleOnly = useCallback(() => {
if (candleOnlyLocked) return;
setCandleOnlyMode(v => !v);
@@ -410,7 +487,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
if (mgr.isCandleOnlyLayout()) {
mgr.restoreFromCandleFullscreen(wrapperH);
}
const ratio = paneAreaRatioRef.current;
// 사용자 드래그 비율 우선, 없으면 prop 기반 비율
const ratio = userDraggedRatioRef.current ?? paneAreaRatioRef.current;
if (ratio && ratio.aux > 0) {
mgr.setPaneAreaRatio(ratio.candle, ratio.aux);
} else {
@@ -443,6 +521,11 @@ const TradingChart: React.FC<TradingChartProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paneLayoutClamp, paneAreaRatio]);
/** paneAreaRatio prop 이 외부에서 변경되면 드래그 비율 초기화 (전략 변경 등) */
useEffect(() => {
userDraggedRatioRef.current = null;
}, [paneAreaRatio]);
/** pane 레이아웃 + 가격·시간축 정상화 (느린 네트워크·레이아웃 지연 후 복구용) */
const normalizeChartViewport = useCallback((mgr: ChartManager) => {
applyPaneLayout(mgr);
@@ -1614,6 +1697,17 @@ const TradingChart: React.FC<TradingChartProps> = ({
ref={containerRef}
className="chart-container"
/>
{/* 캔들 pane 하단 드래그 핸들 — 캔들/보조지표 높이 비율 조절 */}
{candlePaneBottomY != null && !candleOnlyMode && !paneLayoutClamp && chartPaintReady && (
<div
className="chart-pane-resize-handle"
style={{ top: candlePaneBottomY }}
onPointerDown={onPaneResizePointerDown}
onPointerMove={onPaneResizePointerMove}
onPointerUp={onPaneResizePointerUp}
onPointerCancel={onPaneResizePointerUp}
/>
)}
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
<ChartRightToolbar
manager={chartMgr}
@@ -35,7 +35,6 @@ import {
resolveStrategyPrimaryTimeframe,
} from '../../utils/strategyToChartIndicators';
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
import StrategyOscillatorPanes from './StrategyOscillatorPanes';
interface Props {
symbol: string;
@@ -73,6 +72,8 @@ function newIndId() {
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>,
): IndicatorConfig[] {
@@ -89,11 +90,32 @@ function defaultOverlayIndicators(
params,
plots: def.plots,
hlines: def.hlines ?? [],
timeframeVisibility: { '1m': true, '3m': true, '5m': true, '10m': true, '15m': true, '30m': true, '1h': true, '4h': true, '1D': true, '1W': true, '1M': true },
timeframeVisibility: ALL_TF_VISIBLE,
}));
return [cfg];
}
/** 전략 미선택 시 기본 오실레이터(RSI, MACD)를 TradingChart sub-pane 으로 추가 */
function defaultOscillatorIndicators(
getParams: (t: string, d?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
): IndicatorConfig[] {
const result: IndicatorConfig[] = [];
for (const type of ['RSI', 'MACD']) {
const def = getIndicatorDef(type);
if (!def) continue;
const params: Record<string, number | string | boolean> = { ...def.defaultParams, ...getParams(type, def.defaultParams) };
result.push(enrichIndicatorConfig({
id: newIndId(),
type,
params,
plots: def.plots,
hlines: def.hlines ?? [],
timeframeVisibility: ALL_TF_VISIBLE,
}));
}
return result;
}
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
/**
@@ -156,9 +178,11 @@ const BacktestAnalysisChart: React.FC<Props> = ({
}, [strategyId]);
/**
* overlayVisibility 가 없는 경우(BacktestHistoryPage 컨텍스트)에는
* 보조지표(오실레이터)TradingChart sub-pane 이 아닌 아래쪽
* StrategyOscillatorPanes 섹션에서 표시한다.
* 백테스트 화면(overlayVisibility 없음)에서 보조지표를 TradingChart sub-pane 으로 통합.
* - 기존: 오실레이터를 SVG 기반 StrategyOscillatorPanes 에서 별도 렌더링
* → 캔들 차트와 시간 축(x축) 불일치, 우측 가격 라벨 없음
* - 변경: 모든 지표를 TradingChart sub-pane 으로 통합
* → 시간 축 완전 동기화, 우측 가격 라벨 표시
*/
const showOscillatorPanel = !overlayVisibility;
@@ -166,13 +190,15 @@ const BacktestAnalysisChart: React.FC<Props> = ({
let inds: IndicatorConfig[];
if (strategy) {
inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
// 오실레이터 패널을 별도 섹션으로 표시할 때는 TradingChart 에서 제거
if (showOscillatorPanel) {
inds = inds.filter(i => isOverlayType(i.type));
}
// [수정] 오실레이터 필터 제거 — TradingChart sub-pane 에서 직접 렌더링
// (기존: showOscillatorPanel 시 overlay 지표만 남기고 나머지는 SVG 패널로 분리)
inds = inds.filter(i => i.timeframeVisibility?.[chartTimeframe] !== false);
} else {
inds = defaultOverlayIndicators(getParams);
// 전략 미선택 시 기본 오실레이터(RSI, MACD)도 TradingChart sub-pane 으로 추가
if (showOscillatorPanel) {
inds = [...inds, ...defaultOscillatorIndicators(getParams)];
}
}
if (overlayVisibility) {
inds = ensurePaperChartOverlays(inds, getParams, getVisualConfig);
@@ -467,9 +493,7 @@ const BacktestAnalysisChart: React.FC<Props> = ({
<div className="btd-analysis-error"> .</div>
)}
</div>
{showOscillatorPanel && !loading && bars.length > 0 && (
<StrategyOscillatorPanes bars={bars} strategy={strategy} />
)}
{/* 오실레이터 pane 은 TradingChart 내부 sub-pane 으로 통합 (시간 축 동기화) */}
</div>
</div>
);