import alpacaService from "../../../lib/alpacaClient"; export async function loader({ request, params }: { request: Request; params: { ticker: string } }) { const ticker = params.ticker?.toUpperCase(); const url = new URL(request.url); const timeframe = url.searchParams.get("timeframe") || "1D"; const range = url.searchParams.get("range") || "1M"; // 1D, 1W, 1M, 3M, 1Y, 3Y, ALL if (!ticker) { return Response.json({ error: "Ticker is required" }, { status: 400 }); } try { // Normalize timeframe to Alpaca API expected values function mapToAlpacaTimeframe(tf: string) { switch (tf) { case "1H": return "1Hour"; case "1D": return "1Day"; case "1W": case "1M": return "1Day"; // weekly/monthly UI ranges use daily bars default: return tf; // 1Min,5Min,15Min,30Min expected to be supported } } const alpacaTimeframe = mapToAlpacaTimeframe(timeframe); // Get latest bar for current price (uses paper by default unless mode=live) let price = 0; try { const last = await alpacaService.fetchLatestBar(ticker, alpacaTimeframe); price = last ? (last.ClosePrice ?? last.c ?? 0) : 0; } catch (tradeErr) { console.error(`API quote/${ticker}: fetchLatestBar failed`, tradeErr); } // Calculate start date based on range const startDate = new Date(); const isIntraday = ["1Min", "5Min", "15Min", "30Min", "1H"].includes(timeframe); if (range === "1D") { startDate.setDate(startDate.getDate() - 1); } else if (range === "1W") { startDate.setDate(startDate.getDate() - 7); } else if (range === "1M") { startDate.setMonth(startDate.getMonth() - 1); } else if (range === "3M") { startDate.setMonth(startDate.getMonth() - 3); } else if (range === "1Y") { startDate.setFullYear(startDate.getFullYear() - 1); } else if (range === "3Y") { startDate.setFullYear(startDate.getFullYear() - 3); } else if (range === "ALL") { startDate.setFullYear(startDate.getFullYear() - 10); // Max 10 years } else if (isIntraday) { startDate.setDate(startDate.getDate() - 30); // Default 30 days for intraday } const barsOptions: any = { limit: 1000 }; // High limit for time range // For daily/non-intraday queries pass just the date part (YYYY-MM-DD) if (!isIntraday) { barsOptions.start = startDate.toISOString().split('T')[0]; } else { // For intraday, pass full ISO start to be precise barsOptions.start = startDate.toISOString(); } const barsArray = await alpacaService.fetchBars(ticker, alpacaTimeframe, barsOptions); // Transform to chart format const transformedBars = barsArray.map((bar: any) => { // AlpacaBarV2 transforms lowercase to capitalized: o->OpenPrice, h->HighPrice, etc. const open = typeof bar.OpenPrice === 'number' ? bar.OpenPrice : (typeof bar.o === 'number' ? bar.o : 0); const high = typeof bar.HighPrice === 'number' ? bar.HighPrice : (typeof bar.h === 'number' ? bar.h : 0); const low = typeof bar.LowPrice === 'number' ? bar.LowPrice : (typeof bar.l === 'number' ? bar.l : 0); const close = typeof bar.ClosePrice === 'number' ? bar.ClosePrice : (typeof bar.c === 'number' ? bar.c : 0); const volume = typeof bar.Volume === 'number' ? bar.Volume : (typeof bar.v === 'number' ? bar.v : 0); // Normalize timestamp to ISO string so client can parse reliably const rawTs = bar.Timestamp ?? bar.t ?? bar.T ?? bar.timestamp; let dateObj: Date | null = null; if (rawTs != null) { if (typeof rawTs === 'number') { // If it's likely in seconds (< 1e12) convert to ms const asMs = rawTs > 1e12 ? rawTs : rawTs * 1000; dateObj = new Date(asMs); } else { dateObj = new Date(rawTs); } } const iso = dateObj && !isNaN(dateObj.getTime()) ? dateObj.toISOString() : null; return { t: iso, o: open, h: high, l: low, c: close, v: volume, }; }).filter((bar: any) => bar.o > 0 && bar.h > 0 && bar.l > 0 && bar.c > 0 && bar.t); return Response.json({ ticker, price, bars: transformedBars, }); } catch (error) { console.error("Alpaca data error:", error); return Response.json({ ticker, price: 0, bars: [] }, { status: 500 }); } }