@tastytrade/api 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +103 -28
  3. package/dist/account-streamer.d.ts +8 -4
  4. package/dist/account-streamer.d.ts.map +1 -1
  5. package/dist/account-streamer.js +28 -21
  6. package/dist/account-streamer.js.map +1 -1
  7. package/dist/logger.d.ts +24 -0
  8. package/dist/logger.d.ts.map +1 -0
  9. package/dist/logger.js +40 -0
  10. package/dist/logger.js.map +1 -0
  11. package/dist/market-data-streamer.d.ts.map +1 -1
  12. package/dist/market-data-streamer.js +1 -0
  13. package/dist/market-data-streamer.js.map +1 -1
  14. package/dist/models/access-token.d.ts +12 -0
  15. package/dist/models/access-token.d.ts.map +1 -0
  16. package/dist/models/access-token.js +35 -0
  17. package/dist/models/access-token.js.map +1 -0
  18. package/dist/models/tastytrade-session.d.ts.map +1 -1
  19. package/dist/models/tastytrade-session.js.map +1 -1
  20. package/dist/quote-streamer.d.ts +58 -0
  21. package/dist/quote-streamer.d.ts.map +1 -0
  22. package/dist/quote-streamer.js +132 -0
  23. package/dist/quote-streamer.js.map +1 -0
  24. package/dist/services/account-status-service.js +0 -1
  25. package/dist/services/account-status-service.js.map +1 -1
  26. package/dist/services/accounts-and-customers-service.js +0 -1
  27. package/dist/services/accounts-and-customers-service.js.map +1 -1
  28. package/dist/services/balances-and-positions-service.js +0 -1
  29. package/dist/services/balances-and-positions-service.js.map +1 -1
  30. package/dist/services/instruments-service.js +1 -2
  31. package/dist/services/instruments-service.js.map +1 -1
  32. package/dist/services/margin-requirements-service.js +0 -1
  33. package/dist/services/margin-requirements-service.js.map +1 -1
  34. package/dist/services/market-metrics-service.js +0 -1
  35. package/dist/services/market-metrics-service.js.map +1 -1
  36. package/dist/services/net-liquidating-value-history-service.js +0 -1
  37. package/dist/services/net-liquidating-value-history-service.js.map +1 -1
  38. package/dist/services/orders-service.js +0 -1
  39. package/dist/services/orders-service.js.map +1 -1
  40. package/dist/services/risk-parameters-service.js +0 -1
  41. package/dist/services/risk-parameters-service.js.map +1 -1
  42. package/dist/services/session-service.d.ts +6 -1
  43. package/dist/services/session-service.d.ts.map +1 -1
  44. package/dist/services/session-service.js +10 -2
  45. package/dist/services/session-service.js.map +1 -1
  46. package/dist/services/symbol-search-service.js +0 -1
  47. package/dist/services/symbol-search-service.js.map +1 -1
  48. package/dist/services/tastytrade-http-client.d.ts +18 -2
  49. package/dist/services/tastytrade-http-client.d.ts.map +1 -1
  50. package/dist/services/tastytrade-http-client.js +73 -14
  51. package/dist/services/tastytrade-http-client.js.map +1 -1
  52. package/dist/services/transactions-service.js +0 -1
  53. package/dist/services/transactions-service.js.map +1 -1
  54. package/dist/services/watchlists-service.js +0 -1
  55. package/dist/services/watchlists-service.js.map +1 -1
  56. package/dist/tastytrade-api.d.ts +24 -5
  57. package/dist/tastytrade-api.d.ts.map +1 -1
  58. package/dist/tastytrade-api.js +26 -9
  59. package/dist/tastytrade-api.js.map +1 -1
  60. package/dist/utils/json-util.d.ts.map +1 -1
  61. package/dist/utils/json-util.js +0 -2
  62. package/dist/utils/json-util.js.map +1 -1
  63. package/dist/utils/response-util.d.ts.map +1 -1
  64. package/dist/utils/response-util.js +0 -1
  65. package/dist/utils/response-util.js.map +1 -1
  66. package/eslint.config.cjs +46 -0
  67. package/lib/account-streamer.ts +44 -29
  68. package/lib/logger.ts +53 -0
  69. package/lib/models/access-token.ts +40 -0
  70. package/lib/models/tastytrade-session.ts +1 -1
  71. package/lib/quote-streamer.ts +151 -0
  72. package/lib/services/instruments-service.ts +1 -1
  73. package/lib/services/session-service.ts +13 -1
  74. package/lib/services/tastytrade-http-client.ts +98 -16
  75. package/lib/tastytrade-api.ts +45 -5
  76. package/lib/utils/json-util.ts +0 -2
  77. package/lib/utils/response-util.ts +3 -4
  78. package/package.json +26 -18
  79. package/tsconfig.json +3 -3
  80. package/.eslintrc.cjs +0 -18
  81. package/lib/market-data-streamer.ts +0 -376
