From d83ac137a9f353a49239e0eba4c4a7ec5d001529 Mon Sep 17 00:00:00 2001 From: claude-owner Date: Thu, 28 May 2026 19:17:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(chart):=20=EB=A9=94=EC=9D=B8=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=ED=95=98=EB=8B=A8=EC=97=90=20=EA=B1=B0=EB=9E=98?= =?UTF-8?q?=EB=9F=89=20=EB=A7=89=EB=8C=80=20overlay=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 캔들 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 (거래량은 일/분봉 공통으로 의미 있음). --- web/components/StockChart.tsx | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/web/components/StockChart.tsx b/web/components/StockChart.tsx index 67fd462..bb43c6a 100644 --- a/web/components/StockChart.tsx +++ b/web/components/StockChart.tsx @@ -161,6 +161,7 @@ export function StockChart({ chart, prediction }: Props) { const containerRef = useRef(null); const chartRef = useRef(null); const candleRef = useRef | null>(null); + const volumeRef = useRef | null>(null); const predRef = useRef | null>(null); const smaRefs = useRef>>(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]);