Feat(api): support fetching bars from paper or live Alpaca (default paper) via alpacaClient.fetchBars\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
import Alpaca from "@alpacahq/alpaca-trade-api";
|
||||||
|
|
||||||
|
function makeAlpaca(mode: 'paper' | 'live' = 'paper') {
|
||||||
|
const isLive = mode === 'live';
|
||||||
|
const keyId = isLive ? (process.env.ALPACA_API_KEY_LIVE || process.env.ALPACA_API_KEY) : process.env.ALPACA_API_KEY;
|
||||||
|
const secretKey = isLive ? (process.env.ALPACA_SECRET_KEY_LIVE || process.env.ALPACA_SECRET_KEY) : process.env.ALPACA_SECRET_KEY;
|
||||||
|
const baseUrl = isLive ? (process.env.ALPACA_LIVE_BASE_URL || "https://api.alpaca.markets") : (process.env.ALPACA_BASE_URL || "https://paper-api.alpaca.markets");
|
||||||
|
const dataBaseUrl = isLive ? (process.env.ALPACA_LIVE_DATA_URL || "https://data.alpaca.markets") : (process.env.ALPACA_DATA_URL || "https://data.alpaca.markets");
|
||||||
|
return new Alpaca({
|
||||||
|
keyId,
|
||||||
|
secretKey,
|
||||||
|
baseUrl,
|
||||||
|
dataBaseUrl,
|
||||||
|
retryOnError: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccount(mode: 'paper' | 'live' = 'paper') {
|
||||||
|
try {
|
||||||
|
const client = makeAlpaca(mode);
|
||||||
|
const account = await client.getAccount();
|
||||||
|
return {
|
||||||
|
cash: parseFloat(account.cash),
|
||||||
|
buying_power: parseFloat(account.buying_power),
|
||||||
|
portfolio_value: parseFloat(account.portfolio_value),
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("alpacaClient: fetchAccount failed:", err);
|
||||||
|
throw new Error(err?.message || String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRecentCloses(ticker: string, days = 30, mode: 'paper' | 'live' = 'paper') {
|
||||||
|
try {
|
||||||
|
const client = makeAlpaca(mode);
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - days);
|
||||||
|
const barsIter = await client.getBarsV2(ticker, { timeframe: "1D", start: startDate.toISOString().split("T")[0], limit: 1000 });
|
||||||
|
const barsArray: any[] = [];
|
||||||
|
for await (const b of barsIter) barsArray.push(b);
|
||||||
|
const closes = barsArray.map((bar: any) => (typeof bar.ClosePrice === 'number' ? bar.ClosePrice : (typeof bar.c === 'number' ? bar.c : 0))).filter((c: number) => c > 0);
|
||||||
|
if (closes.length) return closes;
|
||||||
|
|
||||||
|
// fallback to latest trade
|
||||||
|
try {
|
||||||
|
const trade: any = await client.getLatestTrade(ticker);
|
||||||
|
const price = trade?.Price || trade?.price || 0;
|
||||||
|
if (price) return [price];
|
||||||
|
} catch (tErr) {
|
||||||
|
console.warn("alpacaClient: getLatestTrade fallback failed:", tErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("No recent price data available from Alpaca");
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("alpacaClient: fetchRecentCloses failed:", err);
|
||||||
|
throw new Error(err?.message || String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLatestBar(ticker: string, timeframe = '1Min', mode: 'paper' | 'live' = 'paper') {
|
||||||
|
try {
|
||||||
|
const client = makeAlpaca(mode);
|
||||||
|
const barsIter = await client.getBarsV2(ticker, { timeframe, limit: 1 });
|
||||||
|
const barsArr: any[] = [];
|
||||||
|
for await (const b of barsIter) barsArr.push(b);
|
||||||
|
const last = barsArr[barsArr.length - 1];
|
||||||
|
return last || null;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('alpacaClient: fetchLatestBar failed:', err);
|
||||||
|
throw new Error(err?.message || String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBars(ticker: string, timeframe = '1D', options: any = {}, mode: 'paper' | 'live' = 'paper') {
|
||||||
|
try {
|
||||||
|
const client = makeAlpaca(mode);
|
||||||
|
const barsIter = await client.getBarsV2(ticker, { timeframe, ...options });
|
||||||
|
const barsArr: any[] = [];
|
||||||
|
for await (const b of barsIter) barsArr.push(b);
|
||||||
|
return barsArr;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('alpacaClient: fetchBars failed:', err);
|
||||||
|
throw new Error(err?.message || String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,24 @@
|
|||||||
import Alpaca from "@alpacahq/alpaca-trade-api";
|
import { fetchBars, fetchLatestBar } from "../../../lib/alpacaClient";
|
||||||
|
|
||||||
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 } }) {
|
export async function loader({ request, params }: { request: Request; params: { ticker: string } }) {
|
||||||
const ticker = params.ticker?.toUpperCase();
|
const ticker = params.ticker?.toUpperCase();
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const timeframe = url.searchParams.get("timeframe") || "1D";
|
const timeframe = url.searchParams.get("timeframe") || "1D";
|
||||||
const range = url.searchParams.get("range") || "1M"; // 1D, 1W, 1M, 3M, 1Y, 3Y, ALL
|
const range = url.searchParams.get("range") || "1M"; // 1D, 1W, 1M, 3M, 1Y, 3Y, ALL
|
||||||
|
const mode = url.searchParams.get('mode') === 'live' ? 'live' : 'paper';
|
||||||
|
|
||||||
if (!ticker) {
|
if (!ticker) {
|
||||||
return Response.json({ error: "Ticker is required" }, { status: 400 });
|
return Response.json({ error: "Ticker is required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get latest trade for current price
|
// Get latest bar for current price (uses paper by default unless mode=live)
|
||||||
let price = 0;
|
let price = 0;
|
||||||
try {
|
try {
|
||||||
const trade = await alpaca.getLatestTrade(ticker);
|
const last = await fetchLatestBar(ticker, timeframe, mode as any);
|
||||||
price = (trade as { Price?: number }).Price || 0;
|
price = last ? (last.ClosePrice ?? last.c ?? 0) : 0;
|
||||||
} catch (tradeErr) {
|
} catch (tradeErr) {
|
||||||
console.error(`API quote/${ticker}: getLatestTrade failed`, tradeErr);
|
console.error(`API quote/${ticker}: fetchLatestBar failed`, tradeErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate start date based on range
|
// Calculate start date based on range
|
||||||
@@ -50,25 +43,14 @@ export async function loader({ request, params }: { request: Request; params: {
|
|||||||
startDate.setDate(startDate.getDate() - 30); // Default 30 days for intraday
|
startDate.setDate(startDate.getDate() - 30); // Default 30 days for intraday
|
||||||
}
|
}
|
||||||
|
|
||||||
const barsOptions: any = { timeframe, limit: 1000 }; // High limit for time range
|
const barsOptions: any = { limit: 1000 }; // High limit for time range
|
||||||
if (!isIntraday && range !== "ALL") {
|
if (!isIntraday && range !== "ALL") {
|
||||||
barsOptions.start = startDate.toISOString().split('T')[0];
|
barsOptions.start = startDate.toISOString().split('T')[0];
|
||||||
} else if (!isIntraday) {
|
} else if (!isIntraday) {
|
||||||
barsOptions.start = startDate.toISOString().split('T')[0];
|
barsOptions.start = startDate.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const bars = await alpaca.getBarsV2(ticker, barsOptions);
|
const barsArray = await fetchBars(ticker, timeframe, barsOptions, mode as any);
|
||||||
|
|
||||||
// Convert async generator to array
|
|
||||||
// Alpaca v2 API returns AlpacaBar with capitalized property names
|
|
||||||
const barsArray = [];
|
|
||||||
try {
|
|
||||||
for await (const bar of bars) {
|
|
||||||
barsArray.push(bar);
|
|
||||||
}
|
|
||||||
} catch (genErr) {
|
|
||||||
console.error(`API quote/${ticker}: error iterating bars`, genErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform to chart format
|
// Transform to chart format
|
||||||
const transformedBars = barsArray.map((bar: any) => {
|
const transformedBars = barsArray.map((bar: any) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user