UNPKG

13.9 kBPlain TextView Raw
1import qs from 'qs';
2import parse from './parse.js';
3import isofetch from 'isomorphic-unfetch';
4import endpoints from './endpoints.js';
5import Bottleneck from 'bottleneck';
6
7import {
8 RawAccount,
9 Account,
10 Order,
11 Position,
12 Asset,
13 Watchlist,
14 Calendar,
15 Clock,
16 AccountConfigurations,
17 PortfolioHistory,
18 RawOrder,
19 RawPosition,
20 RawActivity,
21 Activity,
22 DefaultCredentials,
23 OAuthCredentials,
24 OrderCancelation,
25 RawOrderCancelation,
26 PageOfTrades,
27 PageOfQuotes,
28 PageOfBars,
29 Bar_v1,
30 LastQuote_v1,
31 LastTrade_v1,
32 Snapshot,
33 NewsPage,
34 LatestTrade,
35 Endpoints,
36} from './entities.js';
37
38import {
39 GetOrder,
40 GetOrders,
41 PlaceOrder,
42 ReplaceOrder,
43 CancelOrder,
44 GetPosition,
45 ClosePosition,
46 GetAsset,
47 GetAssets,
48 GetWatchList,
49 CreateWatchList,
50 UpdateWatchList,
51 AddToWatchList,
52 RemoveFromWatchList,
53 DeleteWatchList,
54 GetCalendar,
55 UpdateAccountConfigurations,
56 GetAccountActivities,
57 GetPortfolioHistory,
58 GetBars,
59 GetBars_v1,
60 GetTrades,
61 GetQuotes,
62 GetLastTrade_v1,
63 GetLastQuote_v1,
64 GetSnapshot,
65 GetSnapshots,
66 ClosePositions,
67 GetNews,
68 GetLatestTrade,
69} from './params.js';
70
71const unifetch = typeof fetch !== 'undefined' ? fetch : isofetch;
72export class AlpacaClient {
73 private baseURLs: Endpoints = endpoints;
74 private limiter = new Bottleneck({
75 reservoir: 200, // initial value
76 reservoirRefreshAmount: 200,
77 reservoirRefreshInterval: 60 * 1000, // must be divisible by 250
78 // also use maxConcurrent and/or minTime for safety
79 maxConcurrent: 1,
80 minTime: 200,
81 });
82
83 constructor(
84 public params: {
85 rate_limit?: boolean;
86 endpoints?: Endpoints | Map<keyof Endpoints, any>;
87 credentials?: DefaultCredentials | OAuthCredentials;
88 },
89 ) {
90 // override endpoints if custom provided
91 if ('endpoints' in params) {
92 this.baseURLs = Object.assign(endpoints, params.endpoints);
93 }
94
95 if (
96 // if not specified
97 !('paper' in params.credentials) &&
98 // and live key isn't already provided
99 !('key' in params.credentials && params.credentials.key.startsWith('A'))
100 ) {
101 params.credentials['paper'] = true;
102 }
103
104 if (
105 'access_token' in params.credentials &&
106 ('key' in params.credentials || 'secret' in params.credentials)
107 ) {
108 throw new Error(
109 "can't create client with both default and oauth credentials",
110 );
111 }
112 }
113
114 async isAuthenticated(): Promise<boolean> {
115 try {
116 await this.getAccount();
117 return true;
118 } catch {
119 return false;
120 }
121 }
122
123 async getAccount(): Promise<Account> {
124 return parse.account(
125 await this.request<RawAccount>({
126 method: 'GET',
127 url: `${this.baseURLs.rest.account}/account`,
128 }),
129 );
130 }
131
132 async getOrder(params: GetOrder): Promise<Order> {
133 return parse.order(
134 await this.request<RawOrder>({
135 method: 'GET',
136 url: `${this.baseURLs.rest.account}/orders/${
137 params.order_id || params.client_order_id
138 }`,
139 data: { nested: params.nested },
140 }),
141 );
142 }
143
144 async getOrders(params: GetOrders = {}): Promise<Order[]> {
145 return parse.orders(
146 await this.request<RawOrder[]>({
147 method: 'GET',
148 url: `${this.baseURLs.rest.account}/orders`,
149 data: {
150 ...params,
151 symbols: params.symbols ? params.symbols.join(',') : undefined,
152 },
153 }),
154 );
155 }
156
157 async placeOrder(params: PlaceOrder): Promise<Order> {
158 return parse.order(
159 await this.request<RawOrder>({
160 method: 'POST',
161 url: `${this.baseURLs.rest.account}/orders`,
162 data: params,
163 }),
164 );
165 }
166
167 async replaceOrder(params: ReplaceOrder): Promise<Order> {
168 return parse.order(
169 await this.request<RawOrder>({
170 method: 'PATCH',
171 url: `${this.baseURLs.rest.account}/orders/${params.order_id}`,
172 data: params,
173 }),
174 );
175 }
176
177 cancelOrder(params: CancelOrder): Promise<boolean> {
178 return this.request<boolean>({
179 method: 'DELETE',
180 url: `${this.baseURLs.rest.account}/orders/${params.order_id}`,
181 isJSON: false,
182 });
183 }
184
185 async cancelOrders(): Promise<OrderCancelation[]> {
186 return parse.canceled_orders(
187 await this.request<RawOrderCancelation[]>({
188 method: 'DELETE',
189 url: `${this.baseURLs.rest.account}/orders`,
190 }),
191 );
192 }
193
194 async getPosition(params: GetPosition): Promise<Position> {
195 return parse.position(
196 await this.request<RawPosition>({
197 method: 'GET',
198 url: `${this.baseURLs.rest.account}/positions/${params.symbol}`,
199 }),
200 );
201 }
202
203 async getPositions(): Promise<Position[]> {
204 return parse.positions(
205 await this.request<RawPosition[]>({
206 method: 'GET',
207 url: `${this.baseURLs.rest.account}/positions`,
208 }),
209 );
210 }
211
212 async closePosition(params: ClosePosition): Promise<Order> {
213 return parse.order(
214 await this.request<RawOrder>({
215 method: 'DELETE',
216 url: `${this.baseURLs.rest.account}/positions/${params.symbol}`,
217 data: params,
218 }),
219 );
220 }
221
222 async closePositions(params: ClosePositions): Promise<Order[]> {
223 return parse.orders(
224 await this.request<RawOrder[]>({
225 method: 'DELETE',
226 url: `${
227 this.baseURLs.rest.account
228 }/positions?cancel_orders=${JSON.stringify(
229 params.cancel_orders ?? false,
230 )}`,
231 }),
232 );
233 }
234
235 getAsset(params: GetAsset): Promise<Asset> {
236 return this.request({
237 method: 'GET',
238 url: `${this.baseURLs.rest.account}/assets/${params.asset_id_or_symbol}`,
239 });
240 }
241
242 getAssets(params?: GetAssets): Promise<Asset[]> {
243 return this.request({
244 method: 'GET',
245 url: `${this.baseURLs.rest.account}/assets`,
246 data: params,
247 });
248 }
249
250 getWatchlist(params: GetWatchList): Promise<Watchlist> {
251 return this.request({
252 method: 'GET',
253 url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
254 });
255 }
256
257 getWatchlists(): Promise<Watchlist[]> {
258 return this.request({
259 method: 'GET',
260 url: `${this.baseURLs.rest.account}/watchlists`,
261 });
262 }
263
264 createWatchlist(params: CreateWatchList): Promise<Watchlist[]> {
265 return this.request({
266 method: 'POST',
267 url: `${this.baseURLs.rest.account}/watchlists`,
268 data: params,
269 });
270 }
271
272 updateWatchlist(params: UpdateWatchList): Promise<Watchlist> {
273 return this.request({
274 method: 'PUT',
275 url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
276 data: params,
277 });
278 }
279
280 addToWatchlist(params: AddToWatchList): Promise<Watchlist> {
281 return this.request({
282 method: 'POST',
283 url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
284 data: params,
285 });
286 }
287
288 removeFromWatchlist(params: RemoveFromWatchList): Promise<boolean> {
289 return this.request<boolean>({
290 method: 'DELETE',
291 url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}/${params.symbol}`,
292 });
293 }
294
295 deleteWatchlist(params: DeleteWatchList): Promise<boolean> {
296 return this.request<boolean>({
297 method: 'DELETE',
298 url: `${this.baseURLs.rest.account}/watchlists/${params.uuid}`,
299 });
300 }
301
302 getCalendar(params?: GetCalendar): Promise<Calendar[]> {
303 return this.request({
304 method: 'GET',
305 url: `${this.baseURLs.rest.account}/calendar`,
306 data: params,
307 });
308 }
309
310 getNews(params?: GetNews): Promise<NewsPage> {
311 // transform symbols if necessary
312 if ('symbols' in params && Array.isArray(params.symbols)) {
313 params.symbols = params.symbols.join(',');
314 }
315
316 return this.request({
317 method: 'GET',
318 url: `${this.baseURLs.rest.beta}/news`,
319 data: params,
320 });
321 }
322
323 async getClock(): Promise<Clock> {
324 return parse.clock(
325 await this.request({
326 method: 'GET',
327 url: `${this.baseURLs.rest.account}/clock`,
328 }),
329 );
330 }
331
332 getAccountConfigurations(): Promise<AccountConfigurations> {
333 return this.request({
334 method: 'GET',
335 url: `${this.baseURLs.rest.account}/account/configurations`,
336 });
337 }
338
339 updateAccountConfigurations(
340 params: UpdateAccountConfigurations,
341 ): Promise<AccountConfigurations> {
342 return this.request({
343 method: 'PATCH',
344 url: `${this.baseURLs.rest.account}/account/configurations`,
345 data: params,
346 });
347 }
348
349 async getAccountActivities(
350 params: GetAccountActivities,
351 ): Promise<Activity[]> {
352 if (params.activity_types && Array.isArray(params.activity_types)) {
353 params.activity_types = params.activity_types.join(',');
354 }
355
356 return parse.activities(
357 await this.request<RawActivity[]>({
358 method: 'GET',
359 url: `${this.baseURLs.rest.account}/account/activities${
360 params.activity_type ? '/'.concat(params.activity_type) : ''
361 }`,
362 data: { ...params, activity_type: undefined },
363 }),
364 );
365 }
366
367 getPortfolioHistory(params?: GetPortfolioHistory): Promise<PortfolioHistory> {
368 return this.request({
369 method: 'GET',
370 url: `${this.baseURLs.rest.account}/account/portfolio/history`,
371 data: params,
372 });
373 }
374
375 /** @deprecated Alpaca Data API v2 is currently in public beta. */
376 async getBars_v1(
377 params: GetBars_v1,
378 ): Promise<{ [symbol: string]: Bar_v1[] }> {
379 const transformed: Omit<GetBars_v1, 'symbols'> & { symbols: string } = {
380 ...params,
381 symbols: params.symbols.join(','),
382 };
383
384 return await this.request({
385 method: 'GET',
386 url: `${this.baseURLs.rest.market_data_v1}/bars/${params.timeframe}`,
387 data: transformed,
388 });
389 }
390
391 /** @deprecated Alpaca Data API v2 is currently in public beta. */
392 async getLastTrade_v1(params: GetLastTrade_v1): Promise<LastTrade_v1> {
393 return await this.request({
394 method: 'GET',
395 url: `${this.baseURLs.rest.market_data_v1}/last/stocks/${params.symbol}`,
396 });
397 }
398
399 /** @deprecated Alpaca Data API v2 is currently in public beta. */
400 async getLastQuote_v1(params: GetLastQuote_v1): Promise<LastQuote_v1> {
401 return await this.request({
402 method: 'GET',
403 url: `${this.baseURLs.rest.market_data_v1}/last_quote/stocks/${params.symbol}`,
404 });
405 }
406
407 async getTrades(params: GetTrades): Promise<PageOfTrades> {
408 return parse.pageOfTrades(
409 await this.request({
410 method: 'GET',
411 url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/trades`,
412 data: { ...params, symbol: undefined },
413 }),
414 );
415 }
416
417 async getQuotes(params: GetQuotes): Promise<PageOfQuotes> {
418 return parse.pageOfQuotes(
419 await this.request({
420 method: 'GET',
421 url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/quotes`,
422 data: { ...params, symbol: undefined },
423 }),
424 );
425 }
426
427 async getBars(params: GetBars): Promise<PageOfBars> {
428 return parse.pageOfBars(
429 await this.request({
430 method: 'GET',
431 url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/bars`,
432 data: { ...params, symbol: undefined },
433 }),
434 );
435 }
436
437 async getLatestTrade({
438 symbol,
439 feed,
440 limit,
441 }: GetLatestTrade): Promise<LatestTrade> {
442 let query = '';
443
444 if (feed || limit) {
445 query = '?'.concat(qs.stringify({ feed, limit }));
446 }
447
448 return parse.latestTrade(
449 await this.request({
450 method: 'GET',
451 url: `${this.baseURLs.rest.market_data_v2}/stocks/${symbol}/trades/latest`.concat(
452 query,
453 ),
454 }),
455 );
456 }
457
458 async getSnapshot(params: GetSnapshot): Promise<Snapshot> {
459 return parse.snapshot(
460 await this.request({
461 method: 'GET',
462 url: `${this.baseURLs.rest.market_data_v2}/stocks/${params.symbol}/snapshot`,
463 }),
464 );
465 }
466
467 async getSnapshots(
468 params: GetSnapshots,
469 ): Promise<{ [key: string]: Snapshot }> {
470 return parse.snapshots(
471 await this.request({
472 method: 'GET',
473 url: `${
474 this.baseURLs.rest.market_data_v2
475 }/stocks/snapshots?symbols=${params.symbols.join(',')}`,
476 }),
477 );
478 }
479
480 private async request<T = any>(params: {
481 method: 'GET' | 'DELETE' | 'PUT' | 'PATCH' | 'POST';
482 url: string;
483 data?: { [key: string]: any };
484 isJSON?: boolean;
485 }): Promise<T> {
486 let headers: any = {};
487
488 if ('access_token' in this.params.credentials) {
489 headers[
490 'Authorization'
491 ] = `Bearer ${this.params.credentials.access_token}`;
492 } else {
493 headers['APCA-API-KEY-ID'] = this.params.credentials.key;
494 headers['APCA-API-SECRET-KEY'] = this.params.credentials.secret;
495 }
496
497 if (this.params.credentials.paper) {
498 params.url = params.url.replace('api.', 'paper-api.');
499 }
500
501 let query = '';
502
503 if (params.data) {
504 // translate dates to ISO strings
505 for (let [key, value] of Object.entries(params.data)) {
506 if (value instanceof Date) {
507 params.data[key] = (value as Date).toISOString();
508 }
509 }
510
511 // build query
512 if (!['POST', 'PATCH', 'PUT'].includes(params.method)) {
513 query = '?'.concat(qs.stringify(params.data));
514 params.data = undefined;
515 }
516 }
517
518 const makeCall = () =>
519 unifetch(params.url.concat(query), {
520 method: params.method,
521 headers,
522 body: JSON.stringify(params.data),
523 }),
524 func = this.params.rate_limit
525 ? () => this.limiter.schedule(makeCall)
526 : makeCall;
527
528 let resp,
529 result = {};
530
531 try {
532 resp = await func();
533
534 if (!(params.isJSON == undefined ? true : params.isJSON)) {
535 return resp.ok as any;
536 }
537
538 result = await resp.json();
539 } catch (e) {
540 console.error(e);
541 throw result;
542 }
543
544 if ('code' in result || 'message' in result) {
545 throw result;
546 }
547
548 return result as any;
549 }
550}