package/lib/logger.ts ADDED
@@ -0,0 +1,53 @@
1
+ import _ from 'lodash'
2
+
3
+ export default interface Logger {
4
+ error(...data: any[]): void;
5
+ info(...data: any[]): void;
6
+ warn(...data: any[]): void;
7
+ }
8
+
9
+ export enum LogLevel {
10
+ INFO = 1,
11
+ WARN = 2,
12
+ ERROR = 3
13
+ }
14
+
15
+ export class TastytradeLogger implements Logger {
16
+ public logLevel: LogLevel
17
+ public logger: Logger | null = null
18
+
19
+ constructor(logger?: Logger, logLevel?: LogLevel) {
20
+ this.logger = logger ?? null
21
+ this.logLevel = logLevel ?? LogLevel.ERROR
22
+ }
23
+
24
+ public updateConfig(config: Partial<{logger: Logger, logLevel: LogLevel}>) {
25
+ const loggerConfig = _.pick(config, ['logger', 'logLevel'])
26
+ Object.assign(this, loggerConfig)
27
+ }
28
+
29
+ error(...data: any[]): void {
30
+ if (this.shouldLog(LogLevel.ERROR)) {
31
+ this.logger!.error(...data)
32
+ }
33
+ }
34
+
35
+ info(...data: any[]): void {
36
+ if (this.shouldLog(LogLevel.INFO)) {
37
+ this.logger!.info(...data)
38
+ }
39
+ }
40
+
41
+ warn(...data: any[]): void {
42
+ if (this.shouldLog(LogLevel.WARN)) {
43
+ this.logger!.warn(...data)
44
+ }
45
+ }
46
+
47
+ private shouldLog(level: LogLevel) {
48
+ if (_.isNil(this.logger)) {
49
+ return false
50
+ }
51
+ return LogLevel[level] >= LogLevel[this.logLevel]
52
+ }
53
+ }
@@ -0,0 +1,40 @@
1
+ import _ from 'lodash'
2
+
3
+ export default class AccessToken {
4
+ public token = ''
5
+ public expiresIn = 0
6
+
7
+ get isExpired(): boolean {
8
+ return this.isEmpty || Date.now() >= this.expiration.getTime();
9
+ }
10
+
11
+ get isValid(): boolean {
12
+ return !_.isNil(this.token) && !this.isExpired;
13
+ }
14
+
15
+ get isEmpty(): boolean {
16
+ return _.isNil(this.token) || this.token.length === 0;
17
+ }
18
+
19
+ get expiration() {
20
+ // Set expiration a bit earlier to account for clock skew
21
+ return new Date(Date.now() + ((this.expiresIn - 30) * 1000));
22
+ }
23
+
24
+ public updateFromTokenResponse(tokenResponse: any): void {
25
+ this.token = _.get(tokenResponse, "data.access_token");
26
+ this.expiresIn = _.get(tokenResponse, "data.expires_in");
27
+ }
28
+
29
+ get authorizationHeader(): string | null {
30
+ if (this.isValid) {
31
+ return `Bearer ${this.token}`;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ clear() {
37
+ this.token = ''
38
+ this.expiresIn = 0
39
+ }
40
+ }
@@ -1,7 +1,7 @@
1
1
  import _ from 'lodash'
2
2
 
3
3
  export default class TastytradeSession {
4
- authToken: string | null = null
4
+ public authToken: string | null = null
5
5
 
6
6
  get isValid() {
7
7
  return !_.isNil(this.authToken)
@@ -0,0 +1,151 @@
1
+ import { DXLinkWebSocketClient, DXLinkFeed, FeedContract, FeedDataFormat, type DXLinkFeedEventListener } from '@dxfeed/dxlink-api'
2
+ import type AccountsAndCustomersService from './services/accounts-and-customers-service.js'
3
+ import type Logger from './logger.js'
4
+ import _ from 'lodash'
5
+
6
+ // TODO: Make sure this works in node and we don't have to override the global Websocket class
7
+
8
+ export enum MarketDataSubscriptionType {
9
+ Candle = 'Candle',
10
+ Quote = 'Quote',
11
+ Trade = 'Trade',
12
+ Summary = 'Summary',
13
+ Profile = 'Profile',
14
+ Greeks = 'Greeks',
15
+ Underlying = 'Underlying'
16
+ }
17
+
18
+ const ALL_EVENT_TYPES = [
19
+ MarketDataSubscriptionType.Quote,
20
+ MarketDataSubscriptionType.Trade,
21
+ MarketDataSubscriptionType.Summary,
22
+ MarketDataSubscriptionType.Profile,
23
+ MarketDataSubscriptionType.Greeks,
24
+ MarketDataSubscriptionType.Underlying
25
+ ]
26
+
27
+ export enum CandleType {
28
+ Tick = 't',
29
+ Second = 's',
30
+ Minute = 'm',
31
+ Hour = 'h',
32
+ Day = 'd',
33
+ Week = 'w',
34
+ Month = 'mo',
35
+ ThirdFriday = 'o',
36
+ Year = 'y',
37
+ Volume = 'v',
38
+ Price = 'p'
39
+ }
40
+
41
+ export default class QuoteStreamer {
42
+ public dxLinkFeed: DXLinkFeed<any> | null = null
43
+ public dxLinkUrl: string | null = null
44
+ public dxLinkAuthToken: string | null = null
45
+ public eventListeners: DXLinkFeedEventListener[] = []
46
+
47
+ constructor(private readonly accountsAndCustomersService: AccountsAndCustomersService, private readonly logger: Logger) {
48
+ }
49
+
50
+ /**
51
+ * Connects to the DxLink WebSocket and sets up the feed
52
+ * Make sure to call disconnect() when done
53
+ * Calls `getApiQuoteToken` to get the connection URL and auth token
54
+ * Make sure you have a valid session or access token before calling this
55
+ */
56
+ async connect() {
57
+ const tokenResponse = await this.accountsAndCustomersService.getApiQuoteToken()
58
+ this.dxLinkUrl = _.get(tokenResponse, 'dxlink-url')
59
+ this.dxLinkAuthToken = _.get(tokenResponse, 'token')
60
+
61
+ const client = new DXLinkWebSocketClient()
62
+ client.connect(this.dxLinkUrl!)
63
+ client.setAuthToken(this.dxLinkAuthToken!)
64
+
65
+ this.dxLinkFeed = new DXLinkFeed(client, FeedContract.AUTO)
66
+
67
+ this.dxLinkFeed.configure({
68
+ acceptAggregationPeriod: 10,
69
+ acceptDataFormat: FeedDataFormat.COMPACT
70
+ })
71
+
72
+ this.eventListeners.forEach(listener => this.dxLinkFeed!.addEventListener(listener))
73
+ }
74
+
75
+ disconnect() {
76
+ if (_.isNil(this.dxLinkFeed)) {
77
+ return
78
+ }
79
+
80
+ this.eventListeners.forEach(listener => this.removeEventListener(listener))
81
+ this.dxLinkFeed = null
82
+ }
83
+
84
+ // Returns a function that can be called to remove the listener
85
+ addEventListener(listener: DXLinkFeedEventListener): () => void {
86
+ this.eventListeners.push(listener)
87
+ if (this.dxLinkFeed) {
88
+ this.dxLinkFeed.addEventListener(listener)
89
+ }
90
+
91
+ return () => {
92
+ this.removeEventListener(listener)
93
+ }
94
+ }
95
+
96
+ removeEventListener(listenerToRemove: DXLinkFeedEventListener) {
97
+ _.remove(this.eventListeners, listener => listener === listenerToRemove)
98
+ if (this.dxLinkFeed) {
99
+ this.dxLinkFeed.removeEventListener(listenerToRemove)
100
+ }
101
+ }
102
+
103
+ subscribe(streamerSymbols: string[], types: MarketDataSubscriptionType[] | null = null) {
104
+ if (_.isNil(this.dxLinkFeed)) {
105
+ throw new Error('DxLink feed is not connected')
106
+ }
107
+
108
+ types = types ?? ALL_EVENT_TYPES
109
+ streamerSymbols.forEach(symbol => {
110
+ types.forEach(type => {
111
+ this.dxLinkFeed!.addSubscriptions({ type, symbol })
112
+ })
113
+ })
114
+ }
115
+
116
+ unsubscribe(streamerSymbols: string[]) {
117
+ if (_.isNil(this.dxLinkFeed)) {
118
+ throw new Error('DxLink feed is not connected')
119
+ }
120
+
121
+ streamerSymbols.forEach(symbol => {
122
+ ALL_EVENT_TYPES.forEach(type => {
123
+ this.dxLinkFeed!.removeSubscriptions({ type, symbol })
124
+ })
125
+ })
126
+ }
127
+
128
+ /**
129
+ * Adds a candle subscription
130
+ * @param streamerSymbol Get this from an instrument's streamer-symbol json response field
131
+ * @param fromTime Epoch timestamp from where you want to start
132
+ * @param period The duration of each candle
133
+ * @param type The duration type of the period
134
+ * For example, a period/type of 5/m means you want each candle to represent 5 minutes of data
135
+ * From there, setting fromTime to 24 hours ago would give you 24 hours of data grouped in 5 minute intervals
136
+ * @returns
137
+ */
138
+ subscribeCandles(streamerSymbol: string, fromTime: number, period: number, type: CandleType) {
139
+ // Example: AAPL{=5m} where each candle represents 5 minutes of data
140
+ const candleSymbol = `${streamerSymbol}{=${period}${type}}`
141
+ if (_.isNil(this.dxLinkFeed)) {
142
+ throw new Error('DxLink feed is not connected')
143
+ }
144
+
145
+ this.dxLinkFeed!.addSubscriptions({
146
+ type: MarketDataSubscriptionType.Candle,
147
+ symbol: candleSymbol,
148
+ fromTime
149
+ })
150
+ }
151
+ }
@@ -118,7 +118,7 @@ export default class InstrumentsService {
118
118
  return extractResponseData(futureOptionChain)
119
119
  }
120
120
 
121
- //Option-chains: Allows an API client to fetch futures option chains.
121
+ //Option-chains: Allows an API client to fetch equity option chains.
122
122
  async getNestedOptionChain(symbol: string){
123
123
  //Returns an option chain given an underlying symbol,
124
124
  //i.e. AAPL in a nested form to minimize redundant processing
@@ -5,6 +5,18 @@ export default class SessionService {
5
5
  constructor(public httpClient: TastytradeHttpClient) {
6
6
  }
7
7
 
8
+ private get clientId(): string {
9
+ return '9953f07a-5de4-408c-a8ab-688a6320f00f'
10
+ }
11
+
12
+ private get clientSecret(): string {
13
+ return 'baa245033420a05d013541c0c6ef4f98bb16a1ec'
14
+ }
15
+
16
+ private get refreshToken(): string {
17
+ return 'eyJhbGciOiJFZERTQSIsInR5cCI6InJ0K2p3dCIsImtpZCI6IkZqVTdUT25qVEQ2WnVySlg2cVlwWmVPbzBDQzQ5TnIzR1pUN1E4MTc0cUkiLCJqa3UiOiJodHRwczovL2ludGVyaW9yLWFwaS5hcjIudGFzdHl0cmFkZS5zeXN0ZW1zL29hdXRoL2p3a3MifQ.eyJpc3MiOiJodHRwczovL2FwaS50YXN0eXRyYWRlLmNvbSIsInN1YiI6IlUwMDAwMDM3MTg0IiwiaWF0IjoxNzU4MjQzNjY0LCJhdWQiOiI5OTUzZjA3YS01ZGU0LTQwOGMtYThhYi02ODhhNjMyMGYwMGYiLCJncmFudF9pZCI6Ikc0ZTc4MjFkYy03NTQyLTQ0NTQtODBkMy1iYjU3NGEwMGRkYWMiLCJzY29wZSI6InJlYWQgdHJhZGUifQ.ZsP51rUGQIXsP-cU0OCa-45AwwMp18YxOrT_mrrocClhRL7bfWctX8GOJ35Nn_E48WOuQPxUF3KMhSn1tkqLBQ'
18
+ }
19
+
8
20
  // Sessions: Allows an API client to interact with their session, or create a new one.
9
21
  async login(usernameOrEmail: string, password: string, rememberMe = false) {
10
22
  // Create a new user session.
@@ -30,6 +42,6 @@ export default class SessionService {
30
42
  async logout(){
31
43
  const response = await this.httpClient.deleteData('/sessions', {});// added this for the integration tests?
32
44
  this.httpClient.session.clear()
33
- return extractResponseData(response);
45
+ return { status: response.status };
34
46
  }
35
47
  }
@@ -1,8 +1,11 @@
1
1
  import TastytradeSession from "../models/tastytrade-session.js"
2
+ import AccessToken from "../models/access-token.js"
2
3
  import axios from "axios"
3
4
  import qs from 'qs'
4
5
  import { recursiveDasherizeKeys } from "../utils/json-util.js"
5
6
  import _ from 'lodash'
7
+ import type Logger from "../logger.js"
8
+ import type { ClientConfig } from "../tastytrade-api.js"
6
9
 
7
10
  const ParamsSerializer = {
8
11
  serialize: function (queryParams: object) {
@@ -10,20 +13,65 @@ const ParamsSerializer = {
10
13
  }
11
14
  }
12
15
 
13
- export default class TastytradeHttpClient{
16
+ const ApiVersionRegex = /^\d{8}$/
17
+
18
+ export default class TastytradeHttpClient {
19
+ private readonly logger?: Logger
20
+ public baseUrl: string
21
+ public clientSecret?: string
22
+ public refreshToken?: string
23
+ public oauthScopes?: string[]
24
+ public readonly accessToken: AccessToken
14
25
  public readonly session: TastytradeSession
26
+ private _targetApiVersion?: string
15
27
 
16
- constructor(private readonly baseUrl: string) {
28
+ constructor(clientConfig: Partial<ClientConfig>, logger?: Logger) {
29
+ this.logger = logger
30
+ this.baseUrl = clientConfig.baseUrl!
31
+ this.accessToken = new AccessToken()
17
32
  this.session = new TastytradeSession()
33
+ this.updateConfig(clientConfig)
34
+ }
35
+
36
+ public updateConfig(config: Partial<ClientConfig>) {
37
+ const httpClientConfig = _.pick(config, ['clientSecret', 'refreshToken', 'oauthScopes', 'targetApiVersion'])
38
+ if (!_.isEmpty(httpClientConfig)) {
39
+ Object.assign(this, httpClientConfig)
40
+ this.accessToken.clear()
41
+ }
42
+ }
43
+
44
+ get needsTokenRefresh(): boolean {
45
+ if (this.session.isValid) {
46
+ return false
47
+ }
48
+ if (_.isNil(this.refreshToken) || _.isNil(this.clientSecret)) {
49
+ return false
50
+ }
51
+ return this.accessToken.isExpired
52
+ }
53
+
54
+ get authHeader(): string | null {
55
+ if (this.session.isValid) {
56
+ return this.session.authToken
57
+ }
58
+ if (this.accessToken.isValid) {
59
+ return this.accessToken.authorizationHeader
60
+ }
61
+ return null
18
62
  }
19
63
 
20
64
  private getDefaultHeaders(): any {
21
65
  const headers: { [key: string]: any } = {
22
66
  "Content-Type": "application/json",
23
67
  "Accept": "application/json",
24
- "Authorization": this.session.authToken
68
+ "Authorization": this.authHeader
25
69
  };
26
70
 
71
+ if (!_.isNil(this.targetApiVersion)) {
72
+ headers["Accept-Version"] = this.targetApiVersion
73
+ }
74
+
27
75
  // Only set user agent if running in node
28
76
  if (typeof window === 'undefined') {
29
77
  headers["User-Agent"] = 'tastytrade-sdk-js'
@@ -32,21 +80,44 @@ export default class TastytradeHttpClient{
32
80
  return headers
33
81
  }
34
82
 
35
- private async executeRequest(method: string, url: string, data: object = {}, headers: object = {}, params: object = {}) {
36
- const dasherizedParams = recursiveDasherizeKeys(params)
37
- const dasherizedData = recursiveDasherizeKeys(data)
38
- const mergedHeaders = { ...headers, ...this.getDefaultHeaders() }
83
+ private axiosConfig(method: string, url: string, data: object = {}, headers: object = {}, params: object = {}): any {
84
+ return _.omitBy(
85
+ { method, url, baseURL: this.baseUrl, data, headers, params, paramsSerializer: ParamsSerializer},
86
+ _.isEmpty
87
+ )
88
+ }
89
+
90
+ public async generateAccessToken(): Promise<any> {
91
+ if (_.isNil(this.refreshToken) || _.isNil(this.clientSecret) || _.isNil(this.oauthScopes)) {
92
+ throw new Error('Missing required parameters to generate access token (refreshToken, clientSecret, oauthScopes)')
93
+ }
94
+ const params = {
95
+ refresh_token: this.refreshToken,
96
+ client_secret: this.clientSecret,
97
+ scope: this.oauthScopes!.join(' '),
98
+ grant_type: 'refresh_token'
99
+ }
100
+
101
+ const config = this.axiosConfig('post', '/oauth/token', params)
102
+ this.logger?.info('Making request', config)
103
+ const tokenResponse = await axios.request(config)
104
+ this.accessToken.updateFromTokenResponse(tokenResponse)
105
+ return this.accessToken
106
+ }
107
+
108
+ private async executeRequest(method: string, url: string, data: object = {}, headers: object = {}, params: object = {}): Promise<any> {
109
+ if (this.needsTokenRefresh) {
110
+ await this.generateAccessToken()
111
+ }
112
+ let dasherizedParams = params
113
+ let dasherizedData = data
114
+ dasherizedParams = recursiveDasherizeKeys(params)
115
+ dasherizedData = recursiveDasherizeKeys(data)
39
116
 
40
- const config = _.omitBy({
41
- method,
42
- url,
43
- baseURL: this.baseUrl,
44
- data: dasherizedData,
45
- headers: mergedHeaders,
46
- params: dasherizedParams,
47
- paramsSerializer: ParamsSerializer
48
- }, _.isEmpty)
117
+ const mergedHeaders = { ...headers, ...this.getDefaultHeaders() }
49
118
 
119
+ const config = this.axiosConfig(method, url, dasherizedData, mergedHeaders, dasherizedParams)
120
+ this.logger?.info('Making request', config)
50
121
  return axios.request(config)
51
122
  }
52
123
 
@@ -69,4 +140,15 @@ export default class TastytradeHttpClient{
69
140
  async deleteData(url: string, headers: object): Promise<any> {
70
141
  return this.executeRequest('delete', url, headers);
71
142
  }
143
+
144
+ public get targetApiVersion(): string | undefined {
145
+ return this._targetApiVersion
146
+ }
147
+
148
+ public set targetApiVersion(version: string | undefined) {
149
+ if (!_.isNil(version) && !ApiVersionRegex.test(version)) {
150
+ throw new Error('Invalid API version format. Expected YYYYMMDD.')
151
+ }
152
+ this._targetApiVersion = version
153
+ }
72
154
  }
@@ -1,6 +1,6 @@
1
1
  import TastytradeHttpClient from "./services/tastytrade-http-client.js"
2
2
  import { AccountStreamer, STREAMER_STATE, type Disposer, type StreamerStateObserver } from './account-streamer.js'
3
- import MarketDataStreamer, { type CandleSubscriptionOptions, CandleType, MarketDataSubscriptionType, type MarketDataListener } from "./market-data-streamer.js"
3
+ import _ from 'lodash'
4
4
 
5
5
  //Services:
6
6
  import SessionService from "./services/session-service.js"
@@ -17,11 +17,36 @@ import SymbolSearchService from "./services/symbol-search-service.js"
17
17
  import TransactionsService from "./services/transactions-service.js"
18
18
  import WatchlistsService from "./services/watchlists-service.js"
19
19
  import TastytradeSession from "./models/tastytrade-session.js"
20
+ import type Logger from "./logger.js"
21
+ import { TastytradeLogger, LogLevel } from "./logger.js"
22
+ import QuoteStreamer, { MarketDataSubscriptionType, CandleType } from "./quote-streamer.js"
23
+ import type AccessToken from "./models/access-token.js"
24
+
25
+ export type ClientConfig = {
26
+ baseUrl: string,
27
+ accountStreamerUrl: string,
28
+ clientSecret?: string,
29
+ refreshToken?: string,
30
+ oauthScopes?: string[],
31
+ logger?: Logger,
32
+ logLevel?: LogLevel
33
+ targetApiVersion?: string
34
+ }
20
35
 
21
36
  export default class TastytradeClient {
37
+ public static readonly ProdConfig: ClientConfig = {
38
+ baseUrl: 'https://api.tastyworks.com',
39
+ accountStreamerUrl: 'wss://streamer.tastyworks.com',
40
+ }
41
+ public static readonly SandboxConfig: ClientConfig = {
42
+ baseUrl: 'https://api.cert.tastyworks.com',
43
+ accountStreamerUrl: 'wss://streamer.cert.tastyworks.com',
44
+ }
45
+ public readonly logger: TastytradeLogger
22
46
  public readonly httpClient: TastytradeHttpClient
23
47
 
24
48
  public readonly accountStreamer: AccountStreamer
49
+ public readonly quoteStreamer: QuoteStreamer
25
50
 
26
51
  public readonly sessionService: SessionService
27
52
  public readonly accountStatusService: AccountStatusService
@@ -37,9 +62,9 @@ export default class TastytradeClient {
37
62
  public readonly transactionsService: TransactionsService
38
63
  public readonly watchlistsService: WatchlistsService
39
64
 
40
- constructor(readonly baseUrl: string, readonly accountStreamerUrl: string) {
41
- this.httpClient = new TastytradeHttpClient(baseUrl)
42
- this.accountStreamer = new AccountStreamer(accountStreamerUrl, this.session)
65
+ constructor(config: ClientConfig) {
66
+ this.logger = new TastytradeLogger(config.logger, config.logLevel)
67
+ this.httpClient = new TastytradeHttpClient(config, this.logger)
43
68
 
44
69
  this.sessionService = new SessionService(this.httpClient)
45
70
  this.accountStatusService = new AccountStatusService(this.httpClient)
@@ -54,12 +79,27 @@ export default class TastytradeClient {
54
79
  this.symbolSearchService = new SymbolSearchService(this.httpClient)
55
80
  this.transactionsService = new TransactionsService(this.httpClient)
56
81
  this.watchlistsService = new WatchlistsService(this.httpClient)
82
+
83
+
84
+ this.accountStreamer = new AccountStreamer(config.accountStreamerUrl, this.session, this.accessToken, this.logger)
85
+ this.quoteStreamer = new QuoteStreamer(this.accountsAndCustomersService, this.logger)
86
+ }
87
+
88
+ public updateConfig(config: Partial<ClientConfig>) {
89
+ this.httpClient.updateConfig(config)
90
+ this.logger.updateConfig(config)
57
91
  }
58
92
 
59
93
  get session(): TastytradeSession {
60
94
  return this.httpClient.session
61
95
  }
96
+
97
+ get accessToken(): AccessToken {
98
+ return this.httpClient.accessToken
99
+ }
62
100
  }
63
101
 
64
- export { MarketDataStreamer, MarketDataSubscriptionType, type MarketDataListener, type CandleSubscriptionOptions, CandleType }
102
+ export { MarketDataSubscriptionType, CandleType }
65
103
  export { AccountStreamer, STREAMER_STATE, type Disposer, type StreamerStateObserver }
104
+ export { TastytradeLogger, LogLevel }
105
+ export type { Logger }
@@ -20,11 +20,9 @@ export class JsonBuilder {
20
20
  }
21
21
  }
22
22
 
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
23
  export function recursiveDasherizeKeys(body: any) {
25
24
  let dasherized = _.mapKeys(body, (_value, key) => dasherize(key))
26
25
 
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
26
  dasherized = _.mapValues(dasherized, (value: any) => {
29
27
  if (_.isPlainObject(value)) {
30
28
  return recursiveDasherizeKeys(value)
@@ -1,12 +1,11 @@
1
1
  import _ from 'lodash'
2
2
 
3
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
- export default function extractResponseData(httpResponse: any){
3
+ export default function extractResponseData(httpResponse: any) {
5
4
  if (_.has(httpResponse, 'data.data.items')) {
6
5
  return _.get(httpResponse, 'data.data.items')
7
- } else if (_.has(httpResponse, 'data.data')){
6
+ } else if (_.has(httpResponse, 'data.data')) {
8
7
  return _.get(httpResponse, 'data.data')
9
- }else{
8
+ } else {
10
9
  return httpResponse
11
10
  }
12
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastytrade/api",
3
- "version": "4.0.0",
3
+ "version": "6.0.0",
4
4
  "type": "module",
5
5
  "module": "dist/tastytrade-api.js",
6
6
  "types": "dist/tastytrade-api.d.ts",
@@ -15,37 +15,45 @@
15
15
  "npm": ">=9.0.0",
16
16
  "node": ">=20.0.0"
17
17
  },
18
+ "prettier": {
19
+ "semi": false,
20
+ "singleQuote": true,
21
+ "trailingComma": "none"
22
+ },
18
23
  "scripts": {
19
24
  "build": "tsc -p tsconfig.json",
20
25
  "test": "jest -i --restoreMocks",
21
26
  "unit-test": "node --experimental-vm-modules ./node_modules/.bin/jest tests/unit",
22
27
  "integration-test": "node --experimental-vm-modules ./node_modules/.bin/jest tests/integration",
23
28
  "lint": "eslint lib/** tests/**",
29
+ "format": "prettier --write .",
24
30
  "prepublishOnly": "npm run unit-test && npm run build",
25
31
  "postpack": "git tag -a $npm_package_version -m $npm_package_version && git push origin $npm_package_version"
26
32
  },
27
33
  "dependencies": {
28
- "@types/lodash": "^4.14.182",
29
- "@types/qs": "^6.9.7",
30
- "axios": "^1.3.4",
34
+ "@dxfeed/dxlink-api": "^0.3.0",
35
+ "@types/lodash": "^4.17.16",
36
+ "@types/qs": "^6.9.18",
37
+ "axios": "^1.9.0",
31
38
  "isomorphic-ws": "^5.0.0",
32
39
  "lodash": "^4.17.21",
33
- "qs": "^6.11.1",
34
- "uuid": "^9.0.0",
35
- "ws": "^8.13.0"
40
+ "qs": "^6.14.0",
41
+ "uuid": "^11.1.0",
42
+ "ws": "^8.18.2"
36
43
  },
37
44
  "devDependencies": {
38
- "@types/jest": "^29.5.0",
39
- "@types/node": "20.9.0",
40
- "@types/uuid": "^9.0.2",
41
- "@types/ws": "^8.5.9",
42
- "@typescript-eslint/eslint-plugin": "^5.57.1",
43
- "@typescript-eslint/parser": "^5.57.1",
44
- "dotenv": "^16.0.3",
45
- "eslint": "^8.14.0",
45
+ "@types/jest": "^29.5.14",
46
+ "@types/node": "22.15.14",
47
+ "@types/uuid": "^10.0.0",
48
+ "@types/ws": "^8.18.1",
49
+ "@typescript-eslint/eslint-plugin": "^8.32.0",
50
+ "@typescript-eslint/parser": "^8.32.0",
51
+ "dotenv": "^16.5.0",
52
+ "eslint": "^9.26.0",
46
53
  "jest": "^29.7.0",
47
- "nock": "^13.5.4",
48
- "ts-jest": "^29.1.2",
49
- "typescript": "^5.4.2"
54
+ "nock": "^14.0.4",
55
+ "prettier": "^3.5.3",
56
+ "ts-jest": "^29.3.2",
57
+ "typescript": "^5.8.3"
50
58
  }
51
59
  }
package/tsconfig.json CHANGED
@@ -41,14 +41,14 @@
41
41
  // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42
42
 
43
43
  /* Module Resolution Options */
44
- // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
44
+ "moduleResolution": "nodenext", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45
45
  // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46
46
  // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47
47
  // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48
48
  // "typeRoots": [], /* List of folders to include type definitions from. */
49
49
  // "types": [], /* Type declaration files to be included in compilation. */
50
50
  // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51
- // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
51
+ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
52
52
  // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53
53
  // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54
54
 
@@ -65,7 +65,7 @@
65
65
  /* Advanced Options */
66
66
  "skipLibCheck": true, /* Skip type checking of declaration files. */
67
67
  "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
68
- "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
68
+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
69
69
  },
70
70
  "include": ["lib/**/*"],
71
71
  "exclude": ["node_modules", "dist", "tests/**/*", "examples", ".env", ".env.sample"]