import { useRef, useEffect } from 'react' interface PriceCandle { time: string open: number high: number low: number close: number volume: number } interface LinePoint { time: string value: number } interface ChartEvent { date: string title: string level: string impact_score?: number } interface Props { priceData: PriceCandle[] indicators: { ma20?: LinePoint[] ma50?: LinePoint[] ma100?: LinePoint[] ma200?: LinePoint[] bb_upper?: LinePoint[] bb_lower?: LinePoint[] } events?: ChartEvent[] height?: number } const MA_COLORS: Record = { ma20: '#f59e0b', ma50: '#3b82f6', ma100: '#22d3ee', ma200: '#a855f7', } const LEVEL_COLORS: Record = { long: '#7c3aed', medium: '#3b82f6', short: '#10b981', } export default function InstrumentChart({ priceData, indicators, events = [], height = 420 }: Props) { const containerRef = useRef(null) const cleanupRef = useRef<(() => void) | null>(null) useEffect(() => { cleanupRef.current?.() cleanupRef.current = null if (!containerRef.current || !priceData.length) return let active = true import('lightweight-charts').then((lc: any) => { if (!active || !containerRef.current) return const { createChart, CrosshairMode, ColorType, LineStyle } = lc const chart = createChart(containerRef.current, { width: containerRef.current.clientWidth, height, layout: { background: { type: ColorType.Solid, color: 'transparent' }, textColor: '#64748b', fontSize: 11, }, grid: { vertLines: { color: 'rgba(255,255,255,0.04)' }, horzLines: { color: 'rgba(255,255,255,0.04)' }, }, crosshair: { mode: CrosshairMode.Normal }, rightPriceScale: { borderColor: 'rgba(255,255,255,0.1)' }, timeScale: { borderColor: 'rgba(255,255,255,0.1)', timeVisible: true, secondsVisible: false, fixLeftEdge: true, fixRightEdge: true, }, handleScroll: true, handleScale: true, }) // Volume histogram (bottom 18% of chart) const totalVol = priceData.reduce((s, d) => s + d.volume, 0) if (totalVol > 0) { const volSeries = chart.addHistogramSeries({ color: 'rgba(148,163,184,0.2)', priceFormat: { type: 'volume' }, priceScaleId: 'volume', }) chart.priceScale('volume').applyOptions({ scaleMargins: { top: 0.82, bottom: 0 } }) volSeries.setData(priceData.map(d => ({ time: d.time, value: d.volume, color: d.close >= d.open ? 'rgba(16,185,129,0.25)' : 'rgba(239,68,68,0.25)', }))) } // Candlestick series const candleSeries = chart.addCandlestickSeries({ upColor: '#10b981', downColor: '#ef4444', borderUpColor: '#10b981', borderDownColor:'#ef4444', wickUpColor: '#6ee7b7', wickDownColor: '#fca5a5', }) candleSeries.setData(priceData) // MA lines for (const [key, color] of Object.entries(MA_COLORS)) { const data = indicators[key as keyof typeof indicators] if (data?.length) { const s = chart.addLineSeries({ color, lineWidth: key === 'ma200' ? 1.5 : 1, priceLineVisible: false, lastValueVisible: true, crosshairMarkerVisible: false, }) s.setData(data) } } // Bollinger Bands if (indicators.bb_upper?.length && indicators.bb_lower?.length) { const bbOpts = { color: 'rgba(148,163,184,0.32)', lineWidth: 1, lineStyle: LineStyle.Dashed, priceLineVisible: false, lastValueVisible: false, crosshairMarkerVisible: false, } chart.addLineSeries(bbOpts).setData(indicators.bb_upper) chart.addLineSeries(bbOpts).setData(indicators.bb_lower) } // Event markers const timeSet = new Set(priceData.map(d => d.time)) const markers = events .filter(ev => timeSet.has(ev.date)) .map(ev => ({ time: ev.date, position: 'aboveBar' as const, color: LEVEL_COLORS[ev.level] ?? '#f59e0b', shape: 'arrowDown' as const, text: ev.title.slice(0, 20), })) .sort((a, b) => a.time.localeCompare(b.time)) if (markers.length) candleSeries.setMarkers(markers) chart.timeScale().fitContent() const ro = new ResizeObserver(() => { if (containerRef.current) chart.applyOptions({ width: containerRef.current.clientWidth }) }) ro.observe(containerRef.current) cleanupRef.current = () => { ro.disconnect(); chart.remove() } }) return () => { active = false cleanupRef.current?.() cleanupRef.current = null } }, [priceData, indicators, events, height]) return (
{Object.entries(MA_COLORS).map(([k, c]) => ( {k.toUpperCase()} ))} BB(20,2) {Object.entries(LEVEL_COLORS).map(([l, c]) => ( {l === 'long' ? 'LT' : l === 'medium' ? 'MT' : 'CT'} ))}
{!priceData.length ? (
Chargement...
) : (
)}
) }