@fullstackcraftllc/floe 0.0.8 → 0.0.9
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 +15 -120
- package/dist/client/brokers/TastyTradeClient.js +15 -316
- 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,30 +83,10 @@ 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;
|
|
132
92
|
/** Keepalive timeout in seconds */
|
|
@@ -135,8 +95,6 @@ export declare class TastyTradeClient {
|
|
|
135
95
|
private readonly apiBaseUrl;
|
|
136
96
|
/** Whether to use sandbox environment */
|
|
137
97
|
private readonly sandbox;
|
|
138
|
-
/** Whether to log verbose debug information */
|
|
139
|
-
private readonly verbose;
|
|
140
98
|
/**
|
|
141
99
|
* Creates a new TastyTradeClient instance.
|
|
142
100
|
*
|
|
@@ -145,11 +103,7 @@ export declare class TastyTradeClient {
|
|
|
145
103
|
* @param options.sandbox - Whether to use sandbox environment (default: false)
|
|
146
104
|
* @param options.verbose - Whether to log verbose debug information (default: false)
|
|
147
105
|
*/
|
|
148
|
-
constructor(options:
|
|
149
|
-
sessionToken: string;
|
|
150
|
-
sandbox?: boolean;
|
|
151
|
-
verbose?: boolean;
|
|
152
|
-
});
|
|
106
|
+
constructor(options: TastyTradeClientOptions);
|
|
153
107
|
/**
|
|
154
108
|
* Creates a TastyTradeClient by logging in with username/password.
|
|
155
109
|
*
|
|
@@ -222,40 +176,6 @@ export declare class TastyTradeClient {
|
|
|
222
176
|
* @param occSymbols - Array of OCC option symbols to fetch data for
|
|
223
177
|
*/
|
|
224
178
|
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
179
|
/**
|
|
260
180
|
* Gets API quote token from TastyTrade.
|
|
261
181
|
*/
|
|
@@ -348,18 +268,6 @@ export declare class TastyTradeClient {
|
|
|
348
268
|
* Updates option from Trade event.
|
|
349
269
|
*/
|
|
350
270
|
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
271
|
/**
|
|
364
272
|
* Handles DxLink error messages.
|
|
365
273
|
*/
|
|
@@ -369,24 +277,11 @@ export declare class TastyTradeClient {
|
|
|
369
277
|
*/
|
|
370
278
|
private attemptReconnect;
|
|
371
279
|
/**
|
|
372
|
-
* Checks if symbol is
|
|
280
|
+
* Checks if symbol is a TastyTrade option symbol.
|
|
373
281
|
*/
|
|
374
|
-
private
|
|
282
|
+
private isTastyTradeOptionSymbol;
|
|
375
283
|
/**
|
|
376
284
|
* Sends a message to the WebSocket.
|
|
377
285
|
*/
|
|
378
286
|
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
287
|
}
|
|
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,19 @@ 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;
|
|
98
77
|
/** Keepalive timeout in seconds */
|
|
99
78
|
this.keepaliveTimeoutSeconds = 60;
|
|
100
79
|
this.sessionToken = options.sessionToken;
|
|
101
80
|
this.sandbox = options.sandbox ?? false;
|
|
102
|
-
this.verbose = options.verbose ?? false;
|
|
103
81
|
this.apiBaseUrl = this.sandbox
|
|
104
82
|
? 'https://api.cert.tastyworks.com'
|
|
105
83
|
: '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
84
|
}
|
|
114
85
|
// ==================== Static Factory Methods ====================
|
|
115
86
|
/**
|
|
@@ -339,83 +310,6 @@ class TastyTradeClient {
|
|
|
339
310
|
});
|
|
340
311
|
await Promise.all(fetchPromises);
|
|
341
312
|
}
|
|
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
313
|
// ==================== Private Methods ====================
|
|
420
314
|
/**
|
|
421
315
|
* Gets API quote token from TastyTrade.
|
|
@@ -547,7 +441,7 @@ class TastyTradeClient {
|
|
|
547
441
|
const entries = [];
|
|
548
442
|
for (const symbol of symbols) {
|
|
549
443
|
const streamerSymbol = this.getStreamerSymbol(symbol);
|
|
550
|
-
const isOption = this.
|
|
444
|
+
const isOption = this.isTastyTradeOptionSymbol(symbol) || streamerSymbol.startsWith('.');
|
|
551
445
|
if (isOption) {
|
|
552
446
|
// Subscribe to option-relevant events
|
|
553
447
|
entries.push({ type: 'Quote', symbol: streamerSymbol });
|
|
@@ -588,7 +482,7 @@ class TastyTradeClient {
|
|
|
588
482
|
return symbol;
|
|
589
483
|
}
|
|
590
484
|
// If it's an OCC option symbol, try to convert
|
|
591
|
-
if (this.
|
|
485
|
+
if (this.isTastyTradeOptionSymbol(symbol)) {
|
|
592
486
|
try {
|
|
593
487
|
const parsed = (0, occ_1.parseOCCSymbol)(symbol);
|
|
594
488
|
// TastyTrade streamer format: .UNDERLYING + YYMMDD + C/P + STRIKE
|
|
@@ -853,185 +747,25 @@ class TastyTradeClient {
|
|
|
853
747
|
* Updates ticker from Quote event.
|
|
854
748
|
*/
|
|
855
749
|
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);
|
|
750
|
+
this.updateTickerFromQuoteData(symbol, bidPrice, bidSize, askPrice, askSize, timestamp);
|
|
870
751
|
}
|
|
871
752
|
/**
|
|
872
753
|
* Updates ticker from Trade event.
|
|
873
754
|
*/
|
|
874
755
|
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);
|
|
756
|
+
this.updateTickerFromTradeData(symbol, price, size, dayVolume > 0 ? dayVolume : null, timestamp);
|
|
889
757
|
}
|
|
890
758
|
/**
|
|
891
759
|
* Updates option from Quote event.
|
|
892
760
|
*/
|
|
893
761
|
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);
|
|
762
|
+
this.updateOptionFromQuoteData(occSymbol, bidPrice, bidSize, askPrice, askSize, timestamp, occ_1.parseOCCSymbol);
|
|
932
763
|
}
|
|
933
764
|
/**
|
|
934
765
|
* Updates option from Trade event.
|
|
935
766
|
*/
|
|
936
767
|
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);
|
|
768
|
+
this.updateOptionFromTradeData(occSymbol, price, size, dayVolume > 0 ? dayVolume : null, timestamp, occ_1.parseOCCSymbol);
|
|
1035
769
|
}
|
|
1036
770
|
/**
|
|
1037
771
|
* Handles DxLink error messages.
|
|
@@ -1048,10 +782,8 @@ class TastyTradeClient {
|
|
|
1048
782
|
return;
|
|
1049
783
|
}
|
|
1050
784
|
this.reconnectAttempts++;
|
|
1051
|
-
const delay = this.
|
|
1052
|
-
|
|
1053
|
-
console.log(`[TastyTrade:DxLink] Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
1054
|
-
}
|
|
785
|
+
const delay = this.getReconnectDelay();
|
|
786
|
+
this.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
1055
787
|
await this.sleep(delay);
|
|
1056
788
|
try {
|
|
1057
789
|
await this.connect();
|
|
@@ -1061,10 +793,10 @@ class TastyTradeClient {
|
|
|
1061
793
|
}
|
|
1062
794
|
}
|
|
1063
795
|
/**
|
|
1064
|
-
* Checks if symbol is
|
|
796
|
+
* Checks if symbol is a TastyTrade option symbol.
|
|
1065
797
|
*/
|
|
1066
|
-
|
|
1067
|
-
return OCC_OPTION_PATTERN.test(symbol);
|
|
798
|
+
isTastyTradeOptionSymbol(symbol) {
|
|
799
|
+
return BaseBrokerClient_1.OCC_OPTION_PATTERN.test(symbol);
|
|
1068
800
|
}
|
|
1069
801
|
/**
|
|
1070
802
|
* Sends a message to the WebSocket.
|
|
@@ -1074,38 +806,5 @@ class TastyTradeClient {
|
|
|
1074
806
|
this.ws.send(JSON.stringify(message));
|
|
1075
807
|
}
|
|
1076
808
|
}
|
|
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
809
|
}
|
|
1111
810
|
exports.TastyTradeClient = TastyTradeClient;
|