diff --git a/package.json b/package.json index 66c0261..04dad96 100644 --- a/package.json +++ b/package.json @@ -6,27 +6,7 @@ "types": "dist/web.d.ts", "type": "module", "exports": { - "./wsJsonServer": "./dist/server/wsJsonServer.js", - "./wsJsonClient": "./dist/client/wsJsonClient.js", - "./realWsJsonClient": "./dist/client/realWsJsonClient.js", - "./mockWsJsonClient": "./dist/client/mockWsJsonClient.js", - "./alertTypes": "./dist/client/types/alertTypes.js", - "./tdaWsJsonTypes": "./dist/client/tdaWsJsonTypes.js", - "./wsJsonClientAuth": "./dist/client/wsJsonClientAuth.js", - "./wsJsonClientProxy": "./dist/client/wsJsonClientProxy.js", - "./messageTypeHelpers": "./dist/client/messageTypeHelpers.js", - "./chartMessageHandler": "./dist/client/services/chartMessageHandler.js", - "./quotesMessageHandler": "./dist/client/services/quotesMessageHandler.js", - "./positionsMessageHandler": "./dist/client/services/positionsMessageHandler.js", - "./placeOrderMessageHandler": "./dist/client/services/placeOrderMessageHandler.js", - "./createAlertMessageHandler": "./dist/client/services/createAlertMessageHandler.js", - "./orderEventsMessageHandler": "./dist/client/services/orderEventsMessageHandler.js", - "./optionSeriesMessageHandler": "./dist/client/services/optionSeriesMessageHandler.js", - "./optionQuotesMessageHandler": "./dist/client/services/optionQuotesMessageHandler.js", - "./userPropertiesMessageHandler": "./dist/client/services/userPropertiesMessageHandler.js", - "./instrumentSearchMessageHandler": "./dist/client/services/instrumentSearchMessageHandler.js", - "./optionSeriesQuotesMessageHandler": "./dist/client/services/optionSeriesQuotesMessageHandler.js", - "./optionChainDetailsMessageHandler": "./dist/client/services/optionChainDetailsMessageHandler.js" + ".": "./dist/index.js" }, "scripts": { "prepublish": "tsc -p tsconfig.json", diff --git a/src/client/realWsJsonClient.ts b/src/client/realWsJsonClient.ts index 61b50b6..4ec74a3 100644 --- a/src/client/realWsJsonClient.ts +++ b/src/client/realWsJsonClient.ts @@ -1,6 +1,5 @@ import debug from "debug"; import WebSocket from "isomorphic-ws"; -import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { BufferedIterator, deferredWrap, @@ -103,7 +102,7 @@ const messageHandlers: WebSocketApiMessageHandler[] = [ new GetWatchlistMessageHandler(), ]; -export default class RealWsJsonClient implements WsJsonClient { +export class RealWsJsonClient implements WsJsonClient { private readonly genericHandler = new GenericIncomingMessageHandler(); private buffer = new BufferedIterator(); private iterator = new MulticastIterator(this.buffer); @@ -133,6 +132,14 @@ export default class RealWsJsonClient implements WsJsonClient { private readonly responseParser = new ResponseParser(this.genericHandler) ) {} + get accessToken() { + return this.credentials.accessToken; + } + + get refreshToken() { + return this.credentials.refreshToken; + } + async authenticateWithAccessToken({ accessToken, refreshToken, @@ -369,37 +376,6 @@ export default class RealWsJsonClient implements WsJsonClient { this.socket?.send(msg); } - private updateDotEnvCredentials() { - const { - credentials: { accessToken, refreshToken }, - } = this; - const suffix = process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ""; - const envPath = `.env${suffix}`; - let envContent = ""; - if (existsSync(envPath)) { - envContent = readFileSync(envPath, "utf-8"); - // Remove any existing token lines - envContent = envContent - .split("\n") - .filter( - (line) => - !line.startsWith("TOS_ACCESS_TOKEN=") && - !line.startsWith("TOS_REFRESH_TOKEN=") - ) - .join("\n"); - } - - // Append the new token values - const tokenLines = [ - `TOS_ACCESS_TOKEN=${accessToken}`, - `TOS_REFRESH_TOKEN=${refreshToken}`, - ].join("\n"); - - // Ensure there's a newline between existing content and new tokens - const newContent = envContent.trim() + "\n" + tokenLines + "\n"; - writeFileSync(envPath, newContent); - } - private handleSchwabLoginResponse( message: RawLoginResponse, resolve: (value: RawLoginResponseBody) => void, @@ -418,7 +394,6 @@ export default class RealWsJsonClient implements WsJsonClient { if (loginResponse.refreshToken) { this.credentials.refreshToken = loginResponse.refreshToken; } - this.updateDotEnvCredentials(); resolve(body); } else { this.state = ChannelState.ERROR; @@ -437,7 +412,6 @@ export default class RealWsJsonClient implements WsJsonClient { const [{ body }] = message.payload; if (loginResponse.successful) { this.state = ChannelState.CONNECTED; - this.updateDotEnvCredentials(); resolve(body); } else { this.state = ChannelState.ERROR; diff --git a/src/client/services/chartMessageHandler.ts b/src/client/services/chartMessageHandler.ts index 93c1237..19816bf 100644 --- a/src/client/services/chartMessageHandler.ts +++ b/src/client/services/chartMessageHandler.ts @@ -4,44 +4,20 @@ import WebSocketApiMessageHandler, { newPayload, } from "./webSocketApiMessageHandler.js"; -type RawPayloadResponseChartData = { - candles: { - closes: number[]; - highs: number[]; - lows: number[]; - opens: number[]; - timestamps: number[]; - volumes: number[]; - }; - symbol: string; +export type ChartCandles = { + closes: number[]; + highs: number[]; + lows: number[]; + opens: number[]; + timestamps: number[]; + volumes: number[]; }; -export type RawPayloadResponseChart = - | RawPayloadResponseChartData - | { - patches: { - op: string; - path: string; - value: RawPayloadResponseChartData; - }[]; - }; - -export type ChartResponse = { - candles: PriceItem[]; +export type RawPayloadResponseChart = { + candles: ChartCandles; symbol: string; - service: "chart"; }; -export type OHLC = { - open: number; - high: number; - low: number; - close: number; - volume: number; -}; - -export type PriceItem = { date: Date } & OHLC; - export type ChartRequestParams = { symbol: string; timeAggregation: string; diff --git a/src/client/services/instrumentSearchMessageHandler.ts b/src/client/services/instrumentSearchMessageHandler.ts index 36a0b16..d437202 100644 --- a/src/client/services/instrumentSearchMessageHandler.ts +++ b/src/client/services/instrumentSearchMessageHandler.ts @@ -4,15 +4,12 @@ import { ApiService } from "./apiService.js"; import WebSocketApiMessageHandler from "./webSocketApiMessageHandler.js"; export type RawPayloadResponseInstrumentSearch = { - instruments: { - symbol: string; - displaySymbol: string; - description: string; - }[]; + instruments: InstrumentSearchMatch[]; }; export type InstrumentSearchMatch = { symbol: string; + displaySymbol: string; description: string; }; @@ -21,11 +18,6 @@ type InstrumentSearchRequest = { limit?: number; }; -export type InstrumentSearchResponse = { - instruments: InstrumentSearchMatch[]; - service: "instrument_search"; -}; - export default class InstrumentSearchMessageHandler implements WebSocketApiMessageHandler { diff --git a/src/client/services/optionChainDetailsMessageHandler.ts b/src/client/services/optionChainDetailsMessageHandler.ts index 6412d5a..be2ed69 100644 --- a/src/client/services/optionChainDetailsMessageHandler.ts +++ b/src/client/services/optionChainDetailsMessageHandler.ts @@ -6,11 +6,6 @@ export type RawOptionChainDetailsResponse = { optionSeries: OptionChainDetailsItem[]; }; -export type OptionChainDetailsResponse = { - seriesDetails: OptionChainDetailsItem[]; - service: "option_chain/get"; -}; - export type OptionChainDetailsItem = { expiration: string; // eg "16 JUN 23" expirationString: string; // eg "16 JUN 23 (100)" diff --git a/src/client/services/optionQuotesMessageHandler.ts b/src/client/services/optionQuotesMessageHandler.ts index 79baa81..738ae29 100644 --- a/src/client/services/optionQuotesMessageHandler.ts +++ b/src/client/services/optionQuotesMessageHandler.ts @@ -4,11 +4,9 @@ import WebSocketApiMessageHandler, { newPayload, } from "./webSocketApiMessageHandler.js"; -export type OptionQuotesRequestParams = { - underlyingSymbol: string; - seriesNames: string[]; - minStrike: number; - maxStrike: number; +export type RawOptionQuotesSnapshotBodyResponse = { + exchanges: string[]; + items: OptionQuoteItem[]; }; export type OptionQuoteItem = { @@ -16,29 +14,6 @@ export type OptionQuoteItem = { values: OptionQuoteItemValue; }; -export type OptionQuotesResponse = - | OptionQuotesSnapshotResponse - | OptionQuotesPatchResponse; - -export type OptionQuotesSnapshotResponse = { - items: OptionQuoteItem[]; - service: "quotes/options"; -}; - -export type OptionQuotesPatchResponse = { - patches: { - op: string; - path: string; - value: - | { - exchanges: string[]; - items: OptionQuoteItem[]; - } - | number; - }[]; - service: "quotes/options"; -}; - export type OptionQuoteItemValue = { ASK?: number; BID?: number; @@ -48,6 +23,13 @@ export type OptionQuoteItemValue = { VOLUME?: number; }; +export type OptionQuotesRequestParams = { + underlyingSymbol: string; + seriesNames: string[]; + minStrike: number; + maxStrike: number; +}; + export default class OptionQuotesMessageHandler implements WebSocketApiMessageHandler { diff --git a/src/client/services/optionSeriesMessageHandler.ts b/src/client/services/optionSeriesMessageHandler.ts index faf4fe4..582fc72 100644 --- a/src/client/services/optionSeriesMessageHandler.ts +++ b/src/client/services/optionSeriesMessageHandler.ts @@ -3,37 +3,24 @@ import { newRandomId } from "../util.js"; import { ApiService } from "./apiService.js"; import WebSocketApiMessageHandler from "./webSocketApiMessageHandler.js"; -export type RawOptionSeriesResponse = { - series: { - // symbol - underlying: string; - // "19 JAN 24 100" - name: string; - spc: number; - multiplier: number; - // eg "REGULAR" - expirationStyle: string; - isEuropean: boolean; - // eg "2024-01-20T12:00:00Z" - expiration: string; - lastTradeDate: string; - settlementType: string; // likely AM or PM - }[]; -}; - -export type OptionChainResponse = { - series: OptionChainItem[]; - service: "optionSeries"; -}; - -export type OptionChainItem = { +export type OptionSeriesItem = { + // symbol underlying: string; + // "19 JAN 24 100" name: string; + spc: number; multiplier: number; + // eg "REGULAR" + expirationStyle: string; isEuropean: boolean; - lastTradeDate: Date; - expiration: Date; - settlementType: string; + // eg "2024-01-20T12:00:00Z" + expiration: string; + lastTradeDate: string; + settlementType: string; // likely AM or PM +}; + +export type RawOptionSeriesResponse = { + series: OptionSeriesItem[]; }; export default class OptionSeriesMessageHandler diff --git a/src/client/services/optionSeriesQuotesMessageHandler.ts b/src/client/services/optionSeriesQuotesMessageHandler.ts index 7972d9f..38dfae0 100644 --- a/src/client/services/optionSeriesQuotesMessageHandler.ts +++ b/src/client/services/optionSeriesQuotesMessageHandler.ts @@ -12,24 +12,10 @@ export type OptionSeriesQuote = { }; }; -export type OptionSeriesQuotesPatchResponse = { - patches: { - op: string; - path: string; - value: number | { series: OptionSeriesQuote[] }; - }[]; - service: "optionSeries/quotes"; -}; - export type OptionSeriesQuotesSnapshotResponse = { series: OptionSeriesQuote[]; - service: "optionSeries/quotes"; }; -export type OptionSeriesQuotesResponse = - | OptionSeriesQuotesSnapshotResponse - | OptionSeriesQuotesPatchResponse; - export default class OptionSeriesQuotesMessageHandler implements WebSocketApiMessageHandler { diff --git a/src/client/services/positionsMessageHandler.ts b/src/client/services/positionsMessageHandler.ts index 0d9ddc1..baac55e 100644 --- a/src/client/services/positionsMessageHandler.ts +++ b/src/client/services/positionsMessageHandler.ts @@ -1,5 +1,4 @@ import { RawPayloadRequest } from "../tdaWsJsonTypes.js"; -import { DeepPartial } from "../util.js"; import { ApiService } from "./apiService.js"; import WebSocketApiMessageHandler, { newPayload, @@ -52,29 +51,6 @@ export interface Instrument { underlyingLastPrice?: number; } -export interface AccountPosition { - shortQuantity: number; - averagePrice: number; - currentDayProfitLoss: number; - currentDayProfitLossPercentage: number; - longQuantity: number; - settledLongQuantity: number; - settledShortQuantity: number; - instrument: Instrument; - marketValue: number; - maintenanceRequirement?: number; - lastPrice: number; - // this is a purely presentation related field that was added here so we can keep track of the - // previous last price and determine whether the price is going up or down. That way we can color - // animate price updates similarly to how TOS works. - previousPrice?: number; -} - -export type PositionsResponse = { - positions: DeepPartial[]; - service: "positions"; -}; - export default class PositionsMessageHandler implements WebSocketApiMessageHandler { diff --git a/src/client/services/quotesMessageHandler.ts b/src/client/services/quotesMessageHandler.ts index 53f9b89..8300dc4 100644 --- a/src/client/services/quotesMessageHandler.ts +++ b/src/client/services/quotesMessageHandler.ts @@ -37,47 +37,6 @@ export type RawPayloadResponseQuotesSnapshot = { items: RawPayloadResponseQuotesItem[]; }; -type RawPayloadResponseQuotesPatchValue = { - items: RawPayloadResponseQuotesItem[]; -}; - -export type RawPayloadResponseQuotesPatch = { - patches: { - op: string; - path: string; - value: - | number - | RawPayloadResponseQuotesPatchValue - | RawPayloadResponseQuotesItem; - }[]; -}; - -export type QuotesResponse = { - quotes: Partial[]; - service: "quotes"; -}; - -export type QuotesResponseItem = { - symbol?: string; - symbolIndex?: number; - last?: number; - lastSize?: number; - open?: number; - close?: number; - ask?: number; - bid?: number; - high?: number; - low?: number; - askSize?: number; - bidSize?: number; - netChange?: number; - netChangePercent?: number; - mark?: number; - markChange?: number; - markChangePercent?: number; - volume?: number; -}; - export default class QuotesMessageHandler implements WebSocketApiMessageHandler { diff --git a/src/client/types/alertTypes.ts b/src/client/types/alertTypes.ts index e3f5b72..e0a4ba6 100644 --- a/src/client/types/alertTypes.ts +++ b/src/client/types/alertTypes.ts @@ -67,20 +67,3 @@ export type RawAlertCancelResponse = { alertId: number; result: string; }; - -export function parseAlert({ - rawAlert, - description, -}: { - rawAlert: RawAlertResponse; - description?: string; -}): PriceAlert { - const { market, status, id } = rawAlert; - return { - symbol: market.components[0].symbol, - triggerPrice: market.threshold, - description, - status, - id, - }; -} diff --git a/src/client/util.ts b/src/client/util.ts index d6e2b0b..351534b 100644 --- a/src/client/util.ts +++ b/src/client/util.ts @@ -1,5 +1,3 @@ -import { AccountPosition } from "./services/positionsMessageHandler.js"; - export declare type DeepPartial = { [P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial[] @@ -19,10 +17,6 @@ export function ensure(value: T, msg: string): T { return value; } -export function positionNetQuantity(position: AccountPosition): number { - return position.longQuantity - position.shortQuantity; -} - // eslint-disable-line @typescript-eslint/no-explicit-any export function debugLog(...args: any[]) { if (process.env.NODE_ENV === "development") { diff --git a/src/client/wsJsonClientProxy.ts b/src/client/wsJsonClientProxy.ts index b4562e1..d9e9268 100644 --- a/src/client/wsJsonClientProxy.ts +++ b/src/client/wsJsonClientProxy.ts @@ -54,7 +54,7 @@ export type ProxiedRequest = { export type ProxiedResponse = ProxiedRequest & { response: unknown }; // A WsJsonClient proxy implementation that proxies requests to a WebSocket server using the provided `proxyUrl`. -export default class WsJsonClientProxy implements WsJsonClient { +export class WsJsonClientProxy implements WsJsonClient { private state = ChannelState.DISCONNECTED; private buffer = new BufferedIterator(); private iterator = new MulticastIterator(this.buffer); diff --git a/src/example/testApp.ts b/src/example/testApp.ts index 70b518e..396ed09 100644 --- a/src/example/testApp.ts +++ b/src/example/testApp.ts @@ -1,12 +1,13 @@ import debug from "debug"; import "dotenv/config"; -import RealWsJsonClient from "../client/realWsJsonClient.js"; +import { RealWsJsonClient } from "../client/realWsJsonClient.js"; import { CreateAlertRequestParams } from "../client/services/createAlertMessageHandler.js"; import { OptionQuotesRequestParams } from "../client/services/optionQuotesMessageHandler.js"; import { WsJsonClient } from "../client/wsJsonClient.js"; import MarketDepthStateUpdater from "./marketDepthStateUpdater.js"; import { getAuthCode } from "./browserOauth.js"; import { MarketDepthResponse } from "src/client/services/marketDepthMessageHandler.js"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; const logger = debug("testapp"); @@ -160,6 +161,7 @@ async function run() { } else if (username && password) { const authCode = await getAuthCode(username, password); await client.authenticateWithAuthCode(authCode); + storeTokenInDotEnvFile(client.accessToken!, client.refreshToken!); } else { throw new Error( "TOS_ACCESS_TOKEN or TOS_USERNAME and TOS_PASSWORD env vars must be set" @@ -174,4 +176,32 @@ async function run() { ]); } +function storeTokenInDotEnvFile(accessToken: string, refreshToken: string) { + const suffix = process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ""; + const envPath = `.env${suffix}`; + let envContent = ""; + if (existsSync(envPath)) { + envContent = readFileSync(envPath, "utf-8"); + // Remove any existing token lines + envContent = envContent + .split("\n") + .filter( + (line) => + !line.startsWith("TOS_ACCESS_TOKEN=") && + !line.startsWith("TOS_REFRESH_TOKEN=") + ) + .join("\n"); + } + + // Append the new token values + const tokenLines = [ + `TOS_ACCESS_TOKEN=${accessToken}`, + `TOS_REFRESH_TOKEN=${refreshToken}`, + ].join("\n"); + + // Ensure there's a newline between existing content and new tokens + const newContent = envContent.trim() + "\n" + tokenLines + "\n"; + writeFileSync(envPath, newContent); +} + run().catch(console.error); diff --git a/src/example/wsProxyServer.ts b/src/example/wsProxyServer.ts index 53822cf..5c721cd 100644 --- a/src/example/wsProxyServer.ts +++ b/src/example/wsProxyServer.ts @@ -1,5 +1,5 @@ -import WsJsonServer from "../server/wsJsonServer.js"; -import RealWsJsonClient from "../client/realWsJsonClient.js"; +import { WsJsonServer } from "../server/wsJsonServer.js"; +import { RealWsJsonClient } from "../client/realWsJsonClient.js"; import { createServer } from "http"; const proxy = new WsJsonServer(() => new RealWsJsonClient(), createServer()); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e20c3b4 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +export * from "./server/wsJsonServer.js"; +export * from "./client/wsJsonClient.js"; +export * from "./client/realWsJsonClient.js"; +export * from "./client/mockWsJsonClient.js"; +export * from "./client/types/alertTypes.js"; +export * from "./client/tdaWsJsonTypes.js"; +export * from "./client/wsJsonClientProxy.js"; +export * from "./client/messageTypeHelpers.js"; +export * from "./client/services/chartMessageHandler.js"; +export * from "./client/services/quotesMessageHandler.js"; +export * from "./client/services/positionsMessageHandler.js"; +export * from "./client/services/placeOrderMessageHandler.js"; +export * from "./client/services/createAlertMessageHandler.js"; +export * from "./client/services/orderEventsMessageHandler.js"; +export * from "./client/services/optionSeriesMessageHandler.js"; +export * from "./client/services/optionQuotesMessageHandler.js"; +export * from "./client/services/userPropertiesMessageHandler.js"; +export * from "./client/services/marketDepthMessageHandler.js"; +export * from "./client/services/instrumentSearchMessageHandler.js"; +export * from "./client/services/optionChainDetailsMessageHandler.js"; +export * from "./client/services/optionSeriesQuotesMessageHandler.js"; +export * from "./client/services/alertLookupMessageHandler.js"; diff --git a/src/server/wsJsonServer.ts b/src/server/wsJsonServer.ts index ba07e48..4626caa 100644 --- a/src/server/wsJsonServer.ts +++ b/src/server/wsJsonServer.ts @@ -23,7 +23,7 @@ type DefaultHttpServer = HttpServer< * a "request" property that matches a method on the WsJsonClient interface and an "args" property that is an array of * arguments to pass to the method. The response is then forwarded back to the client as a JSON string. */ -export default class WsJsonServer implements Disposable { +export class WsJsonServer implements Disposable { private readonly wss: WebSocketServer; private proxies: WsJsonServerProxy[] = [];