munger-engine / app /components /StockChart.tsx
dromerosm's picture
feat: integrate real Alpaca orders in detail page and chart with layer toggles
bf9d545
'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>
);
}