diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..ff193ea --- /dev/null +++ b/api/index.ts @@ -0,0 +1,2 @@ +export { default as marketData } from "./marketData/index.ts"; +export { default as trade } from "./trade/index.ts"; diff --git a/api/marketData/methods.ts b/api/marketData/index.ts similarity index 99% rename from api/marketData/methods.ts rename to api/marketData/index.ts index 9172b3c..3848a1e 100644 --- a/api/marketData/methods.ts +++ b/api/marketData/index.ts @@ -40,7 +40,7 @@ import { TradeResponse, } from "./types/stocks.ts"; -export const methods = ({ request }: ClientContext) => ({ +export default ({ request }: ClientContext) => ({ v1beta1: { corporateActions: (queryParams: CorporateActionsQueryParams) => request({ diff --git a/api/trade/methods.ts b/api/trade/index.ts similarity index 99% rename from api/trade/methods.ts rename to api/trade/index.ts index 3d5e3ef..432f49e 100644 --- a/api/trade/methods.ts +++ b/api/trade/index.ts @@ -44,7 +44,7 @@ import { OptionContract, OptionContractsQueryParams } from "./types/options.ts"; import { CreateOrderOptions, Order, PatchOrderOptions } from "./types/order.ts"; import { ClosePositionOptions, Position } from "./types/position.ts"; -export const methods = ({ request }: ClientContext) => ({ +export default ({ request }: ClientContext) => ({ v2: { account: { get: () => diff --git a/factory/createClient.ts b/factory/createClient.ts index 242a195..7c93591 100644 --- a/factory/createClient.ts +++ b/factory/createClient.ts @@ -1,5 +1,4 @@ -import { methods } from "../api/marketData/methods.ts"; -import { methods as methods2 } from "../api/trade/methods.ts"; +import { marketData, trade } from "../api/index.ts"; import { TradeWebSocket } from "../api/trade/types/websocket.ts"; import { StockDataWebSocket } from "../api/trade/types/websocket_2.ts"; import { CryptoWebSocket } from "../api/trade/types/websocket_3.ts"; @@ -7,99 +6,131 @@ import { NewsWebSocket } from "../api/trade/types/websocket_4.ts"; import { OptionsWebSocket } from "../api/trade/types/websocket_5.ts"; import { TokenBucketOptions, createTokenBucket } from "./createTokenBucket.ts"; -export type Client = { - rest: { - trade: ReturnType; - marketData: ReturnType; - }; - websocket: { - trade: TradeWebSocket; - marketData: { - stock: StockDataWebSocket; - crypto: CryptoWebSocket; - news: NewsWebSocket; - options: OptionsWebSocket; - }; - }; -}; - -export type ClientFactory = (context: ClientContext) => any; - +// Used to share the client options and request function between the different API methods export type ClientContext = { options: CreateClientOptions; - request: (options: RequestOptions) => Promise; + request: (options: RequestOptions) => Promise; }; +// The options required to create a client type CreateClientOptions = { keyId: string; secretKey: string; baseURL: string; - rateLimiterOptions?: TokenBucketOptions; + tokenBucket?: TokenBucketOptions; }; +// The options required to make a request type RequestOptions = { - method?: string; path: string; - params?: Record; data?: object; + method?: string; + // deno-lint-ignore no-explicit-any + params?: Record; }; -export function createClient(options: CreateClientOptions): Client { - const { rateLimiterOptions } = options; - const rateLimiter = createTokenBucket(rateLimiterOptions); +// The client object that is returned by createClient +export type Client = { + rest: { + trade: ReturnType; + marketData: ReturnType; + }; + websocket: { + trade: TradeWebSocket; + marketData: { + stock: StockDataWebSocket; + crypto: CryptoWebSocket; + news: NewsWebSocket; + options: OptionsWebSocket; + }; + }; +}; - const throttledRequest = async ({ +/** + * Creates a new client with the given options + * @param {CreateClientOptions} options - the options to create the client with + * @returns {Client} - a new client + * @example + * const client = createClient({ + * keyId: "APCA-API-KEY-ID", + * secretKey: "APCA-API-KEY-SECRET", + * baseURL: "https://paper-api.alpaca.markets", + * rateLimiterOptions: { + * capacity: 5, + * fillRate: 1, + * }, + * }); + */ +export function createClient({ + // De-structured, not needed in the context + tokenBucket, + ...options +}: CreateClientOptions): Client { + // New token bucket (defaults to rate limit in alpaca docs) + const bucket = createTokenBucket(tokenBucket); + + // New request function that waits for tokens to be available before making a request + const request = async ({ method = "GET", path, params, data, - }: RequestOptions): Promise => { + }: RequestOptions): Promise => { await new Promise((resolve) => { + // If there are enough tokens, resolve immediately + // Otherwise, wait until there are enough tokens (polling every second) const timer = setInterval(() => { - if (rateLimiter.take(1)) { + if (bucket.take(1)) { clearInterval(timer); resolve(true); } - }, 1000); // Check every second if a token is available + }, 1000); }); - let finalPath = path; + // Holds the final path with parameters replaced + let qualified = path; + if (params) { + // Replace path parameters with the actual values for (const [key, value] of Object.entries(params)) { - finalPath = finalPath.replace(`{${key}}`, encodeURIComponent(value)); + qualified = qualified.replace(`{${key}}`, encodeURIComponent(value)); } } - const url = `${options.baseURL}${finalPath}`; + const url = `${options.baseURL}${qualified}`; const headers = new Headers({ "APCA-API-KEY-ID": options.keyId, "APCA-API-SECRET-KEY": options.secretKey, "Content-Type": "application/json", }); - return fetch(url, { - method, - headers, - body: data ? JSON.stringify(data) : null, - }).then((response) => { - if (!response.ok) { - throw new Error( - `API call failed: ${response.status} ${response.statusText}` - ); - } - return response.json(); - }); - }; + return ( + fetch(url, { + method, + headers, + body: data ? JSON.stringify(data) : null, + }) + .then((response) => { + // If the response is not ok, throw an error + if (!response.ok) { + throw new Error( + `failed to fetch ${url}: ${response.status} ${response.statusText}` + ); + } - const context: ClientContext = { - options, - request: throttledRequest, + return response.json(); + }) + // Catch any other errors and log them + .catch((error) => console.error(error)) + ); }; + const context: ClientContext = { options, request }; + return { rest: { - trade: methods2(context), - marketData: methods(context), + trade: trade(context), + marketData: marketData(context), }, websocket: {} as any, }; diff --git a/factory/createTokenBucket.ts b/factory/createTokenBucket.ts index 252995b..2cea297 100644 --- a/factory/createTokenBucket.ts +++ b/factory/createTokenBucket.ts @@ -1,7 +1,7 @@ /** * Token Bucket * @see https://en.wikipedia.org/wiki/Token_bucket - * @param {Object} options + * @param {Object} options - token bucket options * @param {number} options.capacity - maximum number of tokens * @param {number} options.fillRate - tokens per second */ @@ -12,7 +12,7 @@ export type TokenBucketOptions = { /** * Create a token bucket - * @param {TokenBucketOptions} options + * @param {TokenBucketOptions} options - token bucket options * @returns {Object} token bucket * @returns {number} token bucket.tokens - current number of tokens * @returns {Function} token bucket.take - take tokens from the bucket diff --git a/factory/index.ts b/factory/index.ts new file mode 100644 index 0000000..f35f2e7 --- /dev/null +++ b/factory/index.ts @@ -0,0 +1,2 @@ +export { createClient } from "./createClient.ts"; +export { createTokenBucket } from "./createTokenBucket.ts";