177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
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<string, { bar: any; ts: number }>();
|
|
|
|
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; |