@fullstackcraftllc/floe 0.0.8 → 0.0.10
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/README.md +1 -1
- package/dist/client/FloeClient.js +1 -1
- package/dist/client/brokers/BaseBrokerClient.d.ts +296 -0
- package/dist/client/brokers/BaseBrokerClient.js +509 -0
- package/dist/client/brokers/SchwabClient.d.ts +11 -128
- package/dist/client/brokers/SchwabClient.js +37 -246
- package/dist/client/brokers/TastyTradeClient.d.ts +17 -120
- package/dist/client/brokers/TastyTradeClient.js +35 -319
- package/dist/client/brokers/TradeStationClient.d.ts +31 -128
- package/dist/client/brokers/TradeStationClient.js +37 -201
- package/dist/client/brokers/TradierClient.d.ts +16 -196
- package/dist/client/brokers/TradierClient.js +19 -421
- package/package.json +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseBrokerClient, BaseBrokerClientOptions, AggressorSide, IntradayTrade, FlowSummary, BrokerClientEventType, BrokerEventListener } from './BaseBrokerClient';
|
|
2
|
+
export { AggressorSide, IntradayTrade, FlowSummary };
|
|
3
|
+
export type TastyTradeClientEventType = BrokerClientEventType;
|
|
4
|
+
export type TastyTradeEventListener<T> = BrokerEventListener<T>;
|
|
2
5
|
/**
|
|
3
6
|
* TastyTrade option chain item
|
|
4
7
|
*/
|
|
@@ -26,38 +29,14 @@ interface TastyTradeOptionChainItem {
|
|
|
26
29
|
'implied-volatility'?: number;
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
|
-
*
|
|
32
|
+
* TastyTrade client configuration options
|
|
30
33
|
*/
|
|
31
|
-
export
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/** OCC option symbol */
|
|
37
|
-
occSymbol: string;
|
|
38
|
-
/** Trade price */
|
|
39
|
-
price: number;
|
|
40
|
-
/** Trade size (number of contracts) */
|
|
41
|
-
size: number;
|
|
42
|
-
/** Bid at time of trade */
|
|
43
|
-
bid: number;
|
|
44
|
-
/** Ask at time of trade */
|
|
45
|
-
ask: number;
|
|
46
|
-
/** Aggressor side determined from price vs NBBO */
|
|
47
|
-
aggressorSide: AggressorSide;
|
|
48
|
-
/** Timestamp of the trade */
|
|
49
|
-
timestamp: number;
|
|
50
|
-
/** Estimated OI change */
|
|
51
|
-
estimatedOIChange: number;
|
|
34
|
+
export interface TastyTradeClientOptions extends BaseBrokerClientOptions {
|
|
35
|
+
/** TastyTrade session token (required) */
|
|
36
|
+
sessionToken: string;
|
|
37
|
+
/** Whether to use sandbox environment (default: false) */
|
|
38
|
+
sandbox?: boolean;
|
|
52
39
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Event types emitted by TastyTradeClient
|
|
55
|
-
*/
|
|
56
|
-
type TastyTradeClientEventType = 'tickerUpdate' | 'optionUpdate' | 'optionTrade' | 'connected' | 'disconnected' | 'error';
|
|
57
|
-
/**
|
|
58
|
-
* Event listener callback type
|
|
59
|
-
*/
|
|
60
|
-
type TastyTradeEventListener<T> = (data: T) => void;
|
|
61
40
|
/**
|
|
62
41
|
* TastyTradeClient handles real-time streaming connections to the TastyTrade API
|
|
63
42
|
* via DxLink WebSockets.
|
|
@@ -86,7 +65,8 @@ type TastyTradeEventListener<T> = (data: T) => void;
|
|
|
86
65
|
* client.subscribe(['SPY', '.SPXW231215C4500']); // Equity and option
|
|
87
66
|
* ```
|
|
88
67
|
*/
|
|
89
|
-
export declare class TastyTradeClient {
|
|
68
|
+
export declare class TastyTradeClient extends BaseBrokerClient {
|
|
69
|
+
protected readonly brokerName = "TastyTrade";
|
|
90
70
|
/** TastyTrade session token */
|
|
91
71
|
private sessionToken;
|
|
92
72
|
/** DxLink API quote token */
|
|
@@ -103,40 +83,20 @@ export declare class TastyTradeClient {
|
|
|
103
83
|
private feedChannelId;
|
|
104
84
|
/** Feed channel opened */
|
|
105
85
|
private feedChannelOpened;
|
|
106
|
-
/** Currently subscribed symbols */
|
|
107
|
-
private subscribedSymbols;
|
|
108
86
|
/** Map from streamer symbol to OCC symbol */
|
|
109
87
|
private streamerToOccMap;
|
|
110
88
|
/** Map from OCC symbol to streamer symbol */
|
|
111
89
|
private occToStreamerMap;
|
|
112
|
-
/** Cached ticker data */
|
|
113
|
-
private tickerCache;
|
|
114
|
-
/** Cached option data */
|
|
115
|
-
private optionCache;
|
|
116
|
-
/** Base open interest from REST API */
|
|
117
|
-
private baseOpenInterest;
|
|
118
|
-
/** Cumulative estimated OI change from intraday trades */
|
|
119
|
-
private cumulativeOIChange;
|
|
120
|
-
/** History of intraday trades */
|
|
121
|
-
private intradayTrades;
|
|
122
|
-
/** Event listeners */
|
|
123
|
-
private eventListeners;
|
|
124
|
-
/** Reconnection attempt counter */
|
|
125
|
-
private reconnectAttempts;
|
|
126
|
-
/** Maximum reconnection attempts */
|
|
127
|
-
private readonly maxReconnectAttempts;
|
|
128
|
-
/** Reconnection delay in ms */
|
|
129
|
-
private readonly baseReconnectDelay;
|
|
130
90
|
/** Keepalive interval handle */
|
|
131
91
|
private keepaliveInterval;
|
|
92
|
+
/** Whether the first UNAUTHORIZED message has been handled yet - prevents incorrect handling of 'true' UNAUTHORIZED messages */
|
|
93
|
+
private firstUnauthorizedMessageHandled;
|
|
132
94
|
/** Keepalive timeout in seconds */
|
|
133
95
|
private readonly keepaliveTimeoutSeconds;
|
|
134
96
|
/** TastyTrade API base URL */
|
|
135
97
|
private readonly apiBaseUrl;
|
|
136
98
|
/** Whether to use sandbox environment */
|
|
137
99
|
private readonly sandbox;
|
|
138
|
-
/** Whether to log verbose debug information */
|
|
139
|
-
private readonly verbose;
|
|
140
100
|
/**
|
|
141
101
|
* Creates a new TastyTradeClient instance.
|
|
142
102
|
*
|
|
@@ -145,11 +105,7 @@ export declare class TastyTradeClient {
|
|
|
145
105
|
* @param options.sandbox - Whether to use sandbox environment (default: false)
|
|
146
106
|
* @param options.verbose - Whether to log verbose debug information (default: false)
|
|
147
107
|
*/
|
|
148
|
-
constructor(options:
|
|
149
|
-
sessionToken: string;
|
|
150
|
-
sandbox?: boolean;
|
|
151
|
-
verbose?: boolean;
|
|
152
|
-
});
|
|
108
|
+
constructor(options: TastyTradeClientOptions);
|
|
153
109
|
/**
|
|
154
110
|
* Creates a TastyTradeClient by logging in with username/password.
|
|
155
111
|
*
|
|
@@ -222,40 +178,6 @@ export declare class TastyTradeClient {
|
|
|
222
178
|
* @param occSymbols - Array of OCC option symbols to fetch data for
|
|
223
179
|
*/
|
|
224
180
|
fetchOpenInterest(occSymbols: string[]): Promise<void>;
|
|
225
|
-
/**
|
|
226
|
-
* Returns cached option data for a symbol.
|
|
227
|
-
*/
|
|
228
|
-
getOption(occSymbol: string): NormalizedOption | undefined;
|
|
229
|
-
/**
|
|
230
|
-
* Returns all cached options.
|
|
231
|
-
*/
|
|
232
|
-
getAllOptions(): Map<string, NormalizedOption>;
|
|
233
|
-
/**
|
|
234
|
-
* Registers an event listener.
|
|
235
|
-
*/
|
|
236
|
-
on<T>(event: TastyTradeClientEventType, listener: TastyTradeEventListener<T>): this;
|
|
237
|
-
/**
|
|
238
|
-
* Removes an event listener.
|
|
239
|
-
*/
|
|
240
|
-
off<T>(event: TastyTradeClientEventType, listener: TastyTradeEventListener<T>): this;
|
|
241
|
-
/**
|
|
242
|
-
* Returns intraday trades for an option.
|
|
243
|
-
*/
|
|
244
|
-
getIntradayTrades(occSymbol: string): IntradayTrade[];
|
|
245
|
-
/**
|
|
246
|
-
* Returns flow summary for an option.
|
|
247
|
-
*/
|
|
248
|
-
getFlowSummary(occSymbol: string): {
|
|
249
|
-
buyVolume: number;
|
|
250
|
-
sellVolume: number;
|
|
251
|
-
unknownVolume: number;
|
|
252
|
-
netOIChange: number;
|
|
253
|
-
tradeCount: number;
|
|
254
|
-
};
|
|
255
|
-
/**
|
|
256
|
-
* Resets intraday tracking data.
|
|
257
|
-
*/
|
|
258
|
-
resetIntradayData(occSymbols?: string[]): void;
|
|
259
181
|
/**
|
|
260
182
|
* Gets API quote token from TastyTrade.
|
|
261
183
|
*/
|
|
@@ -348,18 +270,6 @@ export declare class TastyTradeClient {
|
|
|
348
270
|
* Updates option from Trade event.
|
|
349
271
|
*/
|
|
350
272
|
private updateOptionFromTrade;
|
|
351
|
-
/**
|
|
352
|
-
* Determines aggressor side from trade price vs NBBO.
|
|
353
|
-
*/
|
|
354
|
-
private determineAggressorSide;
|
|
355
|
-
/**
|
|
356
|
-
* Calculates estimated OI change from trade.
|
|
357
|
-
*/
|
|
358
|
-
private calculateOIChangeFromTrade;
|
|
359
|
-
/**
|
|
360
|
-
* Calculates live open interest.
|
|
361
|
-
*/
|
|
362
|
-
private calculateLiveOpenInterest;
|
|
363
273
|
/**
|
|
364
274
|
* Handles DxLink error messages.
|
|
365
275
|
*/
|
|
@@ -369,24 +279,11 @@ export declare class TastyTradeClient {
|
|
|
369
279
|
*/
|
|
370
280
|
private attemptReconnect;
|
|
371
281
|
/**
|
|
372
|
-
* Checks if symbol is
|
|
282
|
+
* Checks if symbol is a TastyTrade option symbol.
|
|
373
283
|
*/
|
|
374
|
-
private
|
|
284
|
+
private isTastyTradeOptionSymbol;
|
|
375
285
|
/**
|
|
376
286
|
* Sends a message to the WebSocket.
|
|
377
287
|
*/
|
|
378
288
|
private sendMessage;
|
|
379
|
-
/**
|
|
380
|
-
* Emits an event to all listeners.
|
|
381
|
-
*/
|
|
382
|
-
private emit;
|
|
383
|
-
/**
|
|
384
|
-
* Converts value to number, handling NaN and null.
|
|
385
|
-
*/
|
|
386
|
-
private toNumber;
|
|
387
|
-
/**
|
|
388
|
-
* Sleep utility.
|
|
389
|
-
*/
|
|
390
|
-
private sleep;
|
|
391
289
|
}
|
|
392
|
-
export {};
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TastyTradeClient = void 0;
|
|
4
4
|
const occ_1 = require("../../utils/occ");
|
|
5
|
-
|
|
6
|
-
* Regex pattern to identify OCC option symbols
|
|
7
|
-
*/
|
|
8
|
-
const OCC_OPTION_PATTERN = /^.{1,6}\d{6}[CP]\d{8}$/;
|
|
5
|
+
const BaseBrokerClient_1 = require("./BaseBrokerClient");
|
|
9
6
|
/**
|
|
10
7
|
* Event field configurations for different event types
|
|
11
8
|
*/
|
|
@@ -45,7 +42,7 @@ const FEED_EVENT_FIELDS = {
|
|
|
45
42
|
* client.subscribe(['SPY', '.SPXW231215C4500']); // Equity and option
|
|
46
43
|
* ```
|
|
47
44
|
*/
|
|
48
|
-
class TastyTradeClient {
|
|
45
|
+
class TastyTradeClient extends BaseBrokerClient_1.BaseBrokerClient {
|
|
49
46
|
/**
|
|
50
47
|
* Creates a new TastyTradeClient instance.
|
|
51
48
|
*
|
|
@@ -55,6 +52,8 @@ class TastyTradeClient {
|
|
|
55
52
|
* @param options.verbose - Whether to log verbose debug information (default: false)
|
|
56
53
|
*/
|
|
57
54
|
constructor(options) {
|
|
55
|
+
super(options);
|
|
56
|
+
this.brokerName = 'TastyTrade';
|
|
58
57
|
/** DxLink API quote token */
|
|
59
58
|
this.quoteToken = null;
|
|
60
59
|
/** DxLink WebSocket URL */
|
|
@@ -69,47 +68,21 @@ class TastyTradeClient {
|
|
|
69
68
|
this.feedChannelId = 1;
|
|
70
69
|
/** Feed channel opened */
|
|
71
70
|
this.feedChannelOpened = false;
|
|
72
|
-
/** Currently subscribed symbols */
|
|
73
|
-
this.subscribedSymbols = new Set();
|
|
74
71
|
/** Map from streamer symbol to OCC symbol */
|
|
75
72
|
this.streamerToOccMap = new Map();
|
|
76
73
|
/** Map from OCC symbol to streamer symbol */
|
|
77
74
|
this.occToStreamerMap = new Map();
|
|
78
|
-
/** Cached ticker data */
|
|
79
|
-
this.tickerCache = new Map();
|
|
80
|
-
/** Cached option data */
|
|
81
|
-
this.optionCache = new Map();
|
|
82
|
-
/** Base open interest from REST API */
|
|
83
|
-
this.baseOpenInterest = new Map();
|
|
84
|
-
/** Cumulative estimated OI change from intraday trades */
|
|
85
|
-
this.cumulativeOIChange = new Map();
|
|
86
|
-
/** History of intraday trades */
|
|
87
|
-
this.intradayTrades = new Map();
|
|
88
|
-
/** Event listeners */
|
|
89
|
-
this.eventListeners = new Map();
|
|
90
|
-
/** Reconnection attempt counter */
|
|
91
|
-
this.reconnectAttempts = 0;
|
|
92
|
-
/** Maximum reconnection attempts */
|
|
93
|
-
this.maxReconnectAttempts = 5;
|
|
94
|
-
/** Reconnection delay in ms */
|
|
95
|
-
this.baseReconnectDelay = 1000;
|
|
96
75
|
/** Keepalive interval handle */
|
|
97
76
|
this.keepaliveInterval = null;
|
|
77
|
+
/** Whether the first UNAUTHORIZED message has been handled yet - prevents incorrect handling of 'true' UNAUTHORIZED messages */
|
|
78
|
+
this.firstUnauthorizedMessageHandled = false;
|
|
98
79
|
/** Keepalive timeout in seconds */
|
|
99
80
|
this.keepaliveTimeoutSeconds = 60;
|
|
100
81
|
this.sessionToken = options.sessionToken;
|
|
101
82
|
this.sandbox = options.sandbox ?? false;
|
|
102
|
-
this.verbose = options.verbose ?? false;
|
|
103
83
|
this.apiBaseUrl = this.sandbox
|
|
104
84
|
? 'https://api.cert.tastyworks.com'
|
|
105
85
|
: 'https://api.tastyworks.com';
|
|
106
|
-
// Initialize event listener maps
|
|
107
|
-
this.eventListeners.set('tickerUpdate', new Set());
|
|
108
|
-
this.eventListeners.set('optionUpdate', new Set());
|
|
109
|
-
this.eventListeners.set('optionTrade', new Set());
|
|
110
|
-
this.eventListeners.set('connected', new Set());
|
|
111
|
-
this.eventListeners.set('disconnected', new Set());
|
|
112
|
-
this.eventListeners.set('error', new Set());
|
|
113
86
|
}
|
|
114
87
|
// ==================== Static Factory Methods ====================
|
|
115
88
|
/**
|
|
@@ -339,83 +312,6 @@ class TastyTradeClient {
|
|
|
339
312
|
});
|
|
340
313
|
await Promise.all(fetchPromises);
|
|
341
314
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Returns cached option data for a symbol.
|
|
344
|
-
*/
|
|
345
|
-
getOption(occSymbol) {
|
|
346
|
-
return this.optionCache.get(occSymbol);
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Returns all cached options.
|
|
350
|
-
*/
|
|
351
|
-
getAllOptions() {
|
|
352
|
-
return new Map(this.optionCache);
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Registers an event listener.
|
|
356
|
-
*/
|
|
357
|
-
on(event, listener) {
|
|
358
|
-
const listeners = this.eventListeners.get(event);
|
|
359
|
-
if (listeners) {
|
|
360
|
-
listeners.add(listener);
|
|
361
|
-
}
|
|
362
|
-
return this;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Removes an event listener.
|
|
366
|
-
*/
|
|
367
|
-
off(event, listener) {
|
|
368
|
-
const listeners = this.eventListeners.get(event);
|
|
369
|
-
if (listeners) {
|
|
370
|
-
listeners.delete(listener);
|
|
371
|
-
}
|
|
372
|
-
return this;
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Returns intraday trades for an option.
|
|
376
|
-
*/
|
|
377
|
-
getIntradayTrades(occSymbol) {
|
|
378
|
-
return this.intradayTrades.get(occSymbol) ?? [];
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Returns flow summary for an option.
|
|
382
|
-
*/
|
|
383
|
-
getFlowSummary(occSymbol) {
|
|
384
|
-
const trades = this.intradayTrades.get(occSymbol) ?? [];
|
|
385
|
-
let buyVolume = 0;
|
|
386
|
-
let sellVolume = 0;
|
|
387
|
-
let unknownVolume = 0;
|
|
388
|
-
for (const trade of trades) {
|
|
389
|
-
switch (trade.aggressorSide) {
|
|
390
|
-
case 'buy':
|
|
391
|
-
buyVolume += trade.size;
|
|
392
|
-
break;
|
|
393
|
-
case 'sell':
|
|
394
|
-
sellVolume += trade.size;
|
|
395
|
-
break;
|
|
396
|
-
case 'unknown':
|
|
397
|
-
unknownVolume += trade.size;
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
buyVolume,
|
|
403
|
-
sellVolume,
|
|
404
|
-
unknownVolume,
|
|
405
|
-
netOIChange: this.cumulativeOIChange.get(occSymbol) ?? 0,
|
|
406
|
-
tradeCount: trades.length,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Resets intraday tracking data.
|
|
411
|
-
*/
|
|
412
|
-
resetIntradayData(occSymbols) {
|
|
413
|
-
const symbolsToReset = occSymbols ?? Array.from(this.intradayTrades.keys());
|
|
414
|
-
for (const symbol of symbolsToReset) {
|
|
415
|
-
this.intradayTrades.delete(symbol);
|
|
416
|
-
this.cumulativeOIChange.set(symbol, 0);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
315
|
// ==================== Private Methods ====================
|
|
420
316
|
/**
|
|
421
317
|
* Gets API quote token from TastyTrade.
|
|
@@ -547,7 +443,7 @@ class TastyTradeClient {
|
|
|
547
443
|
const entries = [];
|
|
548
444
|
for (const symbol of symbols) {
|
|
549
445
|
const streamerSymbol = this.getStreamerSymbol(symbol);
|
|
550
|
-
const isOption = this.
|
|
446
|
+
const isOption = this.isTastyTradeOptionSymbol(symbol) || streamerSymbol.startsWith('.');
|
|
551
447
|
if (isOption) {
|
|
552
448
|
// Subscribe to option-relevant events
|
|
553
449
|
entries.push({ type: 'Quote', symbol: streamerSymbol });
|
|
@@ -588,7 +484,7 @@ class TastyTradeClient {
|
|
|
588
484
|
return symbol;
|
|
589
485
|
}
|
|
590
486
|
// If it's an OCC option symbol, try to convert
|
|
591
|
-
if (this.
|
|
487
|
+
if (this.isTastyTradeOptionSymbol(symbol)) {
|
|
592
488
|
try {
|
|
593
489
|
const parsed = (0, occ_1.parseOCCSymbol)(symbol);
|
|
594
490
|
// TastyTrade streamer format: .UNDERLYING + YYMMDD + C/P + STRIKE
|
|
@@ -670,8 +566,7 @@ class TastyTradeClient {
|
|
|
670
566
|
const message = JSON.parse(data);
|
|
671
567
|
switch (message.type) {
|
|
672
568
|
case 'SETUP':
|
|
673
|
-
//
|
|
674
|
-
this.sendAuth();
|
|
569
|
+
// no op; the server will send it's own first AUTH_STATE UNAUTHORIZED message right after
|
|
675
570
|
break;
|
|
676
571
|
case 'AUTH_STATE':
|
|
677
572
|
this.handleAuthState(message, connectResolve);
|
|
@@ -706,6 +601,15 @@ class TastyTradeClient {
|
|
|
706
601
|
* Handles AUTH_STATE message.
|
|
707
602
|
*/
|
|
708
603
|
handleAuthState(message, connectResolve) {
|
|
604
|
+
// the first message we get back (after SETUP response) is an UNAUTHORIZED state
|
|
605
|
+
// this is an expected part of the flow (though unintuitive)
|
|
606
|
+
// see https://developer.tastytrade.com/streaming-market-data/#dxlink-streamer
|
|
607
|
+
if (!this.firstUnauthorizedMessageHandled && message.state === 'UNAUTHORIZED') {
|
|
608
|
+
// Server acknowledged setup, send auth
|
|
609
|
+
this.sendAuth();
|
|
610
|
+
this.firstUnauthorizedMessageHandled = true;
|
|
611
|
+
}
|
|
612
|
+
// once we are authorized, we can proceed as normal
|
|
709
613
|
if (message.state === 'AUTHORIZED') {
|
|
710
614
|
this.authorized = true;
|
|
711
615
|
this.startKeepalive();
|
|
@@ -716,8 +620,15 @@ class TastyTradeClient {
|
|
|
716
620
|
this.emit('connected', undefined);
|
|
717
621
|
connectResolve?.();
|
|
718
622
|
}
|
|
623
|
+
// a true unauthorized message after being authorized indicates a problem
|
|
624
|
+
else if (message.state === 'UNAUTHORIZED' && this.authorized) {
|
|
625
|
+
this.authorized = false;
|
|
626
|
+
this.emit('error', new Error('DxLink authorization lost'));
|
|
627
|
+
this.disconnect();
|
|
628
|
+
this.attemptReconnect();
|
|
629
|
+
}
|
|
719
630
|
else {
|
|
720
|
-
this.emit('error', new Error('
|
|
631
|
+
this.emit('error', new Error('Unknown AUTH_STATE message state'));
|
|
721
632
|
}
|
|
722
633
|
}
|
|
723
634
|
/**
|
|
@@ -853,185 +764,25 @@ class TastyTradeClient {
|
|
|
853
764
|
* Updates ticker from Quote event.
|
|
854
765
|
*/
|
|
855
766
|
updateTickerFromQuote(symbol, bidPrice, askPrice, bidSize, askSize, timestamp) {
|
|
856
|
-
|
|
857
|
-
const ticker = {
|
|
858
|
-
symbol,
|
|
859
|
-
spot: bidPrice > 0 && askPrice > 0 ? (bidPrice + askPrice) / 2 : existing?.spot ?? 0,
|
|
860
|
-
bid: bidPrice,
|
|
861
|
-
bidSize,
|
|
862
|
-
ask: askPrice,
|
|
863
|
-
askSize,
|
|
864
|
-
last: existing?.last ?? 0,
|
|
865
|
-
volume: existing?.volume ?? 0,
|
|
866
|
-
timestamp,
|
|
867
|
-
};
|
|
868
|
-
this.tickerCache.set(symbol, ticker);
|
|
869
|
-
this.emit('tickerUpdate', ticker);
|
|
767
|
+
this.updateTickerFromQuoteData(symbol, bidPrice, bidSize, askPrice, askSize, timestamp);
|
|
870
768
|
}
|
|
871
769
|
/**
|
|
872
770
|
* Updates ticker from Trade event.
|
|
873
771
|
*/
|
|
874
772
|
updateTickerFromTrade(symbol, price, size, dayVolume, timestamp) {
|
|
875
|
-
|
|
876
|
-
const ticker = {
|
|
877
|
-
symbol,
|
|
878
|
-
spot: existing?.spot ?? price,
|
|
879
|
-
bid: existing?.bid ?? 0,
|
|
880
|
-
bidSize: existing?.bidSize ?? 0,
|
|
881
|
-
ask: existing?.ask ?? 0,
|
|
882
|
-
askSize: existing?.askSize ?? 0,
|
|
883
|
-
last: price,
|
|
884
|
-
volume: dayVolume > 0 ? dayVolume : (existing?.volume ?? 0) + size,
|
|
885
|
-
timestamp,
|
|
886
|
-
};
|
|
887
|
-
this.tickerCache.set(symbol, ticker);
|
|
888
|
-
this.emit('tickerUpdate', ticker);
|
|
773
|
+
this.updateTickerFromTradeData(symbol, price, size, dayVolume > 0 ? dayVolume : null, timestamp);
|
|
889
774
|
}
|
|
890
775
|
/**
|
|
891
776
|
* Updates option from Quote event.
|
|
892
777
|
*/
|
|
893
778
|
updateOptionFromQuote(occSymbol, bidPrice, askPrice, bidSize, askSize, timestamp) {
|
|
894
|
-
|
|
895
|
-
// Parse OCC symbol if we don't have existing data
|
|
896
|
-
let parsed;
|
|
897
|
-
try {
|
|
898
|
-
parsed = (0, occ_1.parseOCCSymbol)(occSymbol);
|
|
899
|
-
}
|
|
900
|
-
catch {
|
|
901
|
-
// Try to use existing data or skip
|
|
902
|
-
if (!existing)
|
|
903
|
-
return;
|
|
904
|
-
parsed = {
|
|
905
|
-
symbol: existing.underlying,
|
|
906
|
-
expiration: new Date(existing.expirationTimestamp),
|
|
907
|
-
optionType: existing.optionType,
|
|
908
|
-
strike: existing.strike,
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
const option = {
|
|
912
|
-
occSymbol,
|
|
913
|
-
underlying: parsed.symbol,
|
|
914
|
-
strike: parsed.strike,
|
|
915
|
-
expiration: parsed.expiration.toISOString().split('T')[0],
|
|
916
|
-
expirationTimestamp: parsed.expiration.getTime(),
|
|
917
|
-
optionType: parsed.optionType,
|
|
918
|
-
bid: bidPrice,
|
|
919
|
-
bidSize,
|
|
920
|
-
ask: askPrice,
|
|
921
|
-
askSize,
|
|
922
|
-
mark: bidPrice > 0 && askPrice > 0 ? (bidPrice + askPrice) / 2 : existing?.mark ?? 0,
|
|
923
|
-
last: existing?.last ?? 0,
|
|
924
|
-
volume: existing?.volume ?? 0,
|
|
925
|
-
openInterest: existing?.openInterest ?? 0,
|
|
926
|
-
liveOpenInterest: this.calculateLiveOpenInterest(occSymbol),
|
|
927
|
-
impliedVolatility: existing?.impliedVolatility ?? 0,
|
|
928
|
-
timestamp,
|
|
929
|
-
};
|
|
930
|
-
this.optionCache.set(occSymbol, option);
|
|
931
|
-
this.emit('optionUpdate', option);
|
|
779
|
+
this.updateOptionFromQuoteData(occSymbol, bidPrice, bidSize, askPrice, askSize, timestamp, occ_1.parseOCCSymbol);
|
|
932
780
|
}
|
|
933
781
|
/**
|
|
934
782
|
* Updates option from Trade event.
|
|
935
783
|
*/
|
|
936
784
|
updateOptionFromTrade(occSymbol, price, size, dayVolume, timestamp) {
|
|
937
|
-
|
|
938
|
-
// Parse OCC symbol
|
|
939
|
-
let parsed;
|
|
940
|
-
try {
|
|
941
|
-
parsed = (0, occ_1.parseOCCSymbol)(occSymbol);
|
|
942
|
-
}
|
|
943
|
-
catch {
|
|
944
|
-
if (!existing)
|
|
945
|
-
return;
|
|
946
|
-
parsed = {
|
|
947
|
-
symbol: existing.underlying,
|
|
948
|
-
expiration: new Date(existing.expirationTimestamp),
|
|
949
|
-
optionType: existing.optionType,
|
|
950
|
-
strike: existing.strike,
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
// Determine aggressor side
|
|
954
|
-
const bid = existing?.bid ?? 0;
|
|
955
|
-
const ask = existing?.ask ?? 0;
|
|
956
|
-
const aggressorSide = this.determineAggressorSide(price, bid, ask);
|
|
957
|
-
// Calculate OI change
|
|
958
|
-
const estimatedOIChange = this.calculateOIChangeFromTrade(aggressorSide, size, parsed.optionType);
|
|
959
|
-
const currentChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
|
|
960
|
-
this.cumulativeOIChange.set(occSymbol, currentChange + estimatedOIChange);
|
|
961
|
-
if (this.verbose && estimatedOIChange !== 0) {
|
|
962
|
-
const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
|
|
963
|
-
const newLiveOI = Math.max(0, baseOI + currentChange + estimatedOIChange);
|
|
964
|
-
console.log(`[TastyTrade:OI] ${occSymbol} trade: price=${price.toFixed(2)}, size=${size}, aggressor=${aggressorSide}, OI change=${estimatedOIChange > 0 ? '+' : ''}${estimatedOIChange}, liveOI=${newLiveOI} (base=${baseOI}, cumulative=${currentChange + estimatedOIChange})`);
|
|
965
|
-
}
|
|
966
|
-
// Record trade
|
|
967
|
-
const trade = {
|
|
968
|
-
occSymbol,
|
|
969
|
-
price,
|
|
970
|
-
size,
|
|
971
|
-
bid,
|
|
972
|
-
ask,
|
|
973
|
-
aggressorSide,
|
|
974
|
-
timestamp,
|
|
975
|
-
estimatedOIChange,
|
|
976
|
-
};
|
|
977
|
-
if (!this.intradayTrades.has(occSymbol)) {
|
|
978
|
-
this.intradayTrades.set(occSymbol, []);
|
|
979
|
-
}
|
|
980
|
-
this.intradayTrades.get(occSymbol).push(trade);
|
|
981
|
-
this.emit('optionTrade', trade);
|
|
982
|
-
const option = {
|
|
983
|
-
occSymbol,
|
|
984
|
-
underlying: parsed.symbol,
|
|
985
|
-
strike: parsed.strike,
|
|
986
|
-
expiration: parsed.expiration.toISOString().split('T')[0],
|
|
987
|
-
expirationTimestamp: parsed.expiration.getTime(),
|
|
988
|
-
optionType: parsed.optionType,
|
|
989
|
-
bid,
|
|
990
|
-
bidSize: existing?.bidSize ?? 0,
|
|
991
|
-
ask,
|
|
992
|
-
askSize: existing?.askSize ?? 0,
|
|
993
|
-
mark: bid > 0 && ask > 0 ? (bid + ask) / 2 : price,
|
|
994
|
-
last: price,
|
|
995
|
-
volume: dayVolume > 0 ? dayVolume : (existing?.volume ?? 0) + size,
|
|
996
|
-
openInterest: existing?.openInterest ?? 0,
|
|
997
|
-
liveOpenInterest: this.calculateLiveOpenInterest(occSymbol),
|
|
998
|
-
impliedVolatility: existing?.impliedVolatility ?? 0,
|
|
999
|
-
timestamp,
|
|
1000
|
-
};
|
|
1001
|
-
this.optionCache.set(occSymbol, option);
|
|
1002
|
-
this.emit('optionUpdate', option);
|
|
1003
|
-
}
|
|
1004
|
-
/**
|
|
1005
|
-
* Determines aggressor side from trade price vs NBBO.
|
|
1006
|
-
*/
|
|
1007
|
-
determineAggressorSide(tradePrice, bid, ask) {
|
|
1008
|
-
if (bid <= 0 || ask <= 0)
|
|
1009
|
-
return 'unknown';
|
|
1010
|
-
const spread = ask - bid;
|
|
1011
|
-
const tolerance = spread > 0 ? spread * 0.001 : 0.001;
|
|
1012
|
-
if (tradePrice >= ask - tolerance) {
|
|
1013
|
-
return 'buy';
|
|
1014
|
-
}
|
|
1015
|
-
else if (tradePrice <= bid + tolerance) {
|
|
1016
|
-
return 'sell';
|
|
1017
|
-
}
|
|
1018
|
-
return 'unknown';
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Calculates estimated OI change from trade.
|
|
1022
|
-
*/
|
|
1023
|
-
calculateOIChangeFromTrade(aggressorSide, size, _optionType) {
|
|
1024
|
-
if (aggressorSide === 'unknown')
|
|
1025
|
-
return 0;
|
|
1026
|
-
return aggressorSide === 'buy' ? size : -size;
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Calculates live open interest.
|
|
1030
|
-
*/
|
|
1031
|
-
calculateLiveOpenInterest(occSymbol) {
|
|
1032
|
-
const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
|
|
1033
|
-
const cumulativeChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
|
|
1034
|
-
return Math.max(0, baseOI + cumulativeChange);
|
|
785
|
+
this.updateOptionFromTradeData(occSymbol, price, size, dayVolume > 0 ? dayVolume : null, timestamp, occ_1.parseOCCSymbol);
|
|
1035
786
|
}
|
|
1036
787
|
/**
|
|
1037
788
|
* Handles DxLink error messages.
|
|
@@ -1048,10 +799,8 @@ class TastyTradeClient {
|
|
|
1048
799
|
return;
|
|
1049
800
|
}
|
|
1050
801
|
this.reconnectAttempts++;
|
|
1051
|
-
const delay = this.
|
|
1052
|
-
|
|
1053
|
-
console.log(`[TastyTrade:DxLink] Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
1054
|
-
}
|
|
802
|
+
const delay = this.getReconnectDelay();
|
|
803
|
+
this.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
1055
804
|
await this.sleep(delay);
|
|
1056
805
|
try {
|
|
1057
806
|
await this.connect();
|
|
@@ -1061,10 +810,10 @@ class TastyTradeClient {
|
|
|
1061
810
|
}
|
|
1062
811
|
}
|
|
1063
812
|
/**
|
|
1064
|
-
* Checks if symbol is
|
|
813
|
+
* Checks if symbol is a TastyTrade option symbol.
|
|
1065
814
|
*/
|
|
1066
|
-
|
|
1067
|
-
return OCC_OPTION_PATTERN.test(symbol);
|
|
815
|
+
isTastyTradeOptionSymbol(symbol) {
|
|
816
|
+
return BaseBrokerClient_1.OCC_OPTION_PATTERN.test(symbol);
|
|
1068
817
|
}
|
|
1069
818
|
/**
|
|
1070
819
|
* Sends a message to the WebSocket.
|
|
@@ -1074,38 +823,5 @@ class TastyTradeClient {
|
|
|
1074
823
|
this.ws.send(JSON.stringify(message));
|
|
1075
824
|
}
|
|
1076
825
|
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Emits an event to all listeners.
|
|
1079
|
-
*/
|
|
1080
|
-
emit(event, data) {
|
|
1081
|
-
const listeners = this.eventListeners.get(event);
|
|
1082
|
-
if (listeners) {
|
|
1083
|
-
listeners.forEach(listener => {
|
|
1084
|
-
try {
|
|
1085
|
-
listener(data);
|
|
1086
|
-
}
|
|
1087
|
-
catch (error) {
|
|
1088
|
-
console.error('Event listener error:', error);
|
|
1089
|
-
}
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Converts value to number, handling NaN and null.
|
|
1095
|
-
*/
|
|
1096
|
-
toNumber(value) {
|
|
1097
|
-
if (value === null || value === undefined)
|
|
1098
|
-
return 0;
|
|
1099
|
-
if (typeof value === 'number')
|
|
1100
|
-
return isNaN(value) ? 0 : value;
|
|
1101
|
-
const num = parseFloat(value);
|
|
1102
|
-
return isNaN(num) ? 0 : num;
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Sleep utility.
|
|
1106
|
-
*/
|
|
1107
|
-
sleep(ms) {
|
|
1108
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1109
|
-
}
|
|
1110
826
|
}
|
|
1111
827
|
exports.TastyTradeClient = TastyTradeClient;
|