@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
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseBrokerClient = exports.OCC_OPTION_PATTERN_WITH_SPACES = exports.OCC_OPTION_PATTERN = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Regex pattern to identify OCC option symbols (compact format)
|
|
6
|
+
* Format: ROOT + YYMMDD + C/P + 8-digit strike
|
|
7
|
+
* Example: "AAPL240517C00170000"
|
|
8
|
+
*/
|
|
9
|
+
exports.OCC_OPTION_PATTERN = /^[A-Z]{1,6}\d{6}[CP]\d{8}$/;
|
|
10
|
+
/**
|
|
11
|
+
* Regex pattern that also matches space-padded OCC symbols
|
|
12
|
+
* Example: "AAPL 240517C00170000"
|
|
13
|
+
*/
|
|
14
|
+
exports.OCC_OPTION_PATTERN_WITH_SPACES = /^.{1,6}\s*\d{6}[CP]\d{8}$/;
|
|
15
|
+
/**
|
|
16
|
+
* Abstract base class for all broker streaming clients.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* This class provides shared state management, event handling, and utility methods
|
|
20
|
+
* that are common across all broker implementations. Subclasses implement the
|
|
21
|
+
* broker-specific connection, subscription, and data parsing logic.
|
|
22
|
+
*
|
|
23
|
+
* All broker clients normalize their data to `NormalizedOption` and `NormalizedTicker`
|
|
24
|
+
* formats, providing a consistent interface regardless of the underlying broker API.
|
|
25
|
+
*/
|
|
26
|
+
class BaseBrokerClient {
|
|
27
|
+
// ==================== Constructor ====================
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
// ==================== Shared State ====================
|
|
30
|
+
/** Cached ticker data */
|
|
31
|
+
this.tickerCache = new Map();
|
|
32
|
+
/** Cached option data */
|
|
33
|
+
this.optionCache = new Map();
|
|
34
|
+
/** Base open interest from REST API - used as t=0 reference */
|
|
35
|
+
this.baseOpenInterest = new Map();
|
|
36
|
+
/** Cumulative estimated OI change from intraday trades */
|
|
37
|
+
this.cumulativeOIChange = new Map();
|
|
38
|
+
/** History of intraday trades with aggressor classification */
|
|
39
|
+
this.intradayTrades = new Map();
|
|
40
|
+
/** Event listeners */
|
|
41
|
+
this.eventListeners = new Map();
|
|
42
|
+
/** Currently subscribed symbols */
|
|
43
|
+
this.subscribedSymbols = new Set();
|
|
44
|
+
/** Reconnection attempt counter */
|
|
45
|
+
this.reconnectAttempts = 0;
|
|
46
|
+
this.verbose = options.verbose ?? false;
|
|
47
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
48
|
+
this.baseReconnectDelay = options.baseReconnectDelay ?? 1000;
|
|
49
|
+
// Initialize event listener maps
|
|
50
|
+
this.eventListeners.set('tickerUpdate', new Set());
|
|
51
|
+
this.eventListeners.set('optionUpdate', new Set());
|
|
52
|
+
this.eventListeners.set('optionTrade', new Set());
|
|
53
|
+
this.eventListeners.set('connected', new Set());
|
|
54
|
+
this.eventListeners.set('disconnected', new Set());
|
|
55
|
+
this.eventListeners.set('error', new Set());
|
|
56
|
+
}
|
|
57
|
+
// ==================== Concrete Public Methods ====================
|
|
58
|
+
/**
|
|
59
|
+
* Returns cached option data for a symbol.
|
|
60
|
+
* @param occSymbol - OCC option symbol
|
|
61
|
+
*/
|
|
62
|
+
getOption(occSymbol) {
|
|
63
|
+
return this.optionCache.get(this.normalizeOccSymbol(occSymbol));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns all cached options.
|
|
67
|
+
*/
|
|
68
|
+
getAllOptions() {
|
|
69
|
+
return new Map(this.optionCache);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns cached ticker data for a symbol.
|
|
73
|
+
* @param symbol - Ticker symbol
|
|
74
|
+
*/
|
|
75
|
+
getTicker(symbol) {
|
|
76
|
+
return this.tickerCache.get(symbol);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns all cached tickers.
|
|
80
|
+
*/
|
|
81
|
+
getAllTickers() {
|
|
82
|
+
return new Map(this.tickerCache);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns intraday trades for an option.
|
|
86
|
+
* @param occSymbol - OCC option symbol
|
|
87
|
+
*/
|
|
88
|
+
getIntradayTrades(occSymbol) {
|
|
89
|
+
return this.intradayTrades.get(this.normalizeOccSymbol(occSymbol)) ?? [];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns flow summary statistics for an option.
|
|
93
|
+
* @param occSymbol - OCC option symbol
|
|
94
|
+
*/
|
|
95
|
+
getFlowSummary(occSymbol) {
|
|
96
|
+
const normalizedSymbol = this.normalizeOccSymbol(occSymbol);
|
|
97
|
+
const trades = this.intradayTrades.get(normalizedSymbol) ?? [];
|
|
98
|
+
let buyVolume = 0;
|
|
99
|
+
let sellVolume = 0;
|
|
100
|
+
let unknownVolume = 0;
|
|
101
|
+
for (const trade of trades) {
|
|
102
|
+
switch (trade.aggressorSide) {
|
|
103
|
+
case 'buy':
|
|
104
|
+
buyVolume += trade.size;
|
|
105
|
+
break;
|
|
106
|
+
case 'sell':
|
|
107
|
+
sellVolume += trade.size;
|
|
108
|
+
break;
|
|
109
|
+
case 'unknown':
|
|
110
|
+
unknownVolume += trade.size;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
buyVolume,
|
|
116
|
+
sellVolume,
|
|
117
|
+
unknownVolume,
|
|
118
|
+
netOIChange: this.cumulativeOIChange.get(normalizedSymbol) ?? 0,
|
|
119
|
+
tradeCount: trades.length,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resets intraday tracking data.
|
|
124
|
+
* @param occSymbols - Optional specific symbols to reset. If not provided, resets all.
|
|
125
|
+
*/
|
|
126
|
+
resetIntradayData(occSymbols) {
|
|
127
|
+
const symbolsToReset = occSymbols?.map(s => this.normalizeOccSymbol(s))
|
|
128
|
+
?? Array.from(this.intradayTrades.keys());
|
|
129
|
+
for (const symbol of symbolsToReset) {
|
|
130
|
+
this.intradayTrades.delete(symbol);
|
|
131
|
+
this.cumulativeOIChange.set(symbol, 0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Registers an event listener.
|
|
136
|
+
* @param event - Event type to listen for
|
|
137
|
+
* @param listener - Callback function
|
|
138
|
+
*/
|
|
139
|
+
on(event, listener) {
|
|
140
|
+
const listeners = this.eventListeners.get(event);
|
|
141
|
+
if (listeners) {
|
|
142
|
+
listeners.add(listener);
|
|
143
|
+
}
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Removes an event listener.
|
|
148
|
+
* @param event - Event type
|
|
149
|
+
* @param listener - Callback function to remove
|
|
150
|
+
*/
|
|
151
|
+
off(event, listener) {
|
|
152
|
+
const listeners = this.eventListeners.get(event);
|
|
153
|
+
if (listeners) {
|
|
154
|
+
listeners.delete(listener);
|
|
155
|
+
}
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
// ==================== Protected Helpers ====================
|
|
159
|
+
/**
|
|
160
|
+
* Determines the aggressor side of a trade by comparing trade price to NBBO.
|
|
161
|
+
*
|
|
162
|
+
* @param tradePrice - The executed trade price
|
|
163
|
+
* @param bid - The bid price at time of trade
|
|
164
|
+
* @param ask - The ask price at time of trade
|
|
165
|
+
* @returns The aggressor side: 'buy' if lifting offer, 'sell' if hitting bid, 'unknown' if mid
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* The aggressor is the party that initiated the trade by crossing the spread:
|
|
169
|
+
* - Buy aggressor: Buyer lifts the offer (trades at or above ask) → bullish intent
|
|
170
|
+
* - Sell aggressor: Seller hits the bid (trades at or below bid) → bearish intent
|
|
171
|
+
* - Unknown: Trade occurred mid-market (could be internalized, crossed, or negotiated)
|
|
172
|
+
*/
|
|
173
|
+
determineAggressorSide(tradePrice, bid, ask) {
|
|
174
|
+
if (bid <= 0 || ask <= 0)
|
|
175
|
+
return 'unknown';
|
|
176
|
+
const spread = ask - bid;
|
|
177
|
+
const tolerance = spread > 0 ? spread * 0.001 : 0.001;
|
|
178
|
+
if (tradePrice >= ask - tolerance) {
|
|
179
|
+
return 'buy';
|
|
180
|
+
}
|
|
181
|
+
else if (tradePrice <= bid + tolerance) {
|
|
182
|
+
return 'sell';
|
|
183
|
+
}
|
|
184
|
+
return 'unknown';
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Calculates the estimated open interest change from a single trade.
|
|
188
|
+
*
|
|
189
|
+
* @param aggressorSide - The aggressor side of the trade
|
|
190
|
+
* @param size - Number of contracts traded
|
|
191
|
+
* @returns Estimated OI change (positive = OI increase, negative = OI decrease)
|
|
192
|
+
*
|
|
193
|
+
* @remarks
|
|
194
|
+
* This uses a simplified heuristic:
|
|
195
|
+
* - Buy aggressor → typically opening new long positions → +OI
|
|
196
|
+
* - Sell aggressor → typically closing longs or opening shorts → -OI
|
|
197
|
+
* - Unknown → ambiguous, assume neutral impact
|
|
198
|
+
*/
|
|
199
|
+
calculateOIChangeFromTrade(aggressorSide, size) {
|
|
200
|
+
if (aggressorSide === 'unknown')
|
|
201
|
+
return 0;
|
|
202
|
+
return aggressorSide === 'buy' ? size : -size;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Calculates the live (intraday) open interest estimate for an option.
|
|
206
|
+
*
|
|
207
|
+
* @param occSymbol - OCC option symbol
|
|
208
|
+
* @returns Live OI estimate = base OI + cumulative estimated changes
|
|
209
|
+
*/
|
|
210
|
+
calculateLiveOpenInterest(occSymbol) {
|
|
211
|
+
const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
|
|
212
|
+
const cumulativeChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
|
|
213
|
+
return Math.max(0, baseOI + cumulativeChange);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Records a trade and updates OI tracking.
|
|
217
|
+
*/
|
|
218
|
+
recordTrade(occSymbol, price, size, bid, ask, timestamp, optionType) {
|
|
219
|
+
const aggressorSide = this.determineAggressorSide(price, bid, ask);
|
|
220
|
+
const estimatedOIChange = this.calculateOIChangeFromTrade(aggressorSide, size);
|
|
221
|
+
const currentChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
|
|
222
|
+
this.cumulativeOIChange.set(occSymbol, currentChange + estimatedOIChange);
|
|
223
|
+
if (this.verbose && estimatedOIChange !== 0) {
|
|
224
|
+
const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
|
|
225
|
+
const newLiveOI = Math.max(0, baseOI + currentChange + estimatedOIChange);
|
|
226
|
+
console.log(`[${this.brokerName}:OI] ${occSymbol} trade: price=${price.toFixed(2)}, size=${size}, ` +
|
|
227
|
+
`aggressor=${aggressorSide}, OI change=${estimatedOIChange > 0 ? '+' : ''}${estimatedOIChange}, ` +
|
|
228
|
+
`liveOI=${newLiveOI} (base=${baseOI}, cumulative=${currentChange + estimatedOIChange})`);
|
|
229
|
+
}
|
|
230
|
+
const trade = {
|
|
231
|
+
occSymbol,
|
|
232
|
+
price,
|
|
233
|
+
size,
|
|
234
|
+
bid,
|
|
235
|
+
ask,
|
|
236
|
+
aggressorSide,
|
|
237
|
+
timestamp,
|
|
238
|
+
estimatedOIChange,
|
|
239
|
+
};
|
|
240
|
+
if (!this.intradayTrades.has(occSymbol)) {
|
|
241
|
+
this.intradayTrades.set(occSymbol, []);
|
|
242
|
+
}
|
|
243
|
+
this.intradayTrades.get(occSymbol).push(trade);
|
|
244
|
+
this.emit('optionTrade', trade);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Sets base open interest for a symbol.
|
|
248
|
+
*/
|
|
249
|
+
setBaseOpenInterest(occSymbol, openInterest) {
|
|
250
|
+
if (openInterest > 0 && !this.baseOpenInterest.has(occSymbol)) {
|
|
251
|
+
this.baseOpenInterest.set(occSymbol, openInterest);
|
|
252
|
+
if (!this.cumulativeOIChange.has(occSymbol)) {
|
|
253
|
+
this.cumulativeOIChange.set(occSymbol, 0);
|
|
254
|
+
}
|
|
255
|
+
if (this.verbose) {
|
|
256
|
+
console.log(`[${this.brokerName}:OI] Base OI set for ${occSymbol}: ${openInterest}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Emits an event to all registered listeners.
|
|
262
|
+
*/
|
|
263
|
+
emit(event, data) {
|
|
264
|
+
const listeners = this.eventListeners.get(event);
|
|
265
|
+
if (listeners) {
|
|
266
|
+
listeners.forEach(listener => {
|
|
267
|
+
try {
|
|
268
|
+
listener(data);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
console.error(`[${this.brokerName}] Event listener error:`, error);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Normalizes an OCC symbol to consistent format.
|
|
278
|
+
* Removes extra spaces, ensures proper formatting.
|
|
279
|
+
*/
|
|
280
|
+
normalizeOccSymbol(symbol) {
|
|
281
|
+
const stripped = symbol.replace(/\s+/g, '');
|
|
282
|
+
const match = stripped.match(/^([A-Z]+)(\d{6})([CP])(\d{8})$/);
|
|
283
|
+
if (match) {
|
|
284
|
+
return `${match[1]}${match[2]}${match[3]}${match[4]}`;
|
|
285
|
+
}
|
|
286
|
+
return stripped;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Checks if a symbol is an OCC option symbol.
|
|
290
|
+
*/
|
|
291
|
+
isOptionSymbol(symbol) {
|
|
292
|
+
const normalized = symbol.replace(/\s+/g, '');
|
|
293
|
+
return exports.OCC_OPTION_PATTERN.test(normalized) || /\d{6}[CP]\d{8}/.test(normalized);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Converts value to number, handling NaN and null.
|
|
297
|
+
*/
|
|
298
|
+
toNumber(value) {
|
|
299
|
+
if (value === null || value === undefined)
|
|
300
|
+
return 0;
|
|
301
|
+
if (typeof value === 'number')
|
|
302
|
+
return isNaN(value) ? 0 : value;
|
|
303
|
+
if (typeof value === 'string') {
|
|
304
|
+
const num = parseFloat(value);
|
|
305
|
+
return isNaN(num) ? 0 : num;
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Sleep utility for delays and reconnection backoff.
|
|
311
|
+
*/
|
|
312
|
+
sleep(ms) {
|
|
313
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Calculates reconnection delay with exponential backoff.
|
|
317
|
+
*/
|
|
318
|
+
getReconnectDelay() {
|
|
319
|
+
return this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Logs a message if verbose mode is enabled.
|
|
323
|
+
*/
|
|
324
|
+
log(message) {
|
|
325
|
+
if (this.verbose) {
|
|
326
|
+
console.log(`[${this.brokerName}] ${message}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// ==================== Common Update Helpers ====================
|
|
330
|
+
/**
|
|
331
|
+
* Updates or creates a ticker from quote data (bid/ask update).
|
|
332
|
+
* @returns The updated ticker
|
|
333
|
+
*/
|
|
334
|
+
updateTickerFromQuoteData(symbol, bid, bidSize, ask, askSize, timestamp) {
|
|
335
|
+
const existing = this.tickerCache.get(symbol);
|
|
336
|
+
const ticker = {
|
|
337
|
+
symbol,
|
|
338
|
+
spot: bid > 0 && ask > 0 ? (bid + ask) / 2 : existing?.spot ?? 0,
|
|
339
|
+
bid,
|
|
340
|
+
bidSize,
|
|
341
|
+
ask,
|
|
342
|
+
askSize,
|
|
343
|
+
last: existing?.last ?? 0,
|
|
344
|
+
volume: existing?.volume ?? 0,
|
|
345
|
+
timestamp,
|
|
346
|
+
};
|
|
347
|
+
this.tickerCache.set(symbol, ticker);
|
|
348
|
+
this.emit('tickerUpdate', ticker);
|
|
349
|
+
return ticker;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Updates or creates a ticker from trade data (last price/volume update).
|
|
353
|
+
* @returns The updated ticker
|
|
354
|
+
*/
|
|
355
|
+
updateTickerFromTradeData(symbol, price, size, dayVolume, timestamp) {
|
|
356
|
+
const existing = this.tickerCache.get(symbol);
|
|
357
|
+
const ticker = {
|
|
358
|
+
symbol,
|
|
359
|
+
spot: existing?.spot ?? price,
|
|
360
|
+
bid: existing?.bid ?? 0,
|
|
361
|
+
bidSize: existing?.bidSize ?? 0,
|
|
362
|
+
ask: existing?.ask ?? 0,
|
|
363
|
+
askSize: existing?.askSize ?? 0,
|
|
364
|
+
last: price,
|
|
365
|
+
volume: dayVolume !== null ? dayVolume : (existing?.volume ?? 0) + size,
|
|
366
|
+
timestamp,
|
|
367
|
+
};
|
|
368
|
+
this.tickerCache.set(symbol, ticker);
|
|
369
|
+
this.emit('tickerUpdate', ticker);
|
|
370
|
+
return ticker;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Updates or creates an option from quote data (bid/ask update).
|
|
374
|
+
* @returns The updated option, or null if symbol cannot be parsed
|
|
375
|
+
*/
|
|
376
|
+
updateOptionFromQuoteData(occSymbol, bid, bidSize, ask, askSize, timestamp, parseSymbolFn) {
|
|
377
|
+
const existing = this.optionCache.get(occSymbol);
|
|
378
|
+
let parsed;
|
|
379
|
+
try {
|
|
380
|
+
parsed = parseSymbolFn(occSymbol);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
if (!existing)
|
|
384
|
+
return null;
|
|
385
|
+
parsed = {
|
|
386
|
+
symbol: existing.underlying,
|
|
387
|
+
expiration: new Date(existing.expirationTimestamp),
|
|
388
|
+
optionType: existing.optionType,
|
|
389
|
+
strike: existing.strike,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const option = {
|
|
393
|
+
occSymbol,
|
|
394
|
+
underlying: parsed.symbol,
|
|
395
|
+
strike: parsed.strike,
|
|
396
|
+
expiration: parsed.expiration.toISOString().split('T')[0],
|
|
397
|
+
expirationTimestamp: parsed.expiration.getTime(),
|
|
398
|
+
optionType: parsed.optionType,
|
|
399
|
+
bid,
|
|
400
|
+
bidSize,
|
|
401
|
+
ask,
|
|
402
|
+
askSize,
|
|
403
|
+
mark: bid > 0 && ask > 0 ? (bid + ask) / 2 : existing?.mark ?? 0,
|
|
404
|
+
last: existing?.last ?? 0,
|
|
405
|
+
volume: existing?.volume ?? 0,
|
|
406
|
+
openInterest: existing?.openInterest ?? 0,
|
|
407
|
+
liveOpenInterest: this.calculateLiveOpenInterest(occSymbol),
|
|
408
|
+
impliedVolatility: existing?.impliedVolatility ?? 0,
|
|
409
|
+
timestamp,
|
|
410
|
+
};
|
|
411
|
+
this.optionCache.set(occSymbol, option);
|
|
412
|
+
this.emit('optionUpdate', option);
|
|
413
|
+
return option;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Updates or creates an option from trade data, including OI tracking.
|
|
417
|
+
* @returns The updated option, or null if symbol cannot be parsed
|
|
418
|
+
*/
|
|
419
|
+
updateOptionFromTradeData(occSymbol, price, size, dayVolume, timestamp, parseSymbolFn) {
|
|
420
|
+
const existing = this.optionCache.get(occSymbol);
|
|
421
|
+
let parsed;
|
|
422
|
+
try {
|
|
423
|
+
parsed = parseSymbolFn(occSymbol);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
if (!existing)
|
|
427
|
+
return null;
|
|
428
|
+
parsed = {
|
|
429
|
+
symbol: existing.underlying,
|
|
430
|
+
expiration: new Date(existing.expirationTimestamp),
|
|
431
|
+
optionType: existing.optionType,
|
|
432
|
+
strike: existing.strike,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const bid = existing?.bid ?? 0;
|
|
436
|
+
const ask = existing?.ask ?? 0;
|
|
437
|
+
// Record trade for OI tracking
|
|
438
|
+
this.recordTrade(occSymbol, price, size, bid, ask, timestamp, parsed.optionType);
|
|
439
|
+
const option = {
|
|
440
|
+
occSymbol,
|
|
441
|
+
underlying: parsed.symbol,
|
|
442
|
+
strike: parsed.strike,
|
|
443
|
+
expiration: parsed.expiration.toISOString().split('T')[0],
|
|
444
|
+
expirationTimestamp: parsed.expiration.getTime(),
|
|
445
|
+
optionType: parsed.optionType,
|
|
446
|
+
bid,
|
|
447
|
+
bidSize: existing?.bidSize ?? 0,
|
|
448
|
+
ask,
|
|
449
|
+
askSize: existing?.askSize ?? 0,
|
|
450
|
+
mark: bid > 0 && ask > 0 ? (bid + ask) / 2 : price,
|
|
451
|
+
last: price,
|
|
452
|
+
volume: dayVolume !== null ? dayVolume : (existing?.volume ?? 0) + size,
|
|
453
|
+
openInterest: existing?.openInterest ?? 0,
|
|
454
|
+
liveOpenInterest: this.calculateLiveOpenInterest(occSymbol),
|
|
455
|
+
impliedVolatility: existing?.impliedVolatility ?? 0,
|
|
456
|
+
timestamp,
|
|
457
|
+
};
|
|
458
|
+
this.optionCache.set(occSymbol, option);
|
|
459
|
+
this.emit('optionUpdate', option);
|
|
460
|
+
return option;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Updates or creates an option from timesale data (trade with bid/ask at time of sale).
|
|
464
|
+
* This is particularly useful for live OI tracking.
|
|
465
|
+
* @returns The updated option, or null if symbol cannot be parsed
|
|
466
|
+
*/
|
|
467
|
+
updateOptionFromTimesaleData(occSymbol, price, size, bid, ask, timestamp, parseSymbolFn) {
|
|
468
|
+
const existing = this.optionCache.get(occSymbol);
|
|
469
|
+
let parsed;
|
|
470
|
+
try {
|
|
471
|
+
parsed = parseSymbolFn(occSymbol);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
if (!existing)
|
|
475
|
+
return null;
|
|
476
|
+
parsed = {
|
|
477
|
+
symbol: existing.underlying,
|
|
478
|
+
expiration: new Date(existing.expirationTimestamp),
|
|
479
|
+
optionType: existing.optionType,
|
|
480
|
+
strike: existing.strike,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
// Record trade for OI tracking (timesale has fresh bid/ask)
|
|
484
|
+
this.recordTrade(occSymbol, price, size, bid, ask, timestamp, parsed.optionType);
|
|
485
|
+
const option = {
|
|
486
|
+
occSymbol,
|
|
487
|
+
underlying: parsed.symbol,
|
|
488
|
+
strike: parsed.strike,
|
|
489
|
+
expiration: parsed.expiration.toISOString().split('T')[0],
|
|
490
|
+
expirationTimestamp: parsed.expiration.getTime(),
|
|
491
|
+
optionType: parsed.optionType,
|
|
492
|
+
bid,
|
|
493
|
+
bidSize: existing?.bidSize ?? 0,
|
|
494
|
+
ask,
|
|
495
|
+
askSize: existing?.askSize ?? 0,
|
|
496
|
+
mark: (bid + ask) / 2,
|
|
497
|
+
last: price,
|
|
498
|
+
volume: (existing?.volume ?? 0) + size,
|
|
499
|
+
openInterest: existing?.openInterest ?? 0,
|
|
500
|
+
liveOpenInterest: this.calculateLiveOpenInterest(occSymbol),
|
|
501
|
+
impliedVolatility: existing?.impliedVolatility ?? 0,
|
|
502
|
+
timestamp,
|
|
503
|
+
};
|
|
504
|
+
this.optionCache.set(occSymbol, option);
|
|
505
|
+
this.emit('optionUpdate', option);
|
|
506
|
+
return option;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
exports.BaseBrokerClient = BaseBrokerClient;
|