import Alpaca from "@alpacahq/alpaca-trade-api"; type Mode = 'paper' | 'live'; function makeAlpaca(mode: Mode = '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, }); } class AlpacaService { private mode: Mode; private client: any; private lastBarCache = new Map(); constructor(mode: Mode = 'paper') { this.mode = mode; this.client = makeAlpaca(mode); } setMode(mode: Mode) { if (this.mode !== mode) { this.mode = mode; this.client = makeAlpaca(mode); } } getMode() { return this.mode; } async fetchAccount() { try { const account = await this.client.getAccount(); return { cash: parseFloat(account.cash), buying_power: parseFloat(account.buying_power), portfolio_value: parseFloat(account.portfolio_value), }; } catch (err: any) { console.error("AlpacaService: fetchAccount failed:", err); throw new Error(err?.message || String(err)); } } async fetchRecentCloses(ticker: string, days = 30) { try { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const barsIter = await this.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 this.client.getLatestTrade(ticker); const price = trade?.Price || trade?.price || 0; if (price) return [price]; } catch (tErr) { console.warn("AlpacaService: getLatestTrade fallback failed:", tErr); } throw new Error("No recent price data available from Alpaca"); } catch (err: any) { console.error("AlpacaService: fetchRecentCloses failed:", err); throw new Error(err?.message || String(err)); } } async fetchLatestBar(ticker: string, timeframe = '1Min') { const cacheKey = `${ticker}:${timeframe}`; const maxRetries = 3; let attempt = 0; let baseDelay = 500; // ms try { while (attempt < maxRetries) { try { const barsIter = await this.client.getBarsV2(ticker, { timeframe, limit: 1 }); const barsArr: any[] = []; for await (const b of barsIter) barsArr.push(b); const last = barsArr[barsArr.length - 1] || null; if (last) { this.lastBarCache.set(cacheKey, { bar: last, ts: Date.now() }); } return last || (this.lastBarCache.get(cacheKey)?.bar ?? null); } catch (err: any) { const msg = err?.message ?? String(err); // Rate limit -> retry with exponential backoff if (/429|too many requests/i.test(msg)) { attempt++; const backoff = Math.min(baseDelay * Math.pow(2, attempt - 1), 10000); console.warn(`AlpacaService.fetchLatestBar rate limited, retry ${attempt}/${maxRetries} after ${backoff}ms`); await new Promise((r) => setTimeout(r, backoff)); continue; } // non-rate-limit error -> rethrow console.error('AlpacaService: fetchLatestBar failed:', err); throw new Error(err?.message || String(err)); } } // exhausted retries, fall back to cache if available const cached = this.lastBarCache.get(cacheKey); if (cached) { console.warn('AlpacaService.fetchLatestBar: returning cached bar after retries'); return cached.bar; } return null; } catch (err: any) { console.error('AlpacaService: fetchLatestBar final error:', err); throw new Error(err?.message || String(err)); } } async fetchBars(ticker: string, timeframe = '1D', options: any = {}) { const maxRetries = 3; let attempt = 0; let baseDelay = 500; try { while (attempt < maxRetries) { try { const barsIter = await this.client.getBarsV2(ticker, { timeframe, ...options }); const barsArr: any[] = []; for await (const b of barsIter) barsArr.push(b); // update last-bar cache for this ticker/timeframe if (barsArr.length) { const cacheKey = `${ticker}:${timeframe}`; this.lastBarCache.set(cacheKey, { bar: barsArr[barsArr.length - 1], ts: Date.now() }); } return barsArr; } catch (err: any) { const msg = err?.message ?? String(err); if (/429|too many requests/i.test(msg)) { attempt++; const backoff = Math.min(baseDelay * Math.pow(2, attempt - 1), 10000); console.warn(`AlpacaService.fetchBars rate limited, retry ${attempt}/${maxRetries} after ${backoff}ms`); await new Promise((r) => setTimeout(r, backoff)); continue; } console.error('AlpacaService: fetchBars failed:', err); throw new Error(err?.message || String(err)); } } console.warn('AlpacaService.fetchBars: exhausted retries, returning empty array'); return []; } catch (err: any) { console.error('AlpacaService: fetchBars final error:', err); throw new Error(err?.message || String(err)); } } } // Singleton configured to use paper trading API by default export const alpacaService = new AlpacaService('paper'); // Backwards-compatible named exports (delegate to singleton) export const fetchAccount = (_mode?: Mode) => alpacaService.fetchAccount(); export const fetchRecentCloses = (ticker: string, days = 30, _mode?: Mode) => alpacaService.fetchRecentCloses(ticker, days); export const fetchLatestBar = (ticker: string, timeframe = '1Min', _mode?: Mode) => alpacaService.fetchLatestBar(ticker, timeframe); export const fetchBars = (ticker: string, timeframe = '1D', options: any = {}, _mode?: Mode) => alpacaService.fetchBars(ticker, timeframe, options); export default alpacaService;