111 lines
3.1 KiB
TypeScript
111 lines
3.1 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import * as LightweightCharts from "lightweight-charts";
|
|
|
|
type ChartTime = string | number;
|
|
|
|
interface ChartDataPoint {
|
|
time: ChartTime;
|
|
open: number;
|
|
high: number;
|
|
low: number;
|
|
close: number;
|
|
}
|
|
|
|
interface TradingViewChartProps {
|
|
ticker: string;
|
|
data?: ChartDataPoint[];
|
|
timeframe?: string;
|
|
currentPrice?: number;
|
|
// priceStream.subscribe(cb) should return an unsubscribe function
|
|
priceStream?: { subscribe: (cb: (price: number) => void) => () => void };
|
|
}
|
|
|
|
const TIMEFRAME_HEIGHTS: Record<string, number> = {
|
|
"1D": 300,
|
|
"5Min": 250,
|
|
"15Min": 250,
|
|
"1H": 350,
|
|
"1W": 400,
|
|
};
|
|
|
|
export default function TradingViewChart({ ticker, data, timeframe = "1D", currentPrice, priceStream }: TradingViewChartProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [livePrice, setLivePrice] = useState<number | undefined>(undefined);
|
|
const height = TIMEFRAME_HEIGHTS[timeframe] ?? 400;
|
|
|
|
const isIntraday = ["1Min", "5Min", "15Min", "30Min", "1H"].includes(timeframe);
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) {
|
|
return;
|
|
}
|
|
|
|
const chart = LightweightCharts.createChart(containerRef.current, {
|
|
height,
|
|
autoSize: true,
|
|
});
|
|
|
|
// Configure time scale based on timeframe and range
|
|
chart.timeScale().applyOptions({
|
|
timeVisible: isIntraday,
|
|
secondsVisible: timeframe === "1Min",
|
|
});
|
|
|
|
const candlestickSeries = chart.addSeries(LightweightCharts.CandlestickSeries, {
|
|
upColor: "#26a69a",
|
|
downColor: "#ef5350",
|
|
borderUpColor: "#26a69a",
|
|
borderDownColor: "#ef5350",
|
|
wickUpColor: "#26a69a",
|
|
wickDownColor: "#ef5350",
|
|
});
|
|
|
|
if (data && data.length > 0) {
|
|
try {
|
|
candlestickSeries.setData(data as any);
|
|
// Fit the visible data range
|
|
chart.timeScale().fitContent();
|
|
} catch (err) {
|
|
console.error(`TradingViewChart: error setting data for ${ticker}`, err);
|
|
}
|
|
}
|
|
|
|
return () => chart.remove();
|
|
}, [data, ticker, isIntraday, timeframe]);
|
|
|
|
// Subscribe to a streaming price if provided
|
|
useEffect(() => {
|
|
if (!priceStream) return;
|
|
let unsub: (() => void) | void = undefined;
|
|
try {
|
|
unsub = priceStream.subscribe((p: number) => {
|
|
setLivePrice(p);
|
|
});
|
|
} catch (e) {
|
|
console.warn("TradingViewChart: priceStream subscribe failed", e);
|
|
}
|
|
return () => {
|
|
try {
|
|
if (typeof unsub === "function") unsub();
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
};
|
|
}, [priceStream]);
|
|
|
|
const derivedPrice = currentPrice ?? livePrice ?? (data && data.length ? data[data.length - 1].close : undefined);
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl shadow-lg p-4">
|
|
<div className="flex items-baseline justify-between mb-3">
|
|
<h3 className="text-lg font-bold">{ticker} Price Chart</h3>
|
|
{typeof derivedPrice === "number" ? (
|
|
<div data-testid="current-price" className="text-xl font-semibold text-gray-900">
|
|
${derivedPrice.toFixed(2)}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<div ref={containerRef} className="w-full" />
|
|
</div>
|
|
);
|
|
} |