diff --git a/README.md b/README.md index 5f9c924..59b9b6b 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ yarn start - ✅ Option chains - ✅ Alert lookup - ✅ Option chain details +- ✅ Option chain quotes - ✅ Option quotes - ✅ Order events diff --git a/package.json b/package.json index c940e39..093f0a9 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,26 @@ { "name": "tda-wsjson-client", - "version": "0.3.8", + "version": "0.3.9", "description": "WebSocket client for the TD Ameritrade wsjson API", "main": "dist/web.bundle.js", "types": "dist/web.d.ts", "exports": { "./wsJsonClient": "./dist/client/wsJsonClient.js", - "./wsJsonClientAuth": "./dist/client/wsJsonClientAuth.js", "./alertTypes": "./dist/client/types/alertTypes.js", "./tdaWsJsonTypes": "./dist/client/tdaWsJsonTypes.js", + "./wsJsonClientAuth": "./dist/client/wsJsonClientAuth.js", "./messageTypeHelpers": "./dist/client/messageTypeHelpers.js", - "./quotesMessageHandler": "./dist/client/services/quotesMessageHandler.js", "./chartMessageHandler": "./dist/client/services/chartMessageHandler.js", - "./createAlertMessageHandler": "./dist/client/services/createAlertMessageHandler.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" }, "scripts": { diff --git a/src/client/messageTypeHelpers.ts b/src/client/messageTypeHelpers.ts index 0b91077..b4392fb 100644 --- a/src/client/messageTypeHelpers.ts +++ b/src/client/messageTypeHelpers.ts @@ -10,6 +10,7 @@ import { PositionsResponse } from "./services/positionsMessageHandler"; import { PlaceOrderSnapshotResponse } from "./services/placeOrderMessageHandler"; import { OrderEventsPatchResponse, + OrderEventsResponse, OrderEventsSnapshotResponse, } from "./services/orderEventsMessageHandler"; import { UserPropertiesResponse } from "./services/userPropertiesMessageHandler"; @@ -19,6 +20,10 @@ import { OptionChainResponse } from "./services/optionSeriesMessageHandler"; import { RawLoginResponse } from "./services/loginMessageHandler"; import { OptionQuotesResponse } from "./services/optionQuotesMessageHandler"; import { CancelOrderResponse } from "./services/cancelOrderMessageHandler"; +import { + OptionSeriesQuotesPatchResponse, + OptionSeriesQuotesSnapshotResponse, +} from "./services/optionSeriesQuotesMessageHandler"; export function isPayloadResponse( response: WsJsonRawMessage @@ -83,16 +88,38 @@ export function isOrderEventsPatchResponse( return "patches" in response && response.service === "order_events"; } +export function isOrderEventsResponse( + response: ParsedWebSocketResponse +): response is OrderEventsResponse { + const isSnapshotResponse = ( + response: ParsedWebSocketResponse + ): response is OrderEventsSnapshotResponse => + "orders" in response && response.service === "order_events"; + + return isSnapshotResponse(response) || isOrderEventsPatchResponse(response); +} + +// TODO: add support for patch responses export function isOptionQuotesResponse( response: ParsedWebSocketResponse ): response is OptionQuotesResponse { return "service" in response && response.service === "quotes/options"; } -export function isOrderEventsSnapshotResponse( +export function isOptionSeriesQuotesResponse( response: ParsedWebSocketResponse -): response is OrderEventsSnapshotResponse { - return "orders" in response && response.service === "order_events"; +): response is OptionSeriesQuotesSnapshotResponse { + const isSnapshotResponse = ( + response: ParsedWebSocketResponse + ): response is OptionSeriesQuotesSnapshotResponse => + "series" in response && response.service === "optionSeries/quotes"; + + const isPatchResponse = ( + response: ParsedWebSocketResponse + ): response is OptionSeriesQuotesPatchResponse => + "patches" in response && response.service === "optionSeries/quotes"; + + return isSnapshotResponse(response) || isPatchResponse(response); } export function isAlertsResponse( @@ -110,5 +137,5 @@ export function isInstrumentsResponse( export function isOptionChainResponse( response: ParsedWebSocketResponse ): response is OptionChainResponse { - return "series" in response; + return "series" in response && response.service === "optionSeries"; } diff --git a/src/client/services/apiService.ts b/src/client/services/apiService.ts index 22c73a7..5ff205c 100644 --- a/src/client/services/apiService.ts +++ b/src/client/services/apiService.ts @@ -14,4 +14,5 @@ export type ApiService = | "alerts/create" | "alerts/cancel" | "alerts/subscribe" - | "alerts/lookup"; + | "alerts/lookup" + | "optionSeries/quotes"; diff --git a/src/client/services/optionSeriesMessageHandler.ts b/src/client/services/optionSeriesMessageHandler.ts index bff77aa..83ca438 100644 --- a/src/client/services/optionSeriesMessageHandler.ts +++ b/src/client/services/optionSeriesMessageHandler.ts @@ -23,6 +23,7 @@ export type RawOptionSeriesResponse = { export type OptionChainResponse = { series: OptionChainItem[]; + service: "optionSeries"; }; export type OptionChainItem = { @@ -43,6 +44,7 @@ export default class OptionSeriesMessageHandler const { series } = body as RawOptionSeriesResponse; if (series) { return { + service: "optionSeries", series: series.map((s) => ({ underlying: s.underlying, name: s.name, @@ -54,7 +56,7 @@ export default class OptionSeriesMessageHandler })), }; } else { - return { series: [] }; + return { series: [], service: "optionSeries" }; } } diff --git a/src/client/services/optionSeriesQuotesMessageHandler.ts b/src/client/services/optionSeriesQuotesMessageHandler.ts new file mode 100644 index 0000000..837d82a --- /dev/null +++ b/src/client/services/optionSeriesQuotesMessageHandler.ts @@ -0,0 +1,68 @@ +import WebSocketApiMessageHandler, { + newPayload, +} from "./webSocketApiMessageHandler"; +import { RawPayloadRequest, RawPayloadResponse } from "../tdaWsJsonTypes"; +import { ApiService } from "./apiService"; + +export type OptionSeriesQuote = { + name: string; + values: { + IMPLIED_VOLATILITY: number; + SERIES_EXPECTED_MOVE: number; + }; +}; + +export type OptionSeriesQuotesPatchResponse = { + patches: { + op: string; + path: string; + value: any; + }; + service: "optionSeries/quotes"; +}; + +export type OptionSeriesQuotesSnapshotResponse = { + series: OptionSeriesQuote[]; + service: "optionSeries/quotes"; +}; + +export type OptionSeriesQuotesResponse = + | OptionSeriesQuotesSnapshotResponse + | OptionSeriesQuotesPatchResponse; + +export default class OptionSeriesQuotesMessageHandler + implements WebSocketApiMessageHandler +{ + buildRequest(symbol: string): RawPayloadRequest { + return newPayload({ + header: { + service: "optionSeries/quotes", + id: "optionSeriesQuotes", + ver: 0, + }, + params: { + underlying: symbol, + exchange: "BEST", + fields: ["IMPLIED_VOLATILITY", "SERIES_EXPECTED_MOVE"], + }, + }); + } + + parseResponse(message: RawPayloadResponse): OptionSeriesQuotesResponse { + const { payload } = message; + const [{ body }] = payload; + if ("series" in body) { + // snapshot response + const { series } = body as unknown as { series: OptionSeriesQuote[] }; + return { series, service: "optionSeries/quotes" }; + } else { + // patch response + const { patches } = body as unknown as { + patches: OptionSeriesQuotesPatchResponse["patches"]; + }; + return { patches, service: "optionSeries/quotes" }; + } + } + + service: ApiService = "optionSeries/quotes"; +} diff --git a/src/client/tdaWsJsonTypes.ts b/src/client/tdaWsJsonTypes.ts index aea179a..26e650b 100644 --- a/src/client/tdaWsJsonTypes.ts +++ b/src/client/tdaWsJsonTypes.ts @@ -42,6 +42,7 @@ import { OptionChainDetailsResponse } from "./services/optionChainDetailsMessage import { RawLoginResponse } from "./services/loginMessageHandler"; import { OptionQuotesResponse } from "./services/optionQuotesMessageHandler"; import { CancelOrderResponse } from "./services/cancelOrderMessageHandler"; +import { OptionSeriesQuotesResponse } from "./services/optionSeriesQuotesMessageHandler"; export type RawPayloadResponseItemBody = | RawPayloadResponseQuotesSnapshot @@ -115,4 +116,5 @@ export type ParsedWebSocketResponse = | PlaceOrderPatchResponse | OptionChainResponse | OptionChainDetailsResponse - | OptionQuotesResponse; + | OptionQuotesResponse + | OptionSeriesQuotesResponse; diff --git a/src/client/wsJsonClient.ts b/src/client/wsJsonClient.ts index 6e16f96..117dbfc 100644 --- a/src/client/wsJsonClient.ts +++ b/src/client/wsJsonClient.ts @@ -16,8 +16,9 @@ import { isLoginResponse, isOptionChainResponse, isOptionQuotesResponse, + isOptionSeriesQuotesResponse, isOrderEventsPatchResponse, - isOrderEventsSnapshotResponse, + isOrderEventsResponse, isPlaceOrderResponse, isPositionsResponse, isQuotesResponse, @@ -79,6 +80,9 @@ import LoginMessageHandler, { } from "./services/loginMessageHandler"; import SubmitOrderMessageHandler from "./services/submitOrderMessageHandler"; import WorkingOrdersMessageHandler from "./services/workingOrdersMessageHandler"; +import OptionSeriesQuotesMessageHandler, { + OptionSeriesQuotesResponse, +} from "./services/optionSeriesQuotesMessageHandler"; export const CONNECTION_REQUEST_MESSAGE = { ver: "27.*.*", @@ -105,6 +109,7 @@ const messageHandlers: WebSocketApiMessageHandler[] = [ new ChartMessageHandler(), new InstrumentSearchMessageHandler(), new OptionSeriesMessageHandler(), + new OptionSeriesQuotesMessageHandler(), new OrderEventsMessageHandler(), new PlaceOrderMessageHandler(), new PositionsMessageHandler(), @@ -233,6 +238,12 @@ export default class WsJsonClient { .promise() as Promise; } + optionChainQuotes(symbol: string): AsyncIterable { + return this.dispatchHandler(OptionSeriesQuotesMessageHandler, symbol) + .filter(isOptionSeriesQuotesResponse) + .iterable() as AsyncIterable; + } + optionChainDetails( request: OptionChainDetailsRequest ): Promise { @@ -272,8 +283,6 @@ export default class WsJsonClient { workingOrders(accountNumber: string): AsyncIterable { const handler = new WorkingOrdersMessageHandler(); - const isOrderEventsResponse = (r: ParsedWebSocketResponse) => - isOrderEventsSnapshotResponse(r) || isOrderEventsPatchResponse(r); return this.dispatch(handler, accountNumber) .filter(isOrderEventsResponse) .iterable() as AsyncIterable; diff --git a/src/testApp.ts b/src/testApp.ts index dcdf77b..b6c2b15 100644 --- a/src/testApp.ts +++ b/src/testApp.ts @@ -83,6 +83,14 @@ class TestApp { logger("optionChain() %O", optionChain); } + async optionChainQuotes(symbol: string) { + logger(" --- optionChainQuotes() requesting option chain quotes ---"); + const events = this.client.optionChainQuotes(symbol); + for await (const event of events) { + logger("optionChainQuotes() : " + JSON.stringify(event)); + } + } + async optionChainDetails(symbol: string, seriesNames: string[]) { logger(" --- optionChainDetails() requesting option chain details ---"); const optionChainDetails = await this.client.optionChainDetails({ @@ -124,7 +132,7 @@ async function run() { const authClient = new WsJsonClientAuth(clientId, fetch); const { client } = await authClient.authenticateWithRetry(token); const app = new TestApp(client); - await app.workingOrders(); + await app.optionChainQuotes("ABNB"); } run().catch(console.error);