diff --git a/app/lib/alpacaClient.ts b/app/lib/alpacaClient.ts new file mode 100644 index 0000000..0cbfcb0 --- /dev/null +++ b/app/lib/alpacaClient.ts @@ -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)); + } +} \ No newline at end of file diff --git a/app/routes/api/alpaca/quote.ts b/app/routes/api/alpaca/quote.ts index 2759382..3c2fa31 100644 --- a/app/routes/api/alpaca/quote.ts +++ b/app/routes/api/alpaca/quote.ts @@ -1,31 +1,24 @@ -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, -}); +import { fetchBars, fetchLatestBar } 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 + const mode = url.searchParams.get('mode') === 'live' ? 'live' : 'paper'; if (!ticker) { return Response.json({ error: "Ticker is required" }, { status: 400 }); } try { - // Get latest trade for current price + // Get latest bar for current price (uses paper by default unless mode=live) let price = 0; try { - const trade = await alpaca.getLatestTrade(ticker); - price = (trade as { Price?: number }).Price || 0; + const last = await fetchLatestBar(ticker, timeframe, mode as any); + price = last ? (last.ClosePrice ?? last.c ?? 0) : 0; } catch (tradeErr) { - console.error(`API quote/${ticker}: getLatestTrade failed`, tradeErr); + console.error(`API quote/${ticker}: fetchLatestBar failed`, tradeErr); } // 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 } - 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") { barsOptions.start = startDate.toISOString().split('T')[0]; } else if (!isIntraday) { barsOptions.start = startDate.toISOString().split('T')[0]; } - const bars = await alpaca.getBarsV2(ticker, barsOptions); - - // 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); - } + const barsArray = await fetchBars(ticker, timeframe, barsOptions, mode as any); // Transform to chart format const transformedBars = barsArray.map((bar: any) => {