@umerx/alpaca
Version:
A TypeScript Node.js library for the https://alpaca.markets REST API and WebSocket streams.
367 lines (366 loc) • 12.3 kB
JavaScript
import qs from 'qs';
import parse from './parse.js';
import isofetch from 'isomorphic-unfetch';
import endpoints from './endpoints.js';
import Bottleneck from 'bottleneck';
const unifetch = typeof fetch !== 'undefined' ? fetch : isofetch;
export class AlpacaClient {
params;
baseURLs = endpoints;
limiter = new Bottleneck({
reservoir: 200,
reservoirRefreshAmount: 200,
reservoirRefreshInterval: 60 * 1000,
// also use maxConcurrent and/or minTime for safety
maxConcurrent: 1,
minTime: 200,
});
constructor(params) {
this.params = params;
// override endpoints if custom provided
if ('endpoints' in params) {
this.baseURLs = Object.assign(endpoints, params.endpoints);
}
if (
// if not specified
!('paper' in params.credentials) &&
// and live key isn't already provided
!('key' in params.credentials && params.credentials.key.startsWith('A'))) {
params.credentials['paper'] = true;
}
if ('access_token' in params.credentials &&
('key' in params.credentials || 'secret' in params.credentials)) {
throw new Error("can't create client with both default and oauth credentials");
}
}
async isAuthenticated() {
try {
await this.getAccount();
return true;
}
catch {
return false;
}
}
async getAccount() {
return parse.account(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/account`,
}));
}
async getOrder(params) {
return parse.order(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/orders/${params.order_id || params.client_order_id}`,
data: { nested: params.nested },
}));
}
async getOrders(params = {}) {
return parse.orders(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/orders`,
data: {
...params,
symbols: params.symbols ? params.symbols.join(',') : undefined,
},
}));
}
async placeOrder(params) {
return parse.order(await this.request({
method: 'POST',
url: `${this.baseURLs.rest.account}/orders`,
data: params,
}));
}
async replaceOrder(params) {
return parse.order(await this.request({
method: 'PATCH',
url: `${this.baseURLs.rest.account}/orders/${params.order_id}`,
data: params,
}));
}
cancelOrder(params) {
return this.request({
method: 'DELETE',
url: `${this.baseURLs.rest.account}/orders/${params.order_id}`,
isJSON: false,
});
}
async cancelOrders() {
return parse.canceled_orders(await this.request({
method: 'DELETE',
url: `${this.baseURLs.rest.account}/orders`,
}));
}
async getPosition(params) {
return parse.position(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/positions/${params.symbol}`,
}));
}
async getPositions() {
return parse.positions(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/positions`,
}));
}
async closePosition(params) {
return parse.order(await this.request({
method: 'DELETE',
url: `${this.baseURLs.rest.account}/positions/${params.symbol}`,
data: params,
}));
}
async closePositions(params) {
return parse.orders(await this.request({
method: 'DELETE',
url: `${this.baseURLs.rest.account}/positions?cancel_orders=${JSON.stringify(params.cancel_orders ?? false)}`,
}));
}
getAsset(params) {
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/assets/${params.asset_id_or_symbol}`,
});
}
getAssets(params) {
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/assets`,
data: params,
});
}
getWatchlist(params) {
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
});
}
getWatchlists() {
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/watchlists`,
});
}
createWatchlist(params) {
return this.request({
method: 'POST',
url: `${this.baseURLs.rest.account}/watchlists`,
data: params,
});
}
updateWatchlist(params) {
return this.request({
method: 'PUT',
url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
data: params,
});
}
addToWatchlist(params) {
return this.request({
method: 'POST',
url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
data: params,
});
}
removeFromWatchlist(params) {
return this.request({
method: 'DELETE',
url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}/${params.symbol}`,
});
}
deleteWatchlist(params) {
return this.request({
method: 'DELETE',
url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
});
}
getCalendar(params) {
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/calendar`,
data: params,
});
}
getNews(params) {
// transform symbols if necessary
if ('symbols' in params && Array.isArray(params.symbols)) {
params.symbols = params.symbols.join(',');
}
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.beta}/news`,
data: params,
});
}
async getClock() {
return parse.clock(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/clock`,
}));
}
getAccountConfigurations() {
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/account/configurations`,
});
}
updateAccountConfigurations(params) {
return this.request({
method: 'PATCH',
url: `${this.baseURLs.rest.account}/account/configurations`,
data: params,
});
}
async getAccountActivities(params) {
if (params.activity_types && Array.isArray(params.activity_types)) {
params.activity_types = params.activity_types.join(',');
}
return parse.activities(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/account/activities${params.activity_type ? '/'.concat(params.activity_type) : ''}`,
data: { ...params, activity_type: undefined },
}));
}
getPortfolioHistory(params) {
return this.request({
method: 'GET',
url: `${this.baseURLs.rest.account}/account/portfolio/history`,
data: params,
});
}
/** @deprecated Alpaca Data API v2 is currently in public beta. */
async getBars_v1(params) {
const transformed = {
...params,
symbols: params.symbols.join(','),
};
return await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v1}/bars/${params.timeframe}`,
data: transformed,
});
}
/**
*
* @link https://docs.alpaca.markets/reference/stockbars
*/
async getBars_v2(params) {
const transformed = {
...params,
symbols: params.symbols.join(','),
};
return await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v2}/stocks/bars`,
data: transformed,
});
}
/** @deprecated Alpaca Data API v2 is currently in public beta. */
async getLastTrade_v1(params) {
return await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v1}/last/stocks/${params.symbol}`,
});
}
/** @deprecated Alpaca Data API v2 is currently in public beta. */
async getLastQuote_v1(params) {
return await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v1}/last_quote/stocks/${params.symbol}`,
});
}
async getTrades(params) {
return parse.pageOfTrades(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/trades`,
data: { ...params, symbol: undefined },
}));
}
async getQuotes(params) {
return parse.pageOfQuotes(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/quotes`,
data: { ...params, symbol: undefined },
}));
}
async getBars(params) {
return parse.pageOfBars(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/bars`,
data: { ...params, symbol: undefined },
}));
}
async getLatestTrade({ symbol, feed, limit, }) {
let query = '';
if (feed || limit) {
query = '?'.concat(qs.stringify({ feed, limit }));
}
return parse.latestTrade(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v2}/stocks/${symbol}/trades/latest`.concat(query),
}));
}
async getSnapshot(params) {
return parse.snapshot(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/snapshot`,
}));
}
async getSnapshots(params) {
return parse.snapshots(await this.request({
method: 'GET',
url: `${this.baseURLs.rest.market_data_v2}/stocks/snapshots?symbols=${params.symbols.join(',')}`,
}));
}
async request(params) {
let headers = {};
if ('access_token' in this.params.credentials) {
headers['Authorization'] = `Bearer ${this.params.credentials.access_token}`;
}
else {
headers['APCA-API-KEY-ID'] = this.params.credentials.key;
headers['APCA-API-SECRET-KEY'] = this.params.credentials.secret;
}
if (this.params.credentials.paper) {
params.url = params.url.replace('api.', 'paper-api.');
}
let query = '';
if (params.data) {
// translate dates to ISO strings
for (let [key, value] of Object.entries(params.data)) {
if (value instanceof Date) {
params.data[key] = value.toISOString();
}
}
// build query
if (!['POST', 'PATCH', 'PUT'].includes(params.method)) {
query = '?'.concat(qs.stringify(params.data));
params.data = undefined;
}
}
const makeCall = () => unifetch(params.url.concat(query), {
method: params.method,
headers,
body: JSON.stringify(params.data),
}), func = this.params.rate_limit
? () => this.limiter.schedule(makeCall)
: makeCall;
let resp, result = {};
try {
resp = await func();
if (!(params.isJSON == undefined ? true : params.isJSON)) {
return resp.ok;
}
result = await resp.json();
}
catch (e) {
console.error(e);
throw result;
}
if ('code' in result || 'message' in result) {
throw result;
}
return result;
}
}