Spaces:
Running
Running
| 'use client'; | |
| import { createChart, ColorType, CrosshairMode, CandlestickSeries, LineSeries, Time } from 'lightweight-charts'; | |
| import { useEffect, useRef, useState } from 'react'; | |
| import { getChartData } from '@/app/actions/chart'; | |
| import { AlpacaOrder } from '@/lib/types'; | |
| interface StockChartProps { | |
| symbol: string; | |
| tradePlan?: { | |
| levels?: { | |
| entry: number; | |
| stopLoss: number; | |
| takeProfit: number; | |
| [key: string]: number | undefined; | |
| }; | |
| [key: string]: any; | |
| }; | |
| alpacaOrders?: AlpacaOrder[]; | |
| showPlan?: boolean; | |
| showReal?: boolean; | |
| } | |
| export function StockChart({ symbol, tradePlan, alpacaOrders, showPlan = true, showReal = true }: StockChartProps) { | |
| const chartContainerRef = useRef<HTMLDivElement>(null); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| if (!chartContainerRef.current) return; | |
| const handleResize = () => { | |
| if (chartContainerRef.current && chart) { | |
| chart.applyOptions({ width: chartContainerRef.current.clientWidth }); | |
| } | |
| }; | |
| const chart = createChart(chartContainerRef.current, { | |
| layout: { | |
| background: { type: ColorType.Solid, color: 'transparent' }, | |
| textColor: '#71717a', // zinc-500 | |
| }, | |
| width: chartContainerRef.current.clientWidth, | |
| height: 400, | |
| grid: { | |
| vertLines: { color: 'rgba(40, 40, 40, 0.05)' }, | |
| horzLines: { color: 'rgba(40, 40, 40, 0.05)' }, | |
| }, | |
| crosshair: { | |
| mode: CrosshairMode.Normal, | |
| }, | |
| rightPriceScale: { | |
| visible: false, | |
| }, | |
| leftPriceScale: { | |
| visible: true, | |
| borderColor: 'rgba(197, 203, 206, 0.3)', | |
| }, | |
| timeScale: { | |
| borderColor: 'rgba(197, 203, 206, 0.3)', | |
| } | |
| }); | |
| const candlestickSeries = chart.addSeries(CandlestickSeries, { | |
| upColor: '#22c55e', // green-500 | |
| downColor: '#ef4444', // red-500 | |
| borderVisible: false, | |
| wickUpColor: '#22c55e', | |
| wickDownColor: '#ef4444', | |
| priceScaleId: 'left', | |
| }); | |
| const wmaSeries = chart.addSeries(LineSeries, { | |
| color: '#3b82f6', // blue-500 | |
| lineWidth: 2, | |
| title: 'WMA 200', | |
| priceScaleId: 'left', | |
| }); | |
| // Fetch Data | |
| getChartData(symbol).then((data) => { | |
| if (data.length > 0) { | |
| const candleData = data.map(d => ({ | |
| time: d.time as Time, | |
| open: d.open, | |
| high: d.high, | |
| low: d.low, | |
| close: d.close, | |
| })); | |
| const wmaData = data | |
| .filter(d => d.wma200 !== undefined) | |
| .map(d => ({ | |
| time: d.time as Time, | |
| value: d.wma200!, | |
| })); | |
| candlestickSeries.setData(candleData); | |
| wmaSeries.setData(wmaData); | |
| // Add Real Alpaca Orders Lines | |
| if (showReal && alpacaOrders && alpacaOrders.length > 0) { | |
| alpacaOrders.forEach(order => { | |
| // Fill Price | |
| if (order.filledAvgPrice && parseFloat(order.filledAvgPrice) > 0) { | |
| candlestickSeries.createPriceLine({ | |
| price: parseFloat(order.filledAvgPrice), | |
| color: '#3b82f6', // blue | |
| lineWidth: 2, | |
| lineStyle: 2, // Dashed | |
| axisLabelVisible: true, | |
| title: 'FILL', | |
| }); | |
| } | |
| // Legs (TP/SL) | |
| if (order.legs && order.legs.length > 0) { | |
| order.legs.forEach(leg => { | |
| if (leg.type === 'limit' && leg.limit_price) { | |
| candlestickSeries.createPriceLine({ | |
| price: parseFloat(leg.limit_price), | |
| color: '#22c55e', // green | |
| lineWidth: 1, | |
| lineStyle: 2, // Dashed | |
| axisLabelVisible: true, | |
| title: 'REAL TP', | |
| }); | |
| } | |
| if (leg.type === 'stop' && leg.stop_price) { | |
| candlestickSeries.createPriceLine({ | |
| price: parseFloat(leg.stop_price), | |
| color: '#ef4444', // red | |
| lineWidth: 1, | |
| lineStyle: 2, // Dashed | |
| axisLabelVisible: true, | |
| title: 'REAL SL', | |
| }); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| // Add Theoretical Trade Plan Lines | |
| if (showPlan && tradePlan && tradePlan.levels) { | |
| // Entry | |
| candlestickSeries.createPriceLine({ | |
| price: tradePlan.levels.entry, | |
| color: '#3b82f6', // blue | |
| lineWidth: 1, | |
| lineStyle: 1, // Dotted | |
| axisLabelVisible: true, | |
| title: 'ENTRY (PLAN)', | |
| }); | |
| // Stop Loss | |
| candlestickSeries.createPriceLine({ | |
| price: tradePlan.levels.stopLoss, | |
| color: '#ef4444', // red | |
| lineWidth: 1, | |
| lineStyle: 1, | |
| axisLabelVisible: true, | |
| title: 'SL (PLAN)', | |
| }); | |
| // Take Profit | |
| candlestickSeries.createPriceLine({ | |
| price: tradePlan.levels.takeProfit, | |
| color: '#22c55e', // green | |
| lineWidth: 1, | |
| lineStyle: 1, | |
| axisLabelVisible: true, | |
| title: 'TP (PLAN)', | |
| }); | |
| } | |
| chart.timeScale().fitContent(); | |
| } | |
| setLoading(false); | |
| }); | |
| window.addEventListener('resize', handleResize); | |
| return () => { | |
| window.removeEventListener('resize', handleResize); | |
| chart.remove(); | |
| }; | |
| }, [symbol, tradePlan, alpacaOrders, showPlan, showReal]); | |
| return ( | |
| <div className="w-full relative bg-white dark:bg-zinc-950/30 rounded-lg border border-zinc-200 dark:border-zinc-800 p-1"> | |
| {loading && ( | |
| <div className="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-black/50 backdrop-blur-sm rounded-lg"> | |
| <span className="text-xs text-zinc-500 animate-pulse">Loading Chart...</span> | |
| </div> | |
| )} | |
| <div ref={chartContainerRef} className="w-full h-[400px]" /> | |
| <div className="absolute top-2 left-2 flex gap-2 text-[10px] bg-white/80 dark:bg-zinc-900/80 p-1 rounded border border-zinc-100 dark:border-zinc-800"> | |
| <span className="text-zinc-500">1W</span> | |
| <span className="font-bold text-zinc-900 dark:text-zinc-100">10Y</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |