import { useLoaderData, useNavigate, useLocation } from "react-router"; import TradingViewChart from "../components/TradingViewChart"; import Navbar from "../components/Navbar"; export const meta = () => [{ title: "Stock Detail - AITrader" }]; interface LoaderData { ticker: string; position: number | null; orders: any[]; bars: any[]; timeframe: string; limit: number; } const TIMEFRAMES = [ { value: "1D", label: "1 Day" }, { value: "5Min", label: "5 Min" }, { value: "15Min", label: "15 Min" }, { value: "1H", label: "1 Hour" }, { value: "1W", label: "1 Week" }, ]; export async function loader({ params, request }: { params: { ticker: string }; request: Request }) { const ticker = params.ticker?.toUpperCase() || ""; const url = new URL(request.url); const timeframe = url.searchParams.get("timeframe") || "1D"; const limit = parseInt(url.searchParams.get("limit") || "30", 10); console.log(`analyze/${ticker}: loader called with timeframe=${timeframe}, limit=${limit}`); // Build base URL from request for server-side fetches const reqUrl = new URL(request.url); const host = request.headers.get("host") || reqUrl.host; const protocol = reqUrl.protocol; const baseUrl = `${protocol}//${host}`; console.log(`analyze/${ticker}: baseUrl = ${baseUrl}`); let position = null; let orders = []; let bars = []; try { // Fetch position const posRes = await fetch(`${baseUrl}/api/alpaca/positions`); console.log(`analyze/${ticker}: positions status = ${posRes.status}`); const positions = posRes.ok ? await posRes.json() : []; position = positions.find((p: any) => p.ticker === ticker)?.qty ?? null; // Fetch orders const ordRes = await fetch(`${baseUrl}/api/alpaca/orders`); const ordersData = ordRes.ok ? await ordRes.json() : { orders: [] }; orders = ordersData.orders?.filter((o: any) => o.symbol === ticker) || []; // Fetch bars for chart with timeframe and limit console.log(`analyze/${ticker}: fetching bars from ${baseUrl}/api/alpaca/quote/${ticker}?timeframe=${timeframe}&limit=${limit}`); const barsRes = await fetch(`${baseUrl}/api/alpaca/quote/${ticker}?timeframe=${timeframe}&limit=${limit}`); console.log(`analyze/${ticker}: bars response status = ${barsRes.status}`); const barsData = barsRes.ok ? await barsRes.json() : null; console.log(`analyze/${ticker}: barsData =`, JSON.stringify(barsData)); bars = barsData?.bars || []; } catch (err) { console.error(`analyze/${ticker}: loader error`, err); } return Response.json({ ticker, position, orders, bars, timeframe, limit }); } export default function StockDetail() { const { ticker, position, orders, bars, timeframe, limit } = useLoaderData() as LoaderData; const navigate = useNavigate(); const location = useLocation(); const updateParams = (newTimeframe: string, newLimit: number) => { const searchParams = new URLSearchParams(location.search); searchParams.set("timeframe", newTimeframe); searchParams.set("limit", newLimit.toString()); navigate(`${location.pathname}?${searchParams.toString()}`, { replace: true }); }; // Convert Alpaca bars to TradingView format (YYYY-MM-DD for time) const chartData = bars?.map((bar: any) => { // Handle timestamp - could be string, number, or Date let time = ""; if (bar.t) { const date = new Date(bar.t); if (!isNaN(date.getTime())) { time = date.toISOString().split('T')[0]; } } return { time, open: bar.o, high: bar.h, low: bar.l, close: bar.c, }; }).filter((bar: any) => bar.time && bar.open != null && bar.high != null && bar.low != null && bar.close != null) || []; console.log(`StockDetail: loaded ${bars?.length ?? 0} bars, transformed to ${chartData.length} chart points`); return (
{position ? `Quantity: ${position} shares` : "No position held"}
No orders found for {ticker}
) : (| Side | Qty | Status | Filled Price | Filled At |
|---|---|---|---|---|
| {order.side?.toUpperCase()} | {order.qty} | {order.status} | {order.filled_avg_price ? `$${parseFloat(order.filled_avg_price).toFixed(2)}` : "-"} | {order.filled_at ? new Date(order.filled_at).toLocaleDateString() : "-"} |