@tastytrade/api 5.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.
- package/CHANGELOG.md +29 -0
- package/README.md +87 -43
- package/dist/account-streamer.d.ts +6 -3
- package/dist/account-streamer.d.ts.map +1 -1
- package/dist/account-streamer.js +21 -15
- package/dist/account-streamer.js.map +1 -1
- package/dist/logger.d.ts +5 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/market-data-streamer.d.ts.map +1 -1
- package/dist/market-data-streamer.js +1 -0
- package/dist/market-data-streamer.js.map +1 -1
- package/dist/models/access-token.d.ts +12 -0
- package/dist/models/access-token.d.ts.map +1 -0
- package/dist/models/access-token.js +35 -0
- package/dist/models/access-token.js.map +1 -0
- package/dist/models/tastytrade-session.d.ts.map +1 -1
- package/dist/models/tastytrade-session.js.map +1 -1
- package/dist/quote-streamer.d.ts +58 -0
- package/dist/quote-streamer.d.ts.map +1 -0
- package/dist/quote-streamer.js +132 -0
- package/dist/quote-streamer.js.map +1 -0
- package/dist/services/account-status-service.js +0 -1
- package/dist/services/account-status-service.js.map +1 -1
- package/dist/services/accounts-and-customers-service.js +0 -1
- package/dist/services/accounts-and-customers-service.js.map +1 -1
- package/dist/services/balances-and-positions-service.js +0 -1
- package/dist/services/balances-and-positions-service.js.map +1 -1
- package/dist/services/instruments-service.js +1 -2
- package/dist/services/instruments-service.js.map +1 -1
- package/dist/services/margin-requirements-service.js +0 -1
- package/dist/services/margin-requirements-service.js.map +1 -1
- package/dist/services/market-metrics-service.js +0 -1
- package/dist/services/market-metrics-service.js.map +1 -1
- package/dist/services/net-liquidating-value-history-service.js +0 -1
- package/dist/services/net-liquidating-value-history-service.js.map +1 -1
- package/dist/services/orders-service.js +0 -1
- package/dist/services/orders-service.js.map +1 -1
- package/dist/services/risk-parameters-service.js +0 -1
- package/dist/services/risk-parameters-service.js.map +1 -1
- package/dist/services/session-service.d.ts +6 -1
- package/dist/services/session-service.d.ts.map +1 -1
- package/dist/services/session-service.js +10 -2
- package/dist/services/session-service.js.map +1 -1
- package/dist/services/symbol-search-service.js +0 -1
- package/dist/services/symbol-search-service.js.map +1 -1
- package/dist/services/tastytrade-http-client.d.ts +16 -2
- package/dist/services/tastytrade-http-client.d.ts.map +1 -1
- package/dist/services/tastytrade-http-client.js +71 -14
- package/dist/services/tastytrade-http-client.js.map +1 -1
- package/dist/services/transactions-service.js +0 -1
- package/dist/services/transactions-service.js.map +1 -1
- package/dist/services/watchlists-service.js +0 -1
- package/dist/services/watchlists-service.js.map +1 -1
- package/dist/tastytrade-api.d.ts +12 -2
- package/dist/tastytrade-api.d.ts.map +1 -1
- package/dist/tastytrade-api.js +22 -6
- package/dist/tastytrade-api.js.map +1 -1
- package/dist/utils/json-util.d.ts.map +1 -1
- package/dist/utils/json-util.js +0 -2
- package/dist/utils/json-util.js.map +1 -1
- package/dist/utils/response-util.d.ts.map +1 -1
- package/dist/utils/response-util.js +0 -1
- package/dist/utils/response-util.js.map +1 -1
- package/eslint.config.cjs +46 -0
- package/lib/account-streamer.ts +31 -23
- package/lib/logger.ts +6 -1
- package/lib/models/access-token.ts +40 -0
- package/lib/models/tastytrade-session.ts +1 -1
- package/lib/quote-streamer.ts +151 -0
- package/lib/services/instruments-service.ts +1 -1
- package/lib/services/session-service.ts +13 -1
- package/lib/services/tastytrade-http-client.ts +93 -15
- package/lib/tastytrade-api.ts +31 -4
- package/lib/utils/json-util.ts +0 -2
- package/lib/utils/response-util.ts +3 -4
- package/package.json +26 -18
- package/tsconfig.json +2 -2
- package/.eslintrc.cjs +0 -18
- package/lib/market-data-streamer.ts +0 -376
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
import WebSocket from 'isomorphic-ws'
|
|
2
|
-
import _ from 'lodash'
|
|
3
|
-
import { v4 as uuidv4 } from 'uuid'
|
|
4
|
-
import { MinTlsVersion } from './utils/constants.js'
|
|
5
|
-
|
|
6
|
-
export enum MarketDataSubscriptionType {
|
|
7
|
-
Candle = 'Candle',
|
|
8
|
-
Quote = 'Quote',
|
|
9
|
-
Trade = 'Trade',
|
|
10
|
-
Summary = 'Summary',
|
|
11
|
-
Profile = 'Profile',
|
|
12
|
-
Greeks = 'Greeks',
|
|
13
|
-
Underlying = 'Underlying'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export enum CandleType {
|
|
17
|
-
Tick = 't',
|
|
18
|
-
Second = 's',
|
|
19
|
-
Minute = 'm',
|
|
20
|
-
Hour = 'h',
|
|
21
|
-
Day = 'd',
|
|
22
|
-
Week = 'w',
|
|
23
|
-
Month = 'mo',
|
|
24
|
-
ThirdFriday = 'o',
|
|
25
|
-
Year = 'y',
|
|
26
|
-
Volume = 'v',
|
|
27
|
-
Price = 'p'
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type MarketDataListener = (data: any) => void
|
|
31
|
-
export type ErrorListener = (error: any) => void
|
|
32
|
-
export type AuthStateListener = (isAuthorized: boolean) => void
|
|
33
|
-
|
|
34
|
-
type QueuedSubscription = { symbol: string, subscriptionTypes: MarketDataSubscriptionType[], subscriptionArgs?: any }
|
|
35
|
-
type SubscriptionOptions = { subscriptionTypes: MarketDataSubscriptionType[], channelId: number, subscriptionArgs?: any }
|
|
36
|
-
export type CandleSubscriptionOptions = { period: number, type: CandleType, channelId: number }
|
|
37
|
-
type Remover = () => void
|
|
38
|
-
|
|
39
|
-
// List of all subscription types except for Candle
|
|
40
|
-
const AllSubscriptionTypes = Object.values(MarketDataSubscriptionType)
|
|
41
|
-
|
|
42
|
-
const KeepaliveInterval = 30000 // 30 seconds
|
|
43
|
-
|
|
44
|
-
const DefaultChannelId = 1
|
|
45
|
-
|
|
46
|
-
export default class MarketDataStreamer {
|
|
47
|
-
private webSocket: WebSocket | null = null
|
|
48
|
-
private token = ''
|
|
49
|
-
private keepaliveIntervalId: any | null = null
|
|
50
|
-
private dataListeners = new Map()
|
|
51
|
-
private openChannels = new Set()
|
|
52
|
-
private subscriptionsQueue: Map<number, QueuedSubscription[]> = new Map()
|
|
53
|
-
private authState = ''
|
|
54
|
-
private errorListeners = new Map()
|
|
55
|
-
private authStateListeners = new Map()
|
|
56
|
-
|
|
57
|
-
constructor() {
|
|
58
|
-
console.warn('MarketDataStreamer is deprecated and will be removed in a future release of @tastytrade/api. Use @dxfeed/dxlink-api instead.')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
addDataListener(dataListener: MarketDataListener, channelId: number | null = null): Remover {
|
|
62
|
-
if (_.isNil(dataListener)) {
|
|
63
|
-
return _.noop
|
|
64
|
-
}
|
|
65
|
-
const guid = uuidv4()
|
|
66
|
-
this.dataListeners.set(guid, { listener: dataListener, channelId })
|
|
67
|
-
|
|
68
|
-
return () => this.dataListeners.delete(guid)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
addErrorListener(errorListener: ErrorListener): Remover {
|
|
72
|
-
if (_.isNil(errorListener)) {
|
|
73
|
-
return _.noop
|
|
74
|
-
}
|
|
75
|
-
const guid = uuidv4()
|
|
76
|
-
this.errorListeners.set(guid, errorListener)
|
|
77
|
-
|
|
78
|
-
return () => this.errorListeners.delete(guid)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
addAuthStateChangeListener(authStateListener: AuthStateListener): Remover {
|
|
82
|
-
if (_.isNil(authStateListener)) {
|
|
83
|
-
return _.noop
|
|
84
|
-
}
|
|
85
|
-
const guid = uuidv4()
|
|
86
|
-
this.authStateListeners.set(guid, authStateListener)
|
|
87
|
-
|
|
88
|
-
return () => this.authStateListeners.delete(guid)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
connect(url: string, token: string) {
|
|
92
|
-
if (this.isConnected) {
|
|
93
|
-
throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected')
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
this.token = token
|
|
97
|
-
this.webSocket = new WebSocket(url, [], {
|
|
98
|
-
minVersion: MinTlsVersion // TLS Config
|
|
99
|
-
})
|
|
100
|
-
this.webSocket.onopen = this.onOpen.bind(this)
|
|
101
|
-
this.webSocket.onerror = this.onError.bind(this)
|
|
102
|
-
this.webSocket.onmessage = this.handleMessageReceived.bind(this)
|
|
103
|
-
this.webSocket.onclose = this.onClose.bind(this)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
disconnect() {
|
|
107
|
-
if (_.isNil(this.webSocket)) {
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.clearKeepalive()
|
|
112
|
-
|
|
113
|
-
this.webSocket.onopen = null
|
|
114
|
-
this.webSocket.onerror = null
|
|
115
|
-
this.webSocket.onmessage = null
|
|
116
|
-
this.webSocket.onclose = null
|
|
117
|
-
|
|
118
|
-
this.webSocket.close()
|
|
119
|
-
this.webSocket = null
|
|
120
|
-
|
|
121
|
-
this.openChannels.clear()
|
|
122
|
-
this.subscriptionsQueue.clear()
|
|
123
|
-
this.authState = ''
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
addSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }): Remover {
|
|
127
|
-
let { subscriptionTypes } = options
|
|
128
|
-
// Don't allow candle subscriptions in this method. Use addCandleSubscription instead
|
|
129
|
-
subscriptionTypes = _.without(subscriptionTypes, MarketDataSubscriptionType.Candle)
|
|
130
|
-
|
|
131
|
-
const isOpen = this.isChannelOpened(options.channelId)
|
|
132
|
-
if (isOpen) {
|
|
133
|
-
this.sendSubscriptionMessage(symbol, subscriptionTypes, options.channelId, 'add')
|
|
134
|
-
} else {
|
|
135
|
-
this.queueSubscription(symbol, { subscriptionTypes, channelId: options.channelId })
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return () => {
|
|
139
|
-
this.removeSubscription(symbol, options)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Adds a candle subscription (historical data)
|
|
145
|
-
* @param streamerSymbol Get this from an instrument's streamer-symbol json response field
|
|
146
|
-
* @param fromTime Epoch timestamp from where you want to start
|
|
147
|
-
* @param options Period and Type are the grouping you want to apply to the candle data
|
|
148
|
-
* For example, a period/type of 5/m means you want each candle to represent 5 minutes of data
|
|
149
|
-
* From there, setting fromTime to 24 hours ago would give you 24 hours of data grouped in 5 minute intervals
|
|
150
|
-
* @returns
|
|
151
|
-
*/
|
|
152
|
-
addCandleSubscription(streamerSymbol: string, fromTime: number, options: CandleSubscriptionOptions) {
|
|
153
|
-
const subscriptionTypes = [MarketDataSubscriptionType.Candle]
|
|
154
|
-
const channelId = options.channelId ?? DefaultChannelId
|
|
155
|
-
|
|
156
|
-
// Example: AAPL{=5m} where each candle represents 5 minutes of data
|
|
157
|
-
const candleSymbol = `${streamerSymbol}{=${options.period}${options.type}}`
|
|
158
|
-
const isOpen = this.isChannelOpened(channelId)
|
|
159
|
-
const subscriptionArgs = { fromTime }
|
|
160
|
-
if (isOpen) {
|
|
161
|
-
this.sendSubscriptionMessage(candleSymbol, subscriptionTypes, channelId, 'add', subscriptionArgs)
|
|
162
|
-
} else {
|
|
163
|
-
this.queueSubscription(candleSymbol, { subscriptionTypes, channelId, subscriptionArgs })
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
this.removeSubscription(candleSymbol, { subscriptionTypes, channelId })
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
removeSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }) {
|
|
172
|
-
const { subscriptionTypes, channelId } = options
|
|
173
|
-
const isOpen = this.isChannelOpened(channelId)
|
|
174
|
-
if (isOpen) {
|
|
175
|
-
this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'remove')
|
|
176
|
-
} else {
|
|
177
|
-
this.dequeueSubscription(symbol, options)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
removeAllSubscriptions(channelId = DefaultChannelId) {
|
|
182
|
-
const isOpen = this.isChannelOpened(channelId)
|
|
183
|
-
if (isOpen) {
|
|
184
|
-
this.sendMessage({ "type": "FEED_SUBSCRIPTION", "channel": channelId, reset: true })
|
|
185
|
-
} else {
|
|
186
|
-
this.subscriptionsQueue.set(channelId, [])
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
openFeedChannel(channelId: number) {
|
|
191
|
-
if (!this.isReadyToOpenChannels) {
|
|
192
|
-
throw new Error(`Unable to open channel ${channelId} due to DxLink authorization state: ${this.authState}`)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (this.isChannelOpened(channelId)) {
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
this.sendMessage({
|
|
200
|
-
"type": "CHANNEL_REQUEST",
|
|
201
|
-
"channel": channelId,
|
|
202
|
-
"service": "FEED",
|
|
203
|
-
"parameters": {
|
|
204
|
-
"contract": "AUTO"
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
isChannelOpened(channelId: number) {
|
|
210
|
-
return this.isConnected && this.openChannels.has(channelId)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
get isReadyToOpenChannels() {
|
|
214
|
-
return this.isConnected && this.isDxLinkAuthorized
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
get isConnected() {
|
|
218
|
-
return !_.isNil(this.webSocket)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private scheduleKeepalive() {
|
|
222
|
-
this.keepaliveIntervalId = setInterval(this.sendKeepalive, KeepaliveInterval)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private sendKeepalive() {
|
|
226
|
-
if (_.isNil(this.keepaliveIntervalId)) {
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
this.sendMessage({
|
|
231
|
-
"type": "KEEPALIVE",
|
|
232
|
-
"channel": 0
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private queueSubscription(symbol: string, options: SubscriptionOptions) {
|
|
237
|
-
const { subscriptionTypes, channelId, subscriptionArgs } = options
|
|
238
|
-
let queue = this.subscriptionsQueue.get(options.channelId)
|
|
239
|
-
if (_.isNil(queue)) {
|
|
240
|
-
queue = []
|
|
241
|
-
this.subscriptionsQueue.set(channelId, queue)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
queue.push({ symbol, subscriptionTypes, subscriptionArgs })
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private dequeueSubscription(symbol: string, options: SubscriptionOptions) {
|
|
248
|
-
const queue = this.subscriptionsQueue.get(options.channelId)
|
|
249
|
-
if (_.isNil(queue) || _.isEmpty(queue)) {
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
_.remove(queue, (queueItem: any) => queueItem.symbol === symbol)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private sendQueuedSubscriptions(channelId: number) {
|
|
257
|
-
const queuedSubscriptions = this.subscriptionsQueue.get(channelId)
|
|
258
|
-
if (_.isNil(queuedSubscriptions)) {
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Clear out queue immediately
|
|
263
|
-
this.subscriptionsQueue.set(channelId, [])
|
|
264
|
-
queuedSubscriptions.forEach(subscription => {
|
|
265
|
-
this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add', subscription.subscriptionArgs)
|
|
266
|
-
})
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
*
|
|
271
|
-
* @param {*} symbol
|
|
272
|
-
* @param {*} subscriptionTypes
|
|
273
|
-
* @param {*} channelId
|
|
274
|
-
* @param {*} direction add or remove
|
|
275
|
-
*/
|
|
276
|
-
private sendSubscriptionMessage(symbol: string, subscriptionTypes: MarketDataSubscriptionType[], channelId: number, direction: string, subscriptionArgs: any = {}) {
|
|
277
|
-
const subscriptions = subscriptionTypes.map(type => (Object.assign({}, { "symbol": symbol, "type": type }, subscriptionArgs ?? {})))
|
|
278
|
-
this.sendMessage({
|
|
279
|
-
"type": "FEED_SUBSCRIPTION",
|
|
280
|
-
"channel": channelId,
|
|
281
|
-
[direction]: subscriptions
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private onError(error: any) {
|
|
286
|
-
console.error('Error received: ', error)
|
|
287
|
-
this.notifyErrorListeners(error)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private onOpen() {
|
|
291
|
-
this.openChannels.clear()
|
|
292
|
-
|
|
293
|
-
this.sendMessage({
|
|
294
|
-
"type": "SETUP",
|
|
295
|
-
"channel": 0,
|
|
296
|
-
"keepaliveTimeout": KeepaliveInterval,
|
|
297
|
-
"acceptKeepaliveTimeout": KeepaliveInterval,
|
|
298
|
-
"version": "0.1-js/1.0.0"
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
this.scheduleKeepalive()
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private onClose() {
|
|
305
|
-
this.webSocket = null
|
|
306
|
-
this.clearKeepalive()
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private clearKeepalive() {
|
|
310
|
-
if (!_.isNil(this.keepaliveIntervalId)) {
|
|
311
|
-
clearInterval(this.keepaliveIntervalId)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
this.keepaliveIntervalId = null
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
get isDxLinkAuthorized() {
|
|
318
|
-
return this.authState === 'AUTHORIZED'
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private handleAuthStateMessage(data: any) {
|
|
322
|
-
this.authState = data.state
|
|
323
|
-
this.authStateListeners.forEach(listener => listener(this.isDxLinkAuthorized))
|
|
324
|
-
|
|
325
|
-
if (this.isDxLinkAuthorized) {
|
|
326
|
-
this.openFeedChannel(DefaultChannelId)
|
|
327
|
-
} else {
|
|
328
|
-
this.sendMessage({
|
|
329
|
-
"type": "AUTH",
|
|
330
|
-
"channel": 0,
|
|
331
|
-
"token": this.token
|
|
332
|
-
})
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
private handleChannelOpened(jsonData: any) {
|
|
337
|
-
this.openChannels.add(jsonData.channel)
|
|
338
|
-
this.sendQueuedSubscriptions(jsonData.channel)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
private notifyListeners(jsonData: any) {
|
|
342
|
-
this.dataListeners.forEach(listenerData => {
|
|
343
|
-
if (listenerData.channelId === jsonData.channel || _.isNil(listenerData.channelId)) {
|
|
344
|
-
listenerData.listener(jsonData)
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private notifyErrorListeners(error: any) {
|
|
350
|
-
this.errorListeners.forEach(listener => listener(error))
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private handleMessageReceived(data: WebSocket.MessageEvent) {
|
|
354
|
-
const messageData = _.get(data, 'data', '{}')
|
|
355
|
-
const jsonData = JSON.parse(messageData as string)
|
|
356
|
-
switch (jsonData.type) {
|
|
357
|
-
case 'AUTH_STATE':
|
|
358
|
-
this.handleAuthStateMessage(jsonData)
|
|
359
|
-
break
|
|
360
|
-
case 'CHANNEL_OPENED':
|
|
361
|
-
this.handleChannelOpened(jsonData)
|
|
362
|
-
break
|
|
363
|
-
case 'FEED_DATA':
|
|
364
|
-
this.notifyListeners(jsonData)
|
|
365
|
-
break
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private sendMessage(json: object) {
|
|
370
|
-
if (_.isNil(this.webSocket)) {
|
|
371
|
-
return
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
this.webSocket.send(JSON.stringify(json))
|
|
375
|
-
}
|
|
376
|
-
}
|