Skip to content

Commit

Permalink
Refactor client creation and request function
Browse files Browse the repository at this point in the history
  • Loading branch information
117 committed Mar 24, 2024
1 parent 6ed194b commit 8d5aede
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 56 deletions.
2 changes: 2 additions & 0 deletions api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as marketData } from "./marketData/index.ts";
export { default as trade } from "./trade/index.ts";
2 changes: 1 addition & 1 deletion api/marketData/methods.ts → api/marketData/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
TradeResponse,
} from "./types/stocks.ts";

export const methods = ({ request }: ClientContext) => ({
export default ({ request }: ClientContext) => ({
v1beta1: {
corporateActions: (queryParams: CorporateActionsQueryParams) =>
request<CorporateActionsResponse>({
Expand Down
2 changes: 1 addition & 1 deletion api/trade/methods.ts → api/trade/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down
135 changes: 83 additions & 52 deletions factory/createClient.ts
Original file line number Diff line number Diff line change
@@ -1,105 +1,136 @@
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";
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<typeof methods2>;
marketData: ReturnType<typeof methods>;
};
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: <T>(options: RequestOptions) => Promise<unknown>;
request: <T>(options: RequestOptions) => Promise<T>;
};

// 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<string, any>;
data?: object;
method?: string;
// deno-lint-ignore no-explicit-any
params?: Record<string, any>;
};

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<typeof trade>;
marketData: ReturnType<typeof marketData>;
};
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 <T>({
method = "GET",
path,
params,
data,
}: RequestOptions): Promise<any> => {
}: RequestOptions): Promise<T> => {
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,
};
Expand Down
4 changes: 2 additions & 2 deletions factory/createTokenBucket.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions factory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { createClient } from "./createClient.ts";
export { createTokenBucket } from "./createTokenBucket.ts";

0 comments on commit 8d5aede

Please sign in to comment.