feat(chart): 메인 차트 하단에 거래량 막대 overlay 추가

- 캔들 priceScale 을 위 78%(top=0.05, bottom=0.22)로 좁힘
- 거래량 Histogram 시리즈를 overlay priceScale("volume")로 같은 차트에 부착,
  scaleMargins top=0.82 → 차트 영역 아래 18% 만 사용
- 상승봉=빨강(0x40 알파), 하락봉=파랑 — 한국 거래소 관행
- 가격축 라벨/마지막값/priceLine 숨김 → 캔들 시야 방해 최소화
- 시계열은 메인 timeScale 그대로 따라가서 별도 sync 불필요

cleanup 에서 volumeRef.current=null 도 같이 정리. intraday 포함 항상 ON
(거래량은 일/분봉 공통으로 의미 있음).
This commit is contained in:
claude-owner
2026-05-28 19:17:13 +09:00
parent fed8ff287e
commit d83ac137a9

View File

@@ -161,6 +161,7 @@ export function StockChart({ chart, prediction }: Props) {
const containerRef = useRef<HTMLDivElement | null>(null);
const chartRef = useRef<IChartApi | null>(null);
const candleRef = useRef<ISeriesApi<"Candlestick"> | null>(null);
const volumeRef = useRef<ISeriesApi<"Histogram"> | null>(null);
const predRef = useRef<ISeriesApi<"Candlestick"> | null>(null);
const smaRefs = useRef<Map<SmaWindow, ISeriesApi<"Line">>>(new Map());
// RSI 보조차트 — 별도 chart 인스턴스로 메인 아래에 붙여 timeScale 동기화.
@@ -213,13 +214,30 @@ export function StockChart({ chart, prediction }: Props) {
wickUpColor: COLOR_UP,
wickDownColor: COLOR_DOWN,
});
// 캔들 priceScale 을 위쪽 78% 로 좁혀서 거래량 막대를 아래쪽에 둘 공간 확보.
c.priceScale("right").applyOptions({
scaleMargins: { top: 0.05, bottom: 0.22 },
});
// 거래량 막대 — overlay priceScale("volume") 로 같은 chart 안에 띄움.
// bottom 0/top 0.82 → 차트 영역의 아래 18% 만 사용. 가격축 라벨은 숨김.
const volume = c.addHistogramSeries({
priceFormat: { type: "volume" },
priceScaleId: "volume",
priceLineVisible: false,
lastValueVisible: false,
});
c.priceScale("volume").applyOptions({
scaleMargins: { top: 0.82, bottom: 0 },
});
chartRef.current = c;
candleRef.current = candle;
volumeRef.current = volume;
const smaMap = smaRefs.current;
return () => {
c.remove();
chartRef.current = null;
candleRef.current = null;
volumeRef.current = null;
predRef.current = null;
smaMap.clear();
};
@@ -241,6 +259,23 @@ export function StockChart({ chart, prediction }: Props) {
chartRef.current?.timeScale().fitContent();
}, [chart, isIntraday]);
// 거래량 막대 — 종가 ≥ 시가면 한국 관행으로 빨강(상승), 아니면 파랑(하락).
// null volume 은 그 시점 캔들 자체가 빠진 케이스(상장폐지/거래정지) 라 건너뛴다.
useEffect(() => {
if (!volumeRef.current) return;
const data: HistogramData[] = chart.ohlcv
.filter((p) => p.volume != null && p.volume > 0)
.map((p) => {
const up = p.close != null && p.open != null ? p.close >= p.open : true;
return {
time: isoToUtcTs(p.date),
value: p.volume as number,
color: up ? "#ef444466" : "#3b82f666",
};
});
volumeRef.current.setData(data);
}, [chart, isIntraday]);
// SMA 시리즈 — chart.ohlcv 종가 기준. 토글 변화 또는 데이터 변화 시 갱신.
// 비활성 윈도우는 제거하고, 활성은 재계산. 마지막에 fitContent 는 호출 안 함 — 사용자 줌 유지.
const closes = useMemo(() => chart.ohlcv.map((p) => p.close), [chart]);