Add tests for Alpaca Historical Bars API
- Implemented tests for fetching historical bars for AAPL with different timeframes (1D, 5Min, 1H). - Verified response structure and data integrity for each timeframe. - Ensured that the API returns valid data and appropriate status for the requests.
This commit is contained in:
@@ -4,43 +4,100 @@ 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({ params }: { params: { ticker: string } }) {
|
||||
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
|
||||
const trade = await alpaca.getLatestTrade(ticker);
|
||||
const price = (trade as { Price?: number }).Price || 0;
|
||||
|
||||
// Get historical bars for chart (last 30 days, daily)
|
||||
const bars = await alpaca.getBarsV2(ticker, {
|
||||
timeframe: "1Day",
|
||||
limit: 30,
|
||||
});
|
||||
|
||||
// Convert async generator to array
|
||||
const barsArray = [];
|
||||
for await (const bar of bars) {
|
||||
barsArray.push({
|
||||
t: (bar as any).Timestamp || (bar as any).t,
|
||||
o: (bar as any).Open || (bar as any).o,
|
||||
h: (bar as any).High || (bar as any).h,
|
||||
l: (bar as any).Low || (bar as any).l,
|
||||
c: (bar as any).Close || (bar as any).c,
|
||||
v: (bar as any).Volume || (bar as any).v,
|
||||
});
|
||||
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: barsArray,
|
||||
bars: transformedBars,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Alpaca data error:", error);
|
||||
|
||||
Reference in New Issue
Block a user