diff --git a/package.json b/package.json index 53abf76..e135925 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tda-wsjson-client", - "version": "0.5.0", + "version": "0.5.2", "description": "WebSocket client for the TD Ameritrade wsjson API", "main": "dist/web.bundle.js", "types": "dist/web.d.ts", diff --git a/src/client/services/quotesMessageHandler.ts b/src/client/services/quotesMessageHandler.ts index 4ca0892..192b691 100644 --- a/src/client/services/quotesMessageHandler.ts +++ b/src/client/services/quotesMessageHandler.ts @@ -2,47 +2,54 @@ import WebSocketApiMessageHandler, { newPayload, } from "./webSocketApiMessageHandler"; import { RawPayloadRequest, RawPayloadResponse } from "../tdaWsJsonTypes"; -import { compact, isEmpty, isNumber } from "lodash"; +import { compact, isEmpty, isNil, isNumber, omitBy } from "lodash"; import { ApiService } from "./apiService"; +const ALL_FIELDS = [ + "MARK", + "MARK_CHANGE", + "MARK_PERCENT_CHANGE", + "NET_CHANGE", + "NET_CHANGE_PERCENT", + "BID", + "ASK", + "BID_SIZE", + "ASK_SIZE", + "VOLUME", + "OPEN", + "HIGH", + "LOW", + "LAST", + "LAST_SIZE", + "CLOSE", +] as const; + +type FieldType = (typeof ALL_FIELDS)[number]; + +export type RawPayloadResponseQuotesItem = { + symbol?: string; + isDelayed?: boolean; + values: { + [key in FieldType]?: number; + }; +}; + export type RawPayloadResponseQuotesSnapshot = { - items: { - isDelayed: boolean; - symbol: string; - values: { [key: string]: any }; - }[]; + items: RawPayloadResponseQuotesItem[]; }; type RawPayloadResponseQuotesPatchValue = { - items: { - symbol: string; - isDelayed: boolean; - values: { - ASK?: number; - ASK_SIZE?: number; - BID?: number; - BID_SIZE?: number; - LAST?: number; - LAST_SIZE?: number; - OPEN?: number; - CLOSE?: number; - HIGH?: number; - LOW?: number; - MARK?: number; - MARK_CHANGE?: number; - MARK_PERCENT_CHANGE?: number; - NET_CHANGE?: number; - NET_CHANGE_PERCENT?: number; - VOLUME?: number; - }; - }[]; + items: RawPayloadResponseQuotesItem[]; }; export type RawPayloadResponseQuotesPatch = { patches: { op: string; path: string; - value: number | RawPayloadResponseQuotesPatchValue; + value: + | number + | RawPayloadResponseQuotesPatchValue + | RawPayloadResponseQuotesItem; }[]; }; @@ -98,24 +105,7 @@ export default class QuotesMessageHandler account: "COMBINED ACCOUNT", symbols, refreshRate: 300, - fields: [ - "MARK", - "MARK_CHANGE", - "MARK_PERCENT_CHANGE", - "NET_CHANGE", - "NET_CHANGE_PERCENT", - "BID", - "ASK", - "BID_SIZE", - "ASK_SIZE", - "VOLUME", - "OPEN", - "HIGH", - "LOW", - "LAST", - "LAST_SIZE", - "CLOSE", - ], + fields: ALL_FIELDS, }, }); } @@ -126,46 +116,35 @@ export default class QuotesMessageHandler function parseSnapshotDataMessage({ items, }: RawPayloadResponseQuotesSnapshot): QuotesResponse { - const quotes = items.map(({ values, symbol }) => { - const last = values.LAST; - const lastSize = values.LAST_SIZE; - const ask = values.ASK; - const bid = values.BID; - const askSize = values.ASK_SIZE; - const bidSize = values.BID_SIZE; - const mark = values.MARK; - const markChange = values.MARK_CHANGE; - const markChangePercent = values.MARK_PERCENT_CHANGE; - const low = values.LOW; - const high = values.HIGH; - const volume = values.VOLUME; - const open = values.OPEN; - const close = values.CLOSE; - const netChange = values.NET_CHANGE; - const netChangePercent = values.NET_CHANGE_PERCENT; - return { - symbol, - last, - lastSize, - ask, - bid, - askSize, - bidSize, - mark, - markChange, - markChangePercent, - low, - high, - volume, - open, - close, - netChange, - netChangePercent, - }; - }); + const quotes = items.map(parseQuoteItem); return { quotes }; } +function parseQuoteItem({ + symbol, + values, +}: RawPayloadResponseQuotesItem): QuotesResponseItem { + return { + last: values.LAST, + lastSize: values.LAST_SIZE, + ask: values.ASK, + askSize: values.ASK_SIZE, + bid: values.BID, + bidSize: values.BID_SIZE, + high: values.HIGH, + low: values.LOW, + open: values.OPEN, + close: values.CLOSE, + mark: values.MARK, + markChange: values.MARK_CHANGE, + markChangePercent: values.MARK_PERCENT_CHANGE, + netChange: values.NET_CHANGE, + netChangePercent: values.NET_CHANGE_PERCENT, + volume: values.VOLUME, + symbol, + }; +} + function parsePatchQuotesDataMessage({ patches, }: RawPayloadResponseQuotesPatch): QuotesResponse | null { @@ -173,118 +152,23 @@ function parsePatchQuotesDataMessage({ path.endsWith(suffix) ? value : undefined; const quotes = patches.flatMap(({ path, value }) => { if (path && isNumber(value)) { - const last = valueIfPath(value, path, "/LAST"); - const lastSize = valueIfPath(value, path, "/LAST_SIZE"); - const mark = valueIfPath(value, path, "/MARK"); - const markChange = valueIfPath(value, path, "/MARK_CHANGE"); - const markChangePercent = valueIfPath( - value, - path, - "/MARK_PERCENT_CHANGE" + const fieldValues = Object.fromEntries( + ALL_FIELDS.map((f) => [f, valueIfPath(value, path, `/${f}`)]) ); - const ask = valueIfPath(value, path, "/ASK"); - const askSize = valueIfPath(value, path, "/ASK_SIZE"); - const bid = valueIfPath(value, path, "/BID"); - const bidSize = valueIfPath(value, path, "/BID_SIZE"); - const netChange = valueIfPath(value, path, "/NET_CHANGE"); - const low = valueIfPath(value, path, "/LOW"); - const high = valueIfPath(value, path, "/HIGH"); - const open = valueIfPath(value, path, "/OPEN"); - const close = valueIfPath(value, path, "/CLOSE"); - const netChangePercent = valueIfPath(value, path, "/NET_CHANGE_PERCENT"); const symbolIndex = +path.split("/")[2]; - if ( - !isEmpty( - compact([ - last, - lastSize, - ask, - bid, - askSize, - bidSize, - symbolIndex, - mark, - markChange, - markChangePercent, - netChange, - netChangePercent, - low, - high, - open, - close, - ]) - ) - ) { - return [ - { - last, - lastSize, - ask, - bid, - askSize, - bidSize, - symbolIndex, - mark, - markChange, - markChangePercent, - netChange, - netChangePercent, - low, - high, - open, - close, - }, - ]; - } else { - // no relevant data to return, omit - return []; - } + const quote = parseQuoteItem({ values: fieldValues }); + return [{ ...quote, symbolIndex }]; + } else if (!isNumber(value) && "items" in value) { + const { items } = value; + return items.map(parseQuoteItem); + } else if (!isNumber(value)) { + return [parseQuoteItem(value)]; } else { - const { items } = value as RawPayloadResponseQuotesPatchValue; - return items.map( - ({ - symbol, - values: { - LAST, - LAST_SIZE, - ASK, - ASK_SIZE, - BID, - BID_SIZE, - OPEN, - CLOSE, - HIGH, - LOW, - MARK, - MARK_CHANGE, - MARK_PERCENT_CHANGE, - NET_CHANGE, - NET_CHANGE_PERCENT, - VOLUME, - }, - }) => - ({ - last: LAST, - lastSize: LAST_SIZE, - ask: ASK, - askSize: ASK_SIZE, - bid: BID, - bidSize: BID_SIZE, - high: HIGH, - low: LOW, - open: OPEN, - close: CLOSE, - mark: MARK, - markChange: MARK_CHANGE, - markChangePercent: MARK_PERCENT_CHANGE, - netChange: NET_CHANGE, - netChangePercent: NET_CHANGE_PERCENT, - volume: VOLUME, - symbol, - } as QuotesResponseItem) - ); + return []; } }); const finalQuotes = compact(quotes); - return !isEmpty(finalQuotes) ? { quotes: finalQuotes } : null; + return !isEmpty(finalQuotes) + ? { quotes: finalQuotes.map((q) => omitBy(q, isNil)) } + : null; } diff --git a/src/testApp.ts b/src/testApp.ts index fc76387..f6d1225 100644 --- a/src/testApp.ts +++ b/src/testApp.ts @@ -6,7 +6,6 @@ import { PlaceLimitOrderRequestParams } from "./client/services/placeOrderMessag import { CreateAlertRequestParams } from "./client/services/createAlertMessageHandler"; import { OptionQuotesRequestParams } from "./client/services/optionQuotesMessageHandler"; import fetch from "node-fetch"; -import { isNil, omitBy } from "lodash"; const logger = debug("testapp"); @@ -44,7 +43,13 @@ class TestApp { for await (const quote of this.client.quotes(symbols)) { logger( "quotes() %O", - quote.quotes.map((q) => omitBy(q, isNil)) + quote.quotes.map((q) => { + if (!q.symbol && q.symbolIndex) { + q.symbol = symbols[q.symbolIndex]; + delete q.symbolIndex; + } + return q; + }) ); } }