Skip to content

Commit

Permalink
update project to esm
Browse files Browse the repository at this point in the history
  • Loading branch information
felipecsl committed Jan 26, 2025
1 parent 39e74ae commit 2214252
Show file tree
Hide file tree
Showing 42 changed files with 354 additions and 240 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# TD Ameritrade WsJson API client

This is a node and browser API client for the (undocumented) TD Ameritrade WebSocket API.
This is a node and browser API client for the (undocumented) Schwab WebSocket API.

🚧 Work in progress 🚧

# Prerequisites

- Node 16+

Create a `.env` file and set your TD Ameritrade oauth access token:
Create a `.env` file and set your Schwab oauth access token:

```
CLIENT_ID=your-client-id
Expand All @@ -27,7 +27,7 @@ yarn build
# Running the example app

```
cd example
cd src/example
yarn install
yarn link tda-wsjson-client
yarn start
Expand Down Expand Up @@ -79,8 +79,8 @@ for await (const event of client.chart(chartRequest)) {
}
```

For more sample usage check out https://github.com/felipecsl/tda-wsjson-client/blob/master/src/testApp.ts and
https://github.com/felipecsl/tda-wsjson-client/blob/master/example/src/App.tsx
For more sample usage check out https://github.com/huskly/tda-wsjson-client/blob/master/src/testApp.ts and
https://github.com/huskly/tda-wsjson-client/blob/master/example/src/App.tsx

# Running tests

Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "WebSocket client for the TD Ameritrade wsjson API",
"main": "dist/web.bundle.js",
"types": "dist/web.d.ts",
"type": "module",
"exports": {
"./wsJsonServer": "./dist/server/wsJsonServer.js",
"./wsJsonClient": "./dist/client/wsJsonClient.js",
Expand Down Expand Up @@ -46,18 +47,19 @@
"@types/node": "^20.3.0",
"debug": "^4.3.4",
"isomorphic-ws": "^5.0.0",
"lodash": "^4.17.21",
"obgen": "^0.5.1",
"lodash-es": "^4.17.21",
"obgen": "^0.5.2",
"prompts": "2.4.2",
"resolve": "1.22.4",
"ws": "^8.13.0"
"ws": "^8.17.0"
},
"author": "Felipe Lima",
"license": "MIT",
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/debug": "^4.1.8",
"@types/jest": "^29.4.0",
"@types/lodash-es": "^4.17.12",
"@types/node-fetch": "^2.6.4",
"@types/ws": "^8.5.5",
"@typescript-eslint/eslint-plugin": "^6.13.2",
Expand All @@ -81,6 +83,7 @@
"prettier": "^2.4.1",
"ts-node": "^10.4.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.4"
}
"typescript": "^5.7.3"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
13 changes: 11 additions & 2 deletions src/client/messageTypeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {
ConnectionResponse,
RawPayloadResponse,
WsJsonRawMessage,
} from "./tdaWsJsonTypes";
import { RawLoginResponse } from "./services/loginMessageHandler";
} from "./tdaWsJsonTypes.js";
import { RawLoginResponse } from "./services/loginMessageHandler.js";

export function isPayloadResponse(
response: WsJsonRawMessage
Expand All @@ -25,3 +25,12 @@ export function isLoginResponse(
const { service } = header;
return service === "login";
}

export function isSchwabLoginResponse(
message: WsJsonRawMessage
): message is RawLoginResponse {
if (!isPayloadResponse(message)) return false;
const [{ header }] = message.payload;
const { service } = header;
return service === "login/schwab";
}
36 changes: 18 additions & 18 deletions src/client/mockWsJsonClient.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
import { WsJsonClient } from "./wsJsonClient";
import { PositionsResponse } from "./services/positionsMessageHandler";
import { RawLoginResponseBody } from "./services/loginMessageHandler";
import { CancelOrderResponse } from "./services/cancelOrderMessageHandler";
import { WsJsonClient } from "./wsJsonClient.js";
import { PositionsResponse } from "./services/positionsMessageHandler.js";
import { RawLoginResponseBody } from "./services/loginMessageHandler.js";
import { CancelOrderResponse } from "./services/cancelOrderMessageHandler.js";
import {
ChartRequestParams,
ChartResponse,
} from "./services/chartMessageHandler";
import { CreateAlertRequestParams } from "./services/createAlertMessageHandler";
import { OptionChainResponse } from "./services/optionSeriesMessageHandler";
} from "./services/chartMessageHandler.js";
import { CreateAlertRequestParams } from "./services/createAlertMessageHandler.js";
import { OptionChainResponse } from "./services/optionSeriesMessageHandler.js";
import {
OptionChainDetailsRequest,
OptionChainDetailsResponse,
} from "./services/optionChainDetailsMessageHandler";
import { OptionSeriesQuotesResponse } from "./services/optionSeriesQuotesMessageHandler";
} from "./services/optionChainDetailsMessageHandler.js";
import { OptionSeriesQuotesResponse } from "./services/optionSeriesQuotesMessageHandler.js";
import {
OptionQuotesRequestParams,
OptionQuotesResponse,
} from "./services/optionQuotesMessageHandler";
} from "./services/optionQuotesMessageHandler.js";
import {
PlaceLimitOrderRequestParams,
PlaceOrderSnapshotResponse,
} from "./services/placeOrderMessageHandler";
import { OrderEventsResponse } from "./services/orderEventsMessageHandler";
import { QuotesResponse } from "./services/quotesMessageHandler";
import { InstrumentSearchResponse } from "./services/instrumentSearchMessageHandler";
import { UserPropertiesResponse } from "./services/userPropertiesMessageHandler";
} from "./services/placeOrderMessageHandler.js";
import { OrderEventsResponse } from "./services/orderEventsMessageHandler.js";
import { QuotesResponse } from "./services/quotesMessageHandler.js";
import { InstrumentSearchResponse } from "./services/instrumentSearchMessageHandler.js";
import { UserPropertiesResponse } from "./services/userPropertiesMessageHandler.js";
import {
CancelAlertResponse,
CreateAlertResponse,
LookupAlertsResponse,
} from "./types/alertTypes";
import { MarketDepthResponse } from "./services/marketDepthMessageHandler";
import { GetWatchlistResponse } from "./services/getWatchlistMessageHandler";
} from "./types/alertTypes.js";
import { MarketDepthResponse } from "./services/marketDepthMessageHandler.js";
import { GetWatchlistResponse } from "./services/getWatchlistMessageHandler.js";

export default class MockWsJsonClient implements WsJsonClient {
async *accountPositions(_: string): AsyncIterable<PositionsResponse> {
Expand Down
88 changes: 55 additions & 33 deletions src/client/realWsJsonClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,79 @@ import {
ParsedWebSocketResponse,
RawPayloadResponse,
WsJsonRawMessage,
} from "./tdaWsJsonTypes";
import { Constructor, debugLog, findByTypeOrThrow, throwError } from "./util";
import MulticastIterator from "obgen/multicastIterator";
import BufferedIterator from "obgen/bufferedIterator";
import { isConnectionResponse, isLoginResponse } from "./messageTypeHelpers";
} from "./tdaWsJsonTypes.js";
import { Constructor, debugLog, findByTypeOrThrow, throwError } from "./util.js";
import { isConnectionResponse, isLoginResponse, isSchwabLoginResponse } from "./messageTypeHelpers.js";
import { deferredWrap } from "obgen";
import debug from "debug";
import Observable from "obgen/observable";
import { Observable, BufferedIterator, MulticastIterator } from "obgen";
import OptionChainDetailsMessageHandler, {
OptionChainDetailsRequest,
OptionChainDetailsResponse,
} from "./services/optionChainDetailsMessageHandler";
import CancelAlertMessageHandler from "./services/cancelAlertMessageHandler";
} from "./services/optionChainDetailsMessageHandler.js";
import CancelAlertMessageHandler from "./services/cancelAlertMessageHandler.js";
import CreateAlertMessageHandler, {
CreateAlertRequestParams,
} from "./services/createAlertMessageHandler";
import AlertLookupMessageHandler from "./services/alertLookupMessageHandler";
import SubscribeToAlertMessageHandler from "./services/subscribeToAlertMessageHandler";
} from "./services/createAlertMessageHandler.js";
import AlertLookupMessageHandler from "./services/alertLookupMessageHandler.js";
import SubscribeToAlertMessageHandler from "./services/subscribeToAlertMessageHandler.js";
import OptionQuotesMessageHandler, {
OptionQuotesRequestParams,
OptionQuotesResponse,
} from "./services/optionQuotesMessageHandler";
} from "./services/optionQuotesMessageHandler.js";
import ChartMessageHandler, {
ChartRequestParams,
ChartResponse,
} from "./services/chartMessageHandler";
} from "./services/chartMessageHandler.js";
import InstrumentSearchMessageHandler, {
InstrumentSearchResponse,
} from "./services/instrumentSearchMessageHandler";
} from "./services/instrumentSearchMessageHandler.js";
import CancelOrderMessageHandler, {
CancelOrderResponse,
} from "./services/cancelOrderMessageHandler";
} from "./services/cancelOrderMessageHandler.js";
import OptionSeriesMessageHandler, {
OptionChainResponse,
} from "./services/optionSeriesMessageHandler";
} from "./services/optionSeriesMessageHandler.js";
import OrderEventsMessageHandler, {
OrderEventsResponse,
} from "./services/orderEventsMessageHandler";
} from "./services/orderEventsMessageHandler.js";
import PositionsMessageHandler, {
PositionsResponse,
} from "./services/positionsMessageHandler";
} from "./services/positionsMessageHandler.js";
import QuotesMessageHandler, {
QuotesResponse,
} from "./services/quotesMessageHandler";
} from "./services/quotesMessageHandler.js";
import UserPropertiesMessageHandler, {
UserPropertiesResponse,
} from "./services/userPropertiesMessageHandler";
} from "./services/userPropertiesMessageHandler.js";
import PlaceOrderMessageHandler, {
PlaceLimitOrderRequestParams,
PlaceOrderSnapshotResponse,
} from "./services/placeOrderMessageHandler";
import WebSocketApiMessageHandler from "./services/webSocketApiMessageHandler";
import ResponseParser from "./responseParser";
} from "./services/placeOrderMessageHandler.js";
import WebSocketApiMessageHandler from "./services/webSocketApiMessageHandler.js";
import ResponseParser from "./responseParser.js";
import LoginMessageHandler, {
RawLoginResponse,
RawLoginResponseBody,
} from "./services/loginMessageHandler";
import SubmitOrderMessageHandler from "./services/submitOrderMessageHandler";
import WorkingOrdersMessageHandler from "./services/workingOrdersMessageHandler";
} from "./services/loginMessageHandler.js";
import SubmitOrderMessageHandler from "./services/submitOrderMessageHandler.js";
import WorkingOrdersMessageHandler from "./services/workingOrdersMessageHandler.js";
import OptionSeriesQuotesMessageHandler, {
OptionSeriesQuotesResponse,
} from "./services/optionSeriesQuotesMessageHandler";
import { WsJsonClient } from "./wsJsonClient";
} from "./services/optionSeriesQuotesMessageHandler.js";
import { WsJsonClient } from "./wsJsonClient.js";
import MarketDepthMessageHandler, {
MarketDepthResponse,
} from "./services/marketDepthMessageHandler";
} from "./services/marketDepthMessageHandler.js";
import {
CancelAlertResponse,
CreateAlertResponse,
LookupAlertsResponse,
} from "./types/alertTypes";
} from "./types/alertTypes.js";
import GetWatchlistMessageHandler, {
GetWatchlistResponse,
} from "./services/getWatchlistMessageHandler";
} from "./services/getWatchlistMessageHandler.js";
import SchwabLoginMessageHandler from "./services/schwabLoginMessageHandler.js";

export const CONNECTION_REQUEST_MESSAGE = {
ver: "27.*.*",
Expand Down Expand Up @@ -112,6 +111,7 @@ const messageHandlers: WebSocketApiMessageHandler<never, any>[] = [
new UserPropertiesMessageHandler(),
new OptionChainDetailsMessageHandler(),
new LoginMessageHandler(),
new SchwabLoginMessageHandler(),
new SubmitOrderMessageHandler(),
new MarketDepthMessageHandler(),
new GetWatchlistMessageHandler(),
Expand All @@ -125,7 +125,7 @@ export default class RealWsJsonClient implements WsJsonClient {

constructor(
private readonly socket = new WebSocket(
"wss://services.thinkorswim.com/Services/WsJson",
"wss://thinkorswim-services.schwab.com/Services/WsJson",
{
headers: {
Pragma: "no-cache",
Expand Down Expand Up @@ -191,13 +191,15 @@ export default class RealWsJsonClient implements WsJsonClient {
const message = JSON.parse(data) as WsJsonRawMessage;
logger("⬅️\treceived %O", message);
if (isConnectionResponse(message)) {
const handler = findByTypeOrThrow(messageHandlers, LoginMessageHandler);
const handler = findByTypeOrThrow(messageHandlers, SchwabLoginMessageHandler);
if (!accessToken) {
throwError("access token is required, cannot authenticate");
}
this.sendMessage(handler.buildRequest(accessToken));
} else if (isLoginResponse(message)) {
this.handleLoginResponse(message, resolve, reject);
} else if (isSchwabLoginResponse(message)) {
this.handleSchwabLoginResponse(message, resolve, reject);
} else {
const parsedResponse = responseParser.parseResponse(message);
if (parsedResponse) {
Expand Down Expand Up @@ -348,6 +350,26 @@ export default class RealWsJsonClient implements WsJsonClient {
this.socket?.send(msg);
}

private handleSchwabLoginResponse(
message: RawLoginResponse,
resolve: (value: RawLoginResponseBody) => void,
reject: (reason?: string) => void
) {
const handler = findByTypeOrThrow(messageHandlers, SchwabLoginMessageHandler);
const loginResponse = handler.parseResponse(message as RawPayloadResponse);
const [{ body }] = message.payload;
if (loginResponse.authenticated) {
this.state = ChannelState.CONNECTED;
logger("Schwab login successful, token=%s", body.token);
this.accessToken = body.token;
resolve(body);
} else {
this.state = ChannelState.ERROR;
reject(`Login failed: ${body.message}`);
this.disconnect();
}
}

private handleLoginResponse(
message: RawLoginResponse,
resolve: (value: RawLoginResponseBody) => void,
Expand Down
12 changes: 6 additions & 6 deletions src/client/responseParser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ParsedWebSocketResponse, WsJsonRawMessage } from "./tdaWsJsonTypes";
import { debugLog } from "./util";
import { isPayloadResponse } from "./messageTypeHelpers";
import WebSocketApiMessageHandler from "./services/webSocketApiMessageHandler";
import { ApiService } from "./services/apiService";
import { keyBy } from "lodash";
import { ParsedWebSocketResponse, WsJsonRawMessage } from "./tdaWsJsonTypes.js";
import { debugLog } from "./util.js";
import { isPayloadResponse } from "./messageTypeHelpers.js";
import WebSocketApiMessageHandler from "./services/webSocketApiMessageHandler.js";
import { ApiService } from "./services/apiService.js";
import { keyBy } from "lodash-es";

export default class ResponseParser {
private readonly messageHandlerRegistry: Record<
Expand Down
10 changes: 5 additions & 5 deletions src/client/services/alertLookupMessageHandler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import WebSocketApiMessageHandler from "./webSocketApiMessageHandler";
import { RawPayloadRequest, RawPayloadResponse } from "../tdaWsJsonTypes";
import WebSocketApiMessageHandler from "./webSocketApiMessageHandler.js";
import { RawPayloadRequest, RawPayloadResponse } from "../tdaWsJsonTypes.js";
import {
LookupAlertsResponse,
parseAlert,
RawAlertResponse,
} from "../types/alertTypes";
import { isEmpty } from "lodash";
import { ApiService } from "./apiService";
} from "../types/alertTypes.js";
import { isEmpty } from "lodash-es";
import { ApiService } from "./apiService.js";

export type RawAlertLookupResponse = {
alerts: RawAlertResponse[];
Expand Down
4 changes: 4 additions & 0 deletions src/client/services/apiService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type ApiService =
| "login"
| "login/schwab"
| "cancel_order"
| "chart"
| "order_events"
Expand All @@ -19,3 +20,6 @@ export type ApiService =
| "market_depth"
| "watchlist/get"
| "fake"; // testing only

// make node happy
export const __internalMarker = true
10 changes: 5 additions & 5 deletions src/client/services/cancelAlertMessageHandler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import WebSocketApiMessageHandler from "./webSocketApiMessageHandler";
import WebSocketApiMessageHandler from "./webSocketApiMessageHandler.js";
import {
CancelAlertResponse,
RawAlertCancelResponse,
} from "../types/alertTypes";
import { RawPayloadRequest, RawPayloadResponse } from "../tdaWsJsonTypes";
import { debugLog } from "../util";
import { ApiService } from "./apiService";
} from "../types/alertTypes.js";
import { RawPayloadRequest, RawPayloadResponse } from "../tdaWsJsonTypes.js";
import { debugLog } from "../util.js";
import { ApiService } from "./apiService.js";

export default class CancelAlertMessageHandler
implements WebSocketApiMessageHandler<number, CancelAlertResponse | null>
Expand Down
Loading

0 comments on commit 2214252

Please sign in to comment.