import Alpaca from "@alpacahq/alpaca-trade-api"; const alpaca = new Alpaca({ keyId: process.env.ALPACA_API_KEY!, secretKey: process.env.ALPACA_SECRET_KEY!, baseUrl: process.env.ALPACA_BASE_URL || "https://paper-api.alpaca.markets", dataBaseUrl: process.env.ALPACA_DATA_URL || "https://data.alpaca.markets", retryOnError: false, }); 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 limit = parseInt(url.searchParams.get("limit") || "30", 10); console.log(`API quote/${ticker}: loader called with timeframe=${timeframe}, limit=${limit}`); if (!ticker) { return Response.json({ error: "Ticker is required" }, { status: 400 }); } try { // Get latest trade for current price let price = 0; try { const trade = await alpaca.getLatestTrade(ticker); price = (trade as { Price?: number }).Price || 0; console.log(`API quote/${ticker}: latest trade price = ${price}`); } catch (tradeErr) { console.error(`API quote/${ticker}: getLatestTrade failed`, tradeErr); } // Calculate start date based on timeframe const startDate = new Date(); const isIntraday = ["1Min", "5Min", "15Min", "30Min", "1H"].includes(timeframe); if (timeframe === "1D") { startDate.setDate(startDate.getDate() - Math.min(limit, 30)); } else if (timeframe === "1W") { startDate.setDate(startDate.getDate() - (limit * 7)); } else if (timeframe === "1M") { startDate.setMonth(startDate.getMonth() - limit); } else if (isIntraday) { startDate.setDate(startDate.getDate() - Math.floor(limit / 5)); } const barsOptions: any = { timeframe, limit }; if (timeframe !== "1Min" && timeframe !== "5Min") { barsOptions.start = startDate.toISOString().split('T')[0]; } console.log(`API quote/${ticker}: calling getBarsV2 with timeframe=${timeframe}, limit=${limit}`); const bars = await alpaca.getBarsV2(ticker, barsOptions); console.log(`API quote/${ticker}: getBarsV2 returned`, typeof bars, bars?.constructor?.name); // Convert async generator to array // Alpaca v2 API returns AlpacaBar with capitalized property names const barsArray = []; try { for await (const bar of bars) { console.log(`API quote/${ticker}: received bar =`, JSON.stringify(bar)); barsArray.push(bar); } } catch (genErr) { console.error(`API quote/${ticker}: error iterating bars`, genErr); } console.log(`API quote/${ticker}: raw bars count = ${barsArray.length}`); if (barsArray.length > 0) { console.log(`API quote/${ticker}: first bar =`, JSON.stringify(barsArray[0])); } else { console.log(`API quote/${ticker}: no bars returned from Alpaca, generator may be empty`); } // 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 timestamp = bar.Timestamp ?? bar.t; const volume = typeof bar.Volume === 'number' ? bar.Volume : (typeof bar.v === 'number' ? bar.v : 0); return { t: timestamp, 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); console.log(`API quote/${ticker}: returning ${transformedBars.length} bars`); return Response.json({ ticker, price, bars: transformedBars, }); } catch (error) { console.error("Alpaca data error:", error); return Response.json({ ticker, price: 0, bars: [] }, { status: 500 }); } }