"use client"; import { useEffect, useRef } from "react"; import { createChart, type CandlestickData, type IChartApi, type ISeriesApi, type LineData, type UTCTimestamp, } from "lightweight-charts"; import type { ChartPayload, LatestPredictionResponse } from "../lib/api"; type Props = { chart: ChartPayload; prediction?: LatestPredictionResponse | null; }; function dateToUtcTs(d: string): UTCTimestamp { // 'YYYY-MM-DD' → UTC midnight epoch seconds return (Date.UTC( Number(d.slice(0, 4)), Number(d.slice(5, 7)) - 1, Number(d.slice(8, 10)), ) / 1000) as UTCTimestamp; } export function StockChart({ chart, prediction }: Props) { const containerRef = useRef(null); const chartRef = useRef(null); const candleRef = useRef | null>(null); const predRef = useRef | null>(null); const predLowRef = useRef | null>(null); const predHighRef = useRef | null>(null); // create chart once useEffect(() => { if (!containerRef.current) return; const c = createChart(containerRef.current, { layout: { background: { color: "transparent" }, textColor: "#cbd5e1", }, grid: { vertLines: { color: "#1f2937" }, horzLines: { color: "#1f2937" }, }, rightPriceScale: { borderColor: "#374151" }, timeScale: { borderColor: "#374151", timeVisible: false }, autoSize: true, }); const candle = c.addCandlestickSeries({ upColor: "#22c55e", downColor: "#ef4444", borderUpColor: "#22c55e", borderDownColor: "#ef4444", wickUpColor: "#22c55e", wickDownColor: "#ef4444", }); chartRef.current = c; candleRef.current = candle; return () => { c.remove(); chartRef.current = null; candleRef.current = null; predRef.current = null; predLowRef.current = null; predHighRef.current = null; }; }, []); // push candle data useEffect(() => { if (!candleRef.current) return; const data: CandlestickData[] = chart.ohlcv .filter((p) => p.open !== null && p.high !== null && p.low !== null && p.close !== null) .map((p) => ({ time: dateToUtcTs(p.date), open: p.open as number, high: p.high as number, low: p.low as number, close: p.close as number, })); candleRef.current.setData(data); chartRef.current?.timeScale().fitContent(); }, [chart]); // push prediction overlay useEffect(() => { if (!chartRef.current) return; // remove previous overlay if (predRef.current) { chartRef.current.removeSeries(predRef.current); predRef.current = null; } if (predLowRef.current) { chartRef.current.removeSeries(predLowRef.current); predLowRef.current = null; } if (predHighRef.current) { chartRef.current.removeSeries(predHighRef.current); predHighRef.current = null; } if (!prediction || !prediction.found || !prediction.steps?.length) return; const baseDate = prediction.base_date!; const baseClose = prediction.base_close; if (!baseClose) return; const sorted = [...prediction.steps].sort((a, b) => a.horizon - b.horizon); const med: LineData[] = [ { time: dateToUtcTs(baseDate), value: baseClose }, ...sorted .filter((s) => s.point_close !== null) .map((s) => ({ time: dateToUtcTs(s.target_date), value: s.point_close as number })), ]; const lo: LineData[] = [ { time: dateToUtcTs(baseDate), value: baseClose }, ...sorted .filter((s) => s.ci_low !== null) .map((s) => ({ time: dateToUtcTs(s.target_date), value: s.ci_low as number })), ]; const hi: LineData[] = [ { time: dateToUtcTs(baseDate), value: baseClose }, ...sorted .filter((s) => s.ci_high !== null) .map((s) => ({ time: dateToUtcTs(s.target_date), value: s.ci_high as number })), ]; const medLine = chartRef.current.addLineSeries({ color: "#a78bfa", lineWidth: 2, lineStyle: 2, // dashed priceLineVisible: false, lastValueVisible: true, title: "예측 median", }); medLine.setData(med); const loLine = chartRef.current.addLineSeries({ color: "#7c3aed", lineWidth: 1, lineStyle: 1, priceLineVisible: false, lastValueVisible: false, title: "q10", }); loLine.setData(lo); const hiLine = chartRef.current.addLineSeries({ color: "#7c3aed", lineWidth: 1, lineStyle: 1, priceLineVisible: false, lastValueVisible: false, title: "q90", }); hiLine.setData(hi); predRef.current = medLine; predLowRef.current = loLine; predHighRef.current = hiLine; chartRef.current.timeScale().fitContent(); }, [prediction]); return (
); }