@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,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseBrokerClient, BaseBrokerClientOptions, AggressorSide, IntradayTrade, FlowSummary } from './BaseBrokerClient';
|
|
2
|
+
export type { AggressorSide, IntradayTrade, FlowSummary };
|
|
2
3
|
/**
|
|
3
4
|
* Schwab option chain response
|
|
4
5
|
*/
|
|
@@ -104,38 +105,12 @@ interface SchwabOptionContract {
|
|
|
104
105
|
mini: boolean;
|
|
105
106
|
}
|
|
106
107
|
/**
|
|
107
|
-
*
|
|
108
|
+
* Schwab client configuration options
|
|
108
109
|
*/
|
|
109
|
-
export
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
*/
|
|
113
|
-
export interface IntradayTrade {
|
|
114
|
-
/** OCC option symbol */
|
|
115
|
-
occSymbol: string;
|
|
116
|
-
/** Trade price */
|
|
117
|
-
price: number;
|
|
118
|
-
/** Trade size (number of contracts) */
|
|
119
|
-
size: number;
|
|
120
|
-
/** Bid at time of trade */
|
|
121
|
-
bid: number;
|
|
122
|
-
/** Ask at time of trade */
|
|
123
|
-
ask: number;
|
|
124
|
-
/** Aggressor side determined from price vs NBBO */
|
|
125
|
-
aggressorSide: AggressorSide;
|
|
126
|
-
/** Timestamp of the trade */
|
|
127
|
-
timestamp: number;
|
|
128
|
-
/** Estimated OI change */
|
|
129
|
-
estimatedOIChange: number;
|
|
110
|
+
export interface SchwabClientOptions extends BaseBrokerClientOptions {
|
|
111
|
+
/** Schwab OAuth access token (required) */
|
|
112
|
+
accessToken: string;
|
|
130
113
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Event types emitted by SchwabClient
|
|
133
|
-
*/
|
|
134
|
-
type SchwabClientEventType = 'tickerUpdate' | 'optionUpdate' | 'optionTrade' | 'connected' | 'disconnected' | 'error';
|
|
135
|
-
/**
|
|
136
|
-
* Event listener callback type
|
|
137
|
-
*/
|
|
138
|
-
type SchwabEventListener<T> = (data: T) => void;
|
|
139
114
|
/**
|
|
140
115
|
* SchwabClient handles real-time streaming connections to the Charles Schwab API
|
|
141
116
|
* via WebSockets.
|
|
@@ -165,7 +140,8 @@ type SchwabEventListener<T> = (data: T) => void;
|
|
|
165
140
|
* client.subscribe(['SPY', 'SPY 240517C00500000']); // Equity and option
|
|
166
141
|
* ```
|
|
167
142
|
*/
|
|
168
|
-
export declare class SchwabClient {
|
|
143
|
+
export declare class SchwabClient extends BaseBrokerClient {
|
|
144
|
+
protected readonly brokerName = "Schwab";
|
|
169
145
|
/** Schwab OAuth access token */
|
|
170
146
|
private accessToken;
|
|
171
147
|
/** WebSocket connection */
|
|
@@ -182,36 +158,14 @@ export declare class SchwabClient {
|
|
|
182
158
|
private streamSocketUrl;
|
|
183
159
|
/** Request ID counter */
|
|
184
160
|
private requestId;
|
|
185
|
-
/** Currently subscribed symbols */
|
|
186
|
-
private subscribedSymbols;
|
|
187
161
|
/** Map from Schwab symbol to OCC symbol */
|
|
188
162
|
private schwabToOccMap;
|
|
189
163
|
/** Map from OCC symbol to Schwab symbol */
|
|
190
164
|
private occToSchwabMap;
|
|
191
|
-
/** Cached ticker data */
|
|
192
|
-
private tickerCache;
|
|
193
|
-
/** Cached option data */
|
|
194
|
-
private optionCache;
|
|
195
|
-
/** Base open interest from REST API */
|
|
196
|
-
private baseOpenInterest;
|
|
197
|
-
/** Cumulative estimated OI change from intraday trades */
|
|
198
|
-
private cumulativeOIChange;
|
|
199
|
-
/** History of intraday trades */
|
|
200
|
-
private intradayTrades;
|
|
201
|
-
/** Event listeners */
|
|
202
|
-
private eventListeners;
|
|
203
|
-
/** Reconnection attempt counter */
|
|
204
|
-
private reconnectAttempts;
|
|
205
|
-
/** Maximum reconnection attempts */
|
|
206
|
-
private readonly maxReconnectAttempts;
|
|
207
|
-
/** Reconnection delay in ms */
|
|
208
|
-
private readonly baseReconnectDelay;
|
|
209
165
|
/** Keepalive interval handle */
|
|
210
166
|
private keepaliveInterval;
|
|
211
167
|
/** Schwab API base URL */
|
|
212
168
|
private readonly apiBaseUrl;
|
|
213
|
-
/** Whether to log verbose debug information */
|
|
214
|
-
private readonly verbose;
|
|
215
169
|
/**
|
|
216
170
|
* Creates a new SchwabClient instance.
|
|
217
171
|
*
|
|
@@ -219,10 +173,7 @@ export declare class SchwabClient {
|
|
|
219
173
|
* @param options.accessToken - Schwab OAuth access token (required)
|
|
220
174
|
* @param options.verbose - Whether to log verbose debug information (default: false)
|
|
221
175
|
*/
|
|
222
|
-
constructor(options:
|
|
223
|
-
accessToken: string;
|
|
224
|
-
verbose?: boolean;
|
|
225
|
-
});
|
|
176
|
+
constructor(options: SchwabClientOptions);
|
|
226
177
|
/**
|
|
227
178
|
* Establishes a streaming connection to Schwab.
|
|
228
179
|
*
|
|
@@ -280,40 +231,6 @@ export declare class SchwabClient {
|
|
|
280
231
|
* @param occSymbols - Array of OCC option symbols to fetch data for
|
|
281
232
|
*/
|
|
282
233
|
fetchOpenInterest(occSymbols: string[]): Promise<void>;
|
|
283
|
-
/**
|
|
284
|
-
* Returns cached option data for a symbol.
|
|
285
|
-
*/
|
|
286
|
-
getOption(occSymbol: string): NormalizedOption | undefined;
|
|
287
|
-
/**
|
|
288
|
-
* Returns all cached options.
|
|
289
|
-
*/
|
|
290
|
-
getAllOptions(): Map<string, NormalizedOption>;
|
|
291
|
-
/**
|
|
292
|
-
* Registers an event listener.
|
|
293
|
-
*/
|
|
294
|
-
on<T>(event: SchwabClientEventType, listener: SchwabEventListener<T>): this;
|
|
295
|
-
/**
|
|
296
|
-
* Removes an event listener.
|
|
297
|
-
*/
|
|
298
|
-
off<T>(event: SchwabClientEventType, listener: SchwabEventListener<T>): this;
|
|
299
|
-
/**
|
|
300
|
-
* Returns intraday trades for an option.
|
|
301
|
-
*/
|
|
302
|
-
getIntradayTrades(occSymbol: string): IntradayTrade[];
|
|
303
|
-
/**
|
|
304
|
-
* Returns flow summary for an option.
|
|
305
|
-
*/
|
|
306
|
-
getFlowSummary(occSymbol: string): {
|
|
307
|
-
buyVolume: number;
|
|
308
|
-
sellVolume: number;
|
|
309
|
-
unknownVolume: number;
|
|
310
|
-
netOIChange: number;
|
|
311
|
-
tradeCount: number;
|
|
312
|
-
};
|
|
313
|
-
/**
|
|
314
|
-
* Resets intraday tracking data.
|
|
315
|
-
*/
|
|
316
|
-
resetIntradayData(occSymbols?: string[]): void;
|
|
317
234
|
/**
|
|
318
235
|
* Gets user preferences containing streaming info from Schwab.
|
|
319
236
|
*/
|
|
@@ -388,10 +305,6 @@ export declare class SchwabClient {
|
|
|
388
305
|
* This provides depth of market which can indicate intraday activity.
|
|
389
306
|
*/
|
|
390
307
|
private handleOptionsBook;
|
|
391
|
-
/**
|
|
392
|
-
* Records a trade and updates OI tracking.
|
|
393
|
-
*/
|
|
394
|
-
private recordTrade;
|
|
395
308
|
/**
|
|
396
309
|
* Processes a contract from the option chain response.
|
|
397
310
|
*/
|
|
@@ -400,36 +313,19 @@ export declare class SchwabClient {
|
|
|
400
313
|
* Starts keepalive interval.
|
|
401
314
|
*/
|
|
402
315
|
private startKeepalive;
|
|
403
|
-
/**
|
|
404
|
-
* Determines aggressor side from trade price vs NBBO.
|
|
405
|
-
*/
|
|
406
|
-
private determineAggressorSide;
|
|
407
|
-
/**
|
|
408
|
-
* Calculates estimated OI change from trade.
|
|
409
|
-
*/
|
|
410
|
-
private calculateOIChangeFromTrade;
|
|
411
|
-
/**
|
|
412
|
-
* Calculates live open interest.
|
|
413
|
-
*/
|
|
414
|
-
private calculateLiveOpenInterest;
|
|
415
316
|
/**
|
|
416
317
|
* Converts Schwab option symbol to OCC format.
|
|
417
318
|
* Schwab format: "AAPL 240517C00170000" (6-char padded underlying)
|
|
418
319
|
*/
|
|
419
320
|
private schwabToOcc;
|
|
420
|
-
/**
|
|
421
|
-
* Normalizes an OCC symbol to consistent format.
|
|
422
|
-
* Removes extra spaces, ensures proper formatting.
|
|
423
|
-
*/
|
|
424
|
-
private normalizeOccSymbol;
|
|
425
321
|
/**
|
|
426
322
|
* Converts OCC symbol to Schwab format (space-padded).
|
|
427
323
|
*/
|
|
428
324
|
private toSchwabOptionSymbol;
|
|
429
325
|
/**
|
|
430
|
-
* Checks if symbol is an option symbol.
|
|
326
|
+
* Checks if symbol is an option symbol (Schwab format allows spaces).
|
|
431
327
|
*/
|
|
432
|
-
private
|
|
328
|
+
private isSchwabOptionSymbol;
|
|
433
329
|
/**
|
|
434
330
|
* Attempts to reconnect with exponential backoff.
|
|
435
331
|
*/
|
|
@@ -438,17 +334,4 @@ export declare class SchwabClient {
|
|
|
438
334
|
* Sends a message to the WebSocket.
|
|
439
335
|
*/
|
|
440
336
|
private sendMessage;
|
|
441
|
-
/**
|
|
442
|
-
* Emits an event to all listeners.
|
|
443
|
-
*/
|
|
444
|
-
private emit;
|
|
445
|
-
/**
|
|
446
|
-
* Converts value to number, handling NaN and null.
|
|
447
|
-
*/
|
|
448
|
-
private toNumber;
|
|
449
|
-
/**
|
|
450
|
-
* Sleep utility.
|
|
451
|
-
*/
|
|
452
|
-
private sleep;
|
|
453
337
|
}
|
|
454
|
-
export {};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SchwabClient = void 0;
|
|
4
4
|
const occ_1 = require("../../utils/occ");
|
|
5
|
+
const BaseBrokerClient_1 = require("./BaseBrokerClient");
|
|
5
6
|
/**
|
|
6
7
|
* Schwab streaming field enums for LEVELONE_OPTIONS
|
|
7
8
|
*/
|
|
@@ -78,7 +79,7 @@ var OptionsBookFields;
|
|
|
78
79
|
* Regex pattern to identify OCC option symbols
|
|
79
80
|
* Schwab uses space-padded format: "AAPL 240517C00170000"
|
|
80
81
|
*/
|
|
81
|
-
const
|
|
82
|
+
const SCHWAB_OCC_OPTION_PATTERN = /^.{1,6}\s*\d{6}[CP]\d{8}$/;
|
|
82
83
|
/**
|
|
83
84
|
* SchwabClient handles real-time streaming connections to the Charles Schwab API
|
|
84
85
|
* via WebSockets.
|
|
@@ -108,7 +109,7 @@ const OCC_OPTION_PATTERN = /^.{1,6}\s*\d{6}[CP]\d{8}$/;
|
|
|
108
109
|
* client.subscribe(['SPY', 'SPY 240517C00500000']); // Equity and option
|
|
109
110
|
* ```
|
|
110
111
|
*/
|
|
111
|
-
class SchwabClient {
|
|
112
|
+
class SchwabClient extends BaseBrokerClient_1.BaseBrokerClient {
|
|
112
113
|
/**
|
|
113
114
|
* Creates a new SchwabClient instance.
|
|
114
115
|
*
|
|
@@ -117,6 +118,8 @@ class SchwabClient {
|
|
|
117
118
|
* @param options.verbose - Whether to log verbose debug information (default: false)
|
|
118
119
|
*/
|
|
119
120
|
constructor(options) {
|
|
121
|
+
super(options);
|
|
122
|
+
this.brokerName = 'Schwab';
|
|
120
123
|
/** WebSocket connection */
|
|
121
124
|
this.ws = null;
|
|
122
125
|
/** Connection state */
|
|
@@ -131,43 +134,15 @@ class SchwabClient {
|
|
|
131
134
|
this.streamSocketUrl = null;
|
|
132
135
|
/** Request ID counter */
|
|
133
136
|
this.requestId = 0;
|
|
134
|
-
/** Currently subscribed symbols */
|
|
135
|
-
this.subscribedSymbols = new Set();
|
|
136
137
|
/** Map from Schwab symbol to OCC symbol */
|
|
137
138
|
this.schwabToOccMap = new Map();
|
|
138
139
|
/** Map from OCC symbol to Schwab symbol */
|
|
139
140
|
this.occToSchwabMap = new Map();
|
|
140
|
-
/** Cached ticker data */
|
|
141
|
-
this.tickerCache = new Map();
|
|
142
|
-
/** Cached option data */
|
|
143
|
-
this.optionCache = new Map();
|
|
144
|
-
/** Base open interest from REST API */
|
|
145
|
-
this.baseOpenInterest = new Map();
|
|
146
|
-
/** Cumulative estimated OI change from intraday trades */
|
|
147
|
-
this.cumulativeOIChange = new Map();
|
|
148
|
-
/** History of intraday trades */
|
|
149
|
-
this.intradayTrades = new Map();
|
|
150
|
-
/** Event listeners */
|
|
151
|
-
this.eventListeners = new Map();
|
|
152
|
-
/** Reconnection attempt counter */
|
|
153
|
-
this.reconnectAttempts = 0;
|
|
154
|
-
/** Maximum reconnection attempts */
|
|
155
|
-
this.maxReconnectAttempts = 5;
|
|
156
|
-
/** Reconnection delay in ms */
|
|
157
|
-
this.baseReconnectDelay = 1000;
|
|
158
141
|
/** Keepalive interval handle */
|
|
159
142
|
this.keepaliveInterval = null;
|
|
160
143
|
/** Schwab API base URL */
|
|
161
144
|
this.apiBaseUrl = 'https://api.schwabapi.com';
|
|
162
145
|
this.accessToken = options.accessToken;
|
|
163
|
-
this.verbose = options.verbose ?? false;
|
|
164
|
-
// Initialize event listener maps
|
|
165
|
-
this.eventListeners.set('tickerUpdate', new Set());
|
|
166
|
-
this.eventListeners.set('optionUpdate', new Set());
|
|
167
|
-
this.eventListeners.set('optionTrade', new Set());
|
|
168
|
-
this.eventListeners.set('connected', new Set());
|
|
169
|
-
this.eventListeners.set('disconnected', new Set());
|
|
170
|
-
this.eventListeners.set('error', new Set());
|
|
171
146
|
}
|
|
172
147
|
// ==================== Public API ====================
|
|
173
148
|
/**
|
|
@@ -233,7 +208,7 @@ class SchwabClient {
|
|
|
233
208
|
const tickers = [];
|
|
234
209
|
const options = [];
|
|
235
210
|
for (const symbol of symbols) {
|
|
236
|
-
if (this.
|
|
211
|
+
if (this.isSchwabOptionSymbol(symbol)) {
|
|
237
212
|
options.push(this.toSchwabOptionSymbol(symbol));
|
|
238
213
|
}
|
|
239
214
|
else {
|
|
@@ -261,7 +236,7 @@ class SchwabClient {
|
|
|
261
236
|
const tickers = [];
|
|
262
237
|
const options = [];
|
|
263
238
|
for (const symbol of symbols) {
|
|
264
|
-
if (this.
|
|
239
|
+
if (this.isSchwabOptionSymbol(symbol)) {
|
|
265
240
|
options.push(this.toSchwabOptionSymbol(symbol));
|
|
266
241
|
}
|
|
267
242
|
else {
|
|
@@ -281,7 +256,7 @@ class SchwabClient {
|
|
|
281
256
|
*/
|
|
282
257
|
unsubscribeFromAll() {
|
|
283
258
|
const allSymbols = Array.from(this.subscribedSymbols);
|
|
284
|
-
const allOptionSymbols = allSymbols.filter(s => this.
|
|
259
|
+
const allOptionSymbols = allSymbols.filter(s => this.isSchwabOptionSymbol(s)).map(s => this.toSchwabOptionSymbol(s));
|
|
285
260
|
this.subscribedSymbols.clear();
|
|
286
261
|
// unsub from all equities
|
|
287
262
|
if (allSymbols.length > 0) {
|
|
@@ -395,101 +370,41 @@ class SchwabClient {
|
|
|
395
370
|
});
|
|
396
371
|
await Promise.all(fetchPromises);
|
|
397
372
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Returns cached option data for a symbol.
|
|
400
|
-
*/
|
|
401
|
-
getOption(occSymbol) {
|
|
402
|
-
return this.optionCache.get(this.normalizeOccSymbol(occSymbol));
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Returns all cached options.
|
|
406
|
-
*/
|
|
407
|
-
getAllOptions() {
|
|
408
|
-
return new Map(this.optionCache);
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Registers an event listener.
|
|
412
|
-
*/
|
|
413
|
-
on(event, listener) {
|
|
414
|
-
const listeners = this.eventListeners.get(event);
|
|
415
|
-
if (listeners) {
|
|
416
|
-
listeners.add(listener);
|
|
417
|
-
}
|
|
418
|
-
return this;
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Removes an event listener.
|
|
422
|
-
*/
|
|
423
|
-
off(event, listener) {
|
|
424
|
-
const listeners = this.eventListeners.get(event);
|
|
425
|
-
if (listeners) {
|
|
426
|
-
listeners.delete(listener);
|
|
427
|
-
}
|
|
428
|
-
return this;
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Returns intraday trades for an option.
|
|
432
|
-
*/
|
|
433
|
-
getIntradayTrades(occSymbol) {
|
|
434
|
-
return this.intradayTrades.get(this.normalizeOccSymbol(occSymbol)) ?? [];
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Returns flow summary for an option.
|
|
438
|
-
*/
|
|
439
|
-
getFlowSummary(occSymbol) {
|
|
440
|
-
const normalizedSymbol = this.normalizeOccSymbol(occSymbol);
|
|
441
|
-
const trades = this.intradayTrades.get(normalizedSymbol) ?? [];
|
|
442
|
-
let buyVolume = 0;
|
|
443
|
-
let sellVolume = 0;
|
|
444
|
-
let unknownVolume = 0;
|
|
445
|
-
for (const trade of trades) {
|
|
446
|
-
switch (trade.aggressorSide) {
|
|
447
|
-
case 'buy':
|
|
448
|
-
buyVolume += trade.size;
|
|
449
|
-
break;
|
|
450
|
-
case 'sell':
|
|
451
|
-
sellVolume += trade.size;
|
|
452
|
-
break;
|
|
453
|
-
case 'unknown':
|
|
454
|
-
unknownVolume += trade.size;
|
|
455
|
-
break;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
buyVolume,
|
|
460
|
-
sellVolume,
|
|
461
|
-
unknownVolume,
|
|
462
|
-
netOIChange: this.cumulativeOIChange.get(normalizedSymbol) ?? 0,
|
|
463
|
-
tradeCount: trades.length,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Resets intraday tracking data.
|
|
468
|
-
*/
|
|
469
|
-
resetIntradayData(occSymbols) {
|
|
470
|
-
const symbolsToReset = occSymbols?.map(s => this.normalizeOccSymbol(s))
|
|
471
|
-
?? Array.from(this.intradayTrades.keys());
|
|
472
|
-
for (const symbol of symbolsToReset) {
|
|
473
|
-
this.intradayTrades.delete(symbol);
|
|
474
|
-
this.cumulativeOIChange.set(symbol, 0);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
373
|
// ==================== Private Methods ====================
|
|
478
374
|
/**
|
|
479
375
|
* Gets user preferences containing streaming info from Schwab.
|
|
480
376
|
*/
|
|
481
377
|
async getUserPreferences() {
|
|
482
378
|
try {
|
|
483
|
-
const
|
|
379
|
+
const url = `${this.apiBaseUrl}/trader/v1/userPreference`;
|
|
380
|
+
const headers = this.getAuthHeaders();
|
|
381
|
+
const authHeader = (headers['Authorization'] ?? headers['authorization'] ?? '');
|
|
382
|
+
const maskedAuth = typeof authHeader === 'string' && authHeader.length > 12 ? `${authHeader.slice(0, 12)}...` : authHeader;
|
|
383
|
+
if (this.verbose) {
|
|
384
|
+
console.debug('[Schwab] GET userPreference', url, { maskedAuthorization: maskedAuth, headerKeys: Object.keys(headers) });
|
|
385
|
+
}
|
|
386
|
+
const response = await fetch(url, {
|
|
484
387
|
method: 'GET',
|
|
485
|
-
headers
|
|
388
|
+
headers,
|
|
486
389
|
});
|
|
390
|
+
const text = await response.text();
|
|
487
391
|
if (!response.ok) {
|
|
488
|
-
|
|
489
|
-
|
|
392
|
+
if (this.verbose) {
|
|
393
|
+
console.error('[Schwab] userPreference failed', response.status, response.statusText, text);
|
|
394
|
+
}
|
|
395
|
+
let parsed = text;
|
|
396
|
+
try {
|
|
397
|
+
parsed = JSON.parse(text);
|
|
398
|
+
}
|
|
399
|
+
catch { }
|
|
400
|
+
this.emit('error', new Error(`Failed to get user preferences: ${response.status} ${response.statusText} - ${typeof parsed === 'string' ? parsed : JSON.stringify(parsed)}`));
|
|
490
401
|
return null;
|
|
491
402
|
}
|
|
492
|
-
|
|
403
|
+
const json = JSON.parse(text);
|
|
404
|
+
if (this.verbose) {
|
|
405
|
+
console.debug('[Schwab] userPreference success', json);
|
|
406
|
+
}
|
|
407
|
+
return json;
|
|
493
408
|
}
|
|
494
409
|
catch (error) {
|
|
495
410
|
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
@@ -828,46 +743,6 @@ class SchwabClient {
|
|
|
828
743
|
this.optionCache.set(occSymbol, existing);
|
|
829
744
|
this.emit('optionUpdate', existing);
|
|
830
745
|
}
|
|
831
|
-
/**
|
|
832
|
-
* Records a trade and updates OI tracking.
|
|
833
|
-
*/
|
|
834
|
-
recordTrade(occSymbol, price, size, bid, ask, timestamp) {
|
|
835
|
-
const aggressorSide = this.determineAggressorSide(price, bid, ask);
|
|
836
|
-
// Get option type for OI calculation
|
|
837
|
-
let optionType = 'call';
|
|
838
|
-
try {
|
|
839
|
-
const parsed = (0, occ_1.parseOCCSymbol)(occSymbol);
|
|
840
|
-
optionType = parsed.optionType;
|
|
841
|
-
}
|
|
842
|
-
catch {
|
|
843
|
-
const existing = this.optionCache.get(occSymbol);
|
|
844
|
-
if (existing)
|
|
845
|
-
optionType = existing.optionType;
|
|
846
|
-
}
|
|
847
|
-
const estimatedOIChange = this.calculateOIChangeFromTrade(aggressorSide, size, optionType);
|
|
848
|
-
const currentChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
|
|
849
|
-
this.cumulativeOIChange.set(occSymbol, currentChange + estimatedOIChange);
|
|
850
|
-
if (this.verbose && estimatedOIChange !== 0) {
|
|
851
|
-
const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
|
|
852
|
-
const newLiveOI = Math.max(0, baseOI + currentChange + estimatedOIChange);
|
|
853
|
-
console.log(`[Schwab:OI] ${occSymbol} trade: price=${price.toFixed(2)}, size=${size}, aggressor=${aggressorSide}, OI change=${estimatedOIChange > 0 ? '+' : ''}${estimatedOIChange}, liveOI=${newLiveOI} (base=${baseOI}, cumulative=${currentChange + estimatedOIChange})`);
|
|
854
|
-
}
|
|
855
|
-
const trade = {
|
|
856
|
-
occSymbol,
|
|
857
|
-
price,
|
|
858
|
-
size,
|
|
859
|
-
bid,
|
|
860
|
-
ask,
|
|
861
|
-
aggressorSide,
|
|
862
|
-
timestamp,
|
|
863
|
-
estimatedOIChange,
|
|
864
|
-
};
|
|
865
|
-
if (!this.intradayTrades.has(occSymbol)) {
|
|
866
|
-
this.intradayTrades.set(occSymbol, []);
|
|
867
|
-
}
|
|
868
|
-
this.intradayTrades.get(occSymbol).push(trade);
|
|
869
|
-
this.emit('optionTrade', trade);
|
|
870
|
-
}
|
|
871
746
|
/**
|
|
872
747
|
* Processes a contract from the option chain response.
|
|
873
748
|
*/
|
|
@@ -930,38 +805,6 @@ class SchwabClient {
|
|
|
930
805
|
}
|
|
931
806
|
}, 60000); // Every 60 seconds
|
|
932
807
|
}
|
|
933
|
-
/**
|
|
934
|
-
* Determines aggressor side from trade price vs NBBO.
|
|
935
|
-
*/
|
|
936
|
-
determineAggressorSide(tradePrice, bid, ask) {
|
|
937
|
-
if (bid <= 0 || ask <= 0)
|
|
938
|
-
return 'unknown';
|
|
939
|
-
const spread = ask - bid;
|
|
940
|
-
const tolerance = spread > 0 ? spread * 0.001 : 0.001;
|
|
941
|
-
if (tradePrice >= ask - tolerance) {
|
|
942
|
-
return 'buy';
|
|
943
|
-
}
|
|
944
|
-
else if (tradePrice <= bid + tolerance) {
|
|
945
|
-
return 'sell';
|
|
946
|
-
}
|
|
947
|
-
return 'unknown';
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
|
-
* Calculates estimated OI change from trade.
|
|
951
|
-
*/
|
|
952
|
-
calculateOIChangeFromTrade(aggressorSide, size, _optionType) {
|
|
953
|
-
if (aggressorSide === 'unknown')
|
|
954
|
-
return 0;
|
|
955
|
-
return aggressorSide === 'buy' ? size : -size;
|
|
956
|
-
}
|
|
957
|
-
/**
|
|
958
|
-
* Calculates live open interest.
|
|
959
|
-
*/
|
|
960
|
-
calculateLiveOpenInterest(occSymbol) {
|
|
961
|
-
const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
|
|
962
|
-
const cumulativeChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
|
|
963
|
-
return Math.max(0, baseOI + cumulativeChange);
|
|
964
|
-
}
|
|
965
808
|
/**
|
|
966
809
|
* Converts Schwab option symbol to OCC format.
|
|
967
810
|
* Schwab format: "AAPL 240517C00170000" (6-char padded underlying)
|
|
@@ -975,20 +818,6 @@ class SchwabClient {
|
|
|
975
818
|
// Just normalize by removing extra spaces and ensuring proper padding
|
|
976
819
|
return this.normalizeOccSymbol(schwabSymbol);
|
|
977
820
|
}
|
|
978
|
-
/**
|
|
979
|
-
* Normalizes an OCC symbol to consistent format.
|
|
980
|
-
* Removes extra spaces, ensures proper formatting.
|
|
981
|
-
*/
|
|
982
|
-
normalizeOccSymbol(symbol) {
|
|
983
|
-
// Remove all spaces and reformat
|
|
984
|
-
const stripped = symbol.replace(/\s+/g, '');
|
|
985
|
-
// Match the parts: ROOT + YYMMDD + C/P + 8-digit strike
|
|
986
|
-
const match = stripped.match(/^([A-Z]+)(\d{6})([CP])(\d{8})$/);
|
|
987
|
-
if (match) {
|
|
988
|
-
return `${match[1]}${match[2]}${match[3]}${match[4]}`;
|
|
989
|
-
}
|
|
990
|
-
return stripped;
|
|
991
|
-
}
|
|
992
821
|
/**
|
|
993
822
|
* Converts OCC symbol to Schwab format (space-padded).
|
|
994
823
|
*/
|
|
@@ -1009,10 +838,10 @@ class SchwabClient {
|
|
|
1009
838
|
return occSymbol;
|
|
1010
839
|
}
|
|
1011
840
|
/**
|
|
1012
|
-
* Checks if symbol is an option symbol.
|
|
841
|
+
* Checks if symbol is an option symbol (Schwab format allows spaces).
|
|
1013
842
|
*/
|
|
1014
|
-
|
|
1015
|
-
return
|
|
843
|
+
isSchwabOptionSymbol(symbol) {
|
|
844
|
+
return SCHWAB_OCC_OPTION_PATTERN.test(symbol) || /\d{6}[CP]\d{8}/.test(symbol.replace(/\s+/g, ''));
|
|
1016
845
|
}
|
|
1017
846
|
/**
|
|
1018
847
|
* Attempts to reconnect with exponential backoff.
|
|
@@ -1023,10 +852,8 @@ class SchwabClient {
|
|
|
1023
852
|
return;
|
|
1024
853
|
}
|
|
1025
854
|
this.reconnectAttempts++;
|
|
1026
|
-
const delay = this.
|
|
1027
|
-
|
|
1028
|
-
console.log(`[Schwab:WS] Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
1029
|
-
}
|
|
855
|
+
const delay = this.getReconnectDelay();
|
|
856
|
+
this.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
1030
857
|
await this.sleep(delay);
|
|
1031
858
|
try {
|
|
1032
859
|
await this.connect();
|
|
@@ -1043,41 +870,5 @@ class SchwabClient {
|
|
|
1043
870
|
this.ws.send(JSON.stringify(message));
|
|
1044
871
|
}
|
|
1045
872
|
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Emits an event to all listeners.
|
|
1048
|
-
*/
|
|
1049
|
-
emit(event, data) {
|
|
1050
|
-
const listeners = this.eventListeners.get(event);
|
|
1051
|
-
if (listeners) {
|
|
1052
|
-
listeners.forEach(listener => {
|
|
1053
|
-
try {
|
|
1054
|
-
listener(data);
|
|
1055
|
-
}
|
|
1056
|
-
catch (error) {
|
|
1057
|
-
console.error('Event listener error:', error);
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
/**
|
|
1063
|
-
* Converts value to number, handling NaN and null.
|
|
1064
|
-
*/
|
|
1065
|
-
toNumber(value) {
|
|
1066
|
-
if (value === null || value === undefined)
|
|
1067
|
-
return 0;
|
|
1068
|
-
if (typeof value === 'number')
|
|
1069
|
-
return isNaN(value) ? 0 : value;
|
|
1070
|
-
if (typeof value === 'string') {
|
|
1071
|
-
const num = parseFloat(value);
|
|
1072
|
-
return isNaN(num) ? 0 : num;
|
|
1073
|
-
}
|
|
1074
|
-
return 0;
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* Sleep utility.
|
|
1078
|
-
*/
|
|
1079
|
-
sleep(ms) {
|
|
1080
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1081
|
-
}
|
|
1082
873
|
}
|
|
1083
874
|
exports.SchwabClient = SchwabClient;
|