@fullstackcraftllc/floe 0.0.1 → 0.0.2

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.
@@ -0,0 +1,465 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FloeClient = exports.Broker = void 0;
4
+ const TradierClient_1 = require("./brokers/TradierClient");
5
+ /**
6
+ * Supported broker integrations for the FloeClient.
7
+ * @enum {string}
8
+ */
9
+ var Broker;
10
+ (function (Broker) {
11
+ /** Tradier brokerage API */
12
+ Broker["TRADIER"] = "tradier";
13
+ // Future brokers can be added here
14
+ })(Broker || (exports.Broker = Broker = {}));
15
+ /**
16
+ * FloeClient provides a unified, broker-agnostic interface for subscribing to
17
+ * real-time market data including stock tickers and options.
18
+ *
19
+ * @remarks
20
+ * The client normalizes data from various brokers into a consistent format,
21
+ * allowing consumers to switch brokers without changing their application code.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const client = new FloeClient();
26
+ *
27
+ * // Connect to a broker
28
+ * client.connect(Broker.TRADIER, 'your-api-key');
29
+ *
30
+ * // Subscribe to updates using the event emitter pattern
31
+ * client.on('tickerUpdate', (ticker) => {
32
+ * console.log(`${ticker.symbol}: ${ticker.price}`);
33
+ * });
34
+ *
35
+ * // Or use the callback pattern
36
+ * client.onTickerDataChange((ticker) => {
37
+ * console.log(`${ticker.symbol}: ${ticker.price}`);
38
+ * });
39
+ *
40
+ * // Subscribe to specific tickers
41
+ * client.subscribeToTickers(['AAPL', 'GOOGL', 'MSFT']);
42
+ * ```
43
+ */
44
+ class FloeClient {
45
+ /**
46
+ * Creates a new FloeClient instance.
47
+ *
48
+ * @remarks
49
+ * The client is created in a disconnected state. Call {@link connect} to
50
+ * establish a connection to a broker before subscribing to data.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const client = new FloeClient();
55
+ * ```
56
+ */
57
+ constructor() {
58
+ /** Currently connected broker, or null if not connected */
59
+ this.currentBroker = null;
60
+ /** List of ticker symbols currently subscribed to */
61
+ this.currentSubscribedTickers = [];
62
+ /** List of option symbols (OCC format) currently subscribed to */
63
+ this.currentSubscribedOptions = [];
64
+ /** Cache of the latest normalized ticker data */
65
+ this.normalizedTickers = [];
66
+ /** Cache of the latest normalized option data */
67
+ this.normalizedOptions = [];
68
+ /** Tradier broker client instance */
69
+ this.tradierClient = null;
70
+ /** Event listeners registry for the EventEmitter pattern */
71
+ this.eventListeners = new Map();
72
+ /** Callback for ticker data changes (legacy callback pattern) */
73
+ this.tickerDataCallback = null;
74
+ /** Callback for option data changes (legacy callback pattern) */
75
+ this.optionDataCallback = null;
76
+ // Initialize event listener maps for each event type
77
+ this.eventListeners.set('tickerUpdate', new Set());
78
+ this.eventListeners.set('optionUpdate', new Set());
79
+ this.eventListeners.set('error', new Set());
80
+ this.eventListeners.set('connected', new Set());
81
+ this.eventListeners.set('disconnected', new Set());
82
+ }
83
+ /**
84
+ * Establishes a connection to a broker's API.
85
+ *
86
+ * @param broker - The broker to connect to (e.g., Broker.TRADIER)
87
+ * @param authKey - The API authentication key or token for the broker
88
+ *
89
+ * @throws {Error} Throws if the specified broker is not supported
90
+ *
91
+ * @remarks
92
+ * Must be called before subscribing to any market data. Only one broker
93
+ * connection is active at a time; calling connect again will switch brokers.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * await client.connect(Broker.TRADIER, 'your-tradier-api-key');
98
+ * ```
99
+ */
100
+ async connect(broker, authKey) {
101
+ this.currentBroker = broker;
102
+ // Connection logic to the broker's API using the authKey
103
+ switch (broker.toLowerCase()) {
104
+ case Broker.TRADIER:
105
+ this.tradierClient = new TradierClient_1.TradierClient(authKey);
106
+ // Wire up TradierClient events to FloeClient events
107
+ this.tradierClient.on('tickerUpdate', (ticker) => {
108
+ this.emit('tickerUpdate', ticker);
109
+ });
110
+ this.tradierClient.on('optionUpdate', (option) => {
111
+ this.emit('optionUpdate', option);
112
+ });
113
+ this.tradierClient.on('error', (error) => {
114
+ this.emit('error', error);
115
+ });
116
+ this.tradierClient.on('disconnected', () => {
117
+ this.emit('disconnected', { broker, reason: 'WebSocket disconnected' });
118
+ });
119
+ // Connect to the streaming API
120
+ await this.tradierClient.connect();
121
+ break;
122
+ default:
123
+ throw new Error(`Unsupported broker: ${broker}`);
124
+ }
125
+ this.emit('connected', { broker });
126
+ }
127
+ /**
128
+ * Disconnects from the current broker.
129
+ *
130
+ * @remarks
131
+ * Closes the WebSocket connection and clears all subscriptions.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * client.disconnect();
136
+ * ```
137
+ */
138
+ disconnect() {
139
+ if (this.tradierClient) {
140
+ this.tradierClient.disconnect();
141
+ this.tradierClient = null;
142
+ }
143
+ const broker = this.currentBroker;
144
+ this.currentBroker = null;
145
+ this.currentSubscribedTickers = [];
146
+ this.currentSubscribedOptions = [];
147
+ if (broker) {
148
+ this.emit('disconnected', { broker, reason: 'Client disconnect' });
149
+ }
150
+ }
151
+ /**
152
+ * Subscribes to real-time updates for the specified stock ticker symbols.
153
+ *
154
+ * @param tickers - Array of ticker symbols to subscribe to (e.g., ['AAPL', 'GOOGL'])
155
+ *
156
+ * @throws {Error} Throws if no broker connection has been established
157
+ *
158
+ * @remarks
159
+ * Ticker updates will be delivered via the 'tickerUpdate' event or through
160
+ * the callback registered with {@link onTickerDataChange}.
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * client.subscribeToTickers(['AAPL', 'GOOGL', 'MSFT']);
165
+ * ```
166
+ */
167
+ subscribeToTickers(tickers) {
168
+ this.currentSubscribedTickers.push(...tickers);
169
+ switch (this.currentBroker) {
170
+ case Broker.TRADIER:
171
+ this.tradierClient?.subscribe(tickers);
172
+ break;
173
+ default:
174
+ throw new Error(`Unsupported broker: ${this.currentBroker}`);
175
+ }
176
+ }
177
+ /**
178
+ * Subscribes to real-time updates for the specified option contracts.
179
+ *
180
+ * @param symbols - Array of option symbols in OCC format
181
+ * (e.g., ['AAPL230120C00150000'] for AAPL $150 Call expiring Jan 20, 2023)
182
+ *
183
+ * @throws {Error} Throws if no broker connection has been established
184
+ *
185
+ * @remarks
186
+ * Option symbols must be in the standard OCC (Options Clearing Corporation) format:
187
+ * - Root symbol (up to 6 characters, left-padded)
188
+ * - Expiration date (YYMMDD)
189
+ * - Option type (C for call, P for put)
190
+ * - Strike price (8 digits, price × 1000, left-padded with zeros)
191
+ *
192
+ * Option updates will be delivered via the 'optionUpdate' event or through
193
+ * the callback registered with {@link onOptionDataChange}.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * // Subscribe to AAPL $150 Call expiring Jan 20, 2023
198
+ * client.subscribeToOptions(['AAPL230120C00150000']);
199
+ * ```
200
+ */
201
+ subscribeToOptions(symbols) {
202
+ this.currentSubscribedOptions.push(...symbols);
203
+ switch (this.currentBroker) {
204
+ case Broker.TRADIER:
205
+ this.tradierClient?.subscribe(symbols);
206
+ break;
207
+ default:
208
+ throw new Error(`Unsupported broker: ${this.currentBroker}`);
209
+ }
210
+ }
211
+ /**
212
+ * Unsubscribes from real-time updates for the specified stock ticker symbols.
213
+ *
214
+ * @param tickers - Array of ticker symbols to unsubscribe from
215
+ *
216
+ * @throws {Error} Throws if no broker connection has been established
217
+ *
218
+ * @remarks
219
+ * After unsubscribing, no further updates will be received for these tickers.
220
+ * Has no effect if the specified tickers were not previously subscribed.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * client.unsubscribeFromTickers(['AAPL', 'GOOGL']);
225
+ * ```
226
+ */
227
+ unsubscribeFromTickers(tickers) {
228
+ this.currentSubscribedTickers = this.currentSubscribedTickers.filter(ticker => !tickers.includes(ticker));
229
+ switch (this.currentBroker) {
230
+ case Broker.TRADIER:
231
+ this.tradierClient?.unsubscribe(tickers);
232
+ break;
233
+ default:
234
+ throw new Error(`Unsupported broker: ${this.currentBroker}`);
235
+ }
236
+ }
237
+ /**
238
+ * Unsubscribes from real-time updates for the specified option contracts.
239
+ *
240
+ * @param symbols - Array of option symbols in OCC format to unsubscribe from
241
+ *
242
+ * @throws {Error} Throws if no broker connection has been established
243
+ *
244
+ * @remarks
245
+ * After unsubscribing, no further updates will be received for these options.
246
+ * Has no effect if the specified options were not previously subscribed.
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * client.unsubscribeFromOptions(['AAPL230120C00150000']);
251
+ * ```
252
+ */
253
+ unsubscribeFromOptions(symbols) {
254
+ this.currentSubscribedOptions = this.currentSubscribedOptions.filter(symbol => !symbols.includes(symbol));
255
+ switch (this.currentBroker) {
256
+ case Broker.TRADIER:
257
+ this.tradierClient?.unsubscribe(symbols);
258
+ break;
259
+ default:
260
+ throw new Error(`Unsupported broker: ${this.currentBroker}`);
261
+ }
262
+ }
263
+ // ==================== Event Emitter Pattern ====================
264
+ /**
265
+ * Registers an event listener for the specified event type.
266
+ *
267
+ * @template T - The event type
268
+ * @param event - The event type to listen for
269
+ * @param listener - The callback function to invoke when the event occurs
270
+ * @returns The FloeClient instance for method chaining
271
+ *
272
+ * @remarks
273
+ * Multiple listeners can be registered for the same event type.
274
+ * Use {@link off} to remove a listener when it's no longer needed.
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * client
279
+ * .on('tickerUpdate', (ticker) => console.log(ticker))
280
+ * .on('error', (error) => console.error(error));
281
+ * ```
282
+ */
283
+ on(event, listener) {
284
+ const listeners = this.eventListeners.get(event);
285
+ if (listeners) {
286
+ listeners.add(listener);
287
+ }
288
+ return this;
289
+ }
290
+ /**
291
+ * Removes an event listener for the specified event type.
292
+ *
293
+ * @template T - The event type
294
+ * @param event - The event type to stop listening for
295
+ * @param listener - The callback function to remove
296
+ * @returns The FloeClient instance for method chaining
297
+ *
298
+ * @remarks
299
+ * The listener must be the exact same function reference that was passed to {@link on}.
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * const handler = (ticker) => console.log(ticker);
304
+ * client.on('tickerUpdate', handler);
305
+ * // Later...
306
+ * client.off('tickerUpdate', handler);
307
+ * ```
308
+ */
309
+ off(event, listener) {
310
+ const listeners = this.eventListeners.get(event);
311
+ if (listeners) {
312
+ listeners.delete(listener);
313
+ }
314
+ return this;
315
+ }
316
+ /**
317
+ * Registers a one-time event listener that automatically removes itself after firing.
318
+ *
319
+ * @template T - The event type
320
+ * @param event - The event type to listen for
321
+ * @param listener - The callback function to invoke once when the event occurs
322
+ * @returns The FloeClient instance for method chaining
323
+ *
324
+ * @example
325
+ * ```typescript
326
+ * client.once('connected', ({ broker }) => {
327
+ * console.log(`Connected to ${broker}`);
328
+ * });
329
+ * ```
330
+ */
331
+ once(event, listener) {
332
+ const onceWrapper = ((data) => {
333
+ this.off(event, onceWrapper);
334
+ listener(data);
335
+ });
336
+ return this.on(event, onceWrapper);
337
+ }
338
+ /**
339
+ * Emits an event to all registered listeners.
340
+ *
341
+ * @template T - The event type
342
+ * @param event - The event type to emit
343
+ * @param data - The event payload
344
+ *
345
+ * @internal
346
+ * @remarks
347
+ * This method is used internally to dispatch events. It also triggers
348
+ * legacy callback handlers for backwards compatibility.
349
+ */
350
+ emit(event, data) {
351
+ const listeners = this.eventListeners.get(event);
352
+ if (listeners) {
353
+ listeners.forEach(listener => {
354
+ try {
355
+ listener(data);
356
+ }
357
+ catch (error) {
358
+ // Emit error event if a listener throws (but avoid infinite loops)
359
+ if (event !== 'error') {
360
+ this.emit('error', error instanceof Error ? error : new Error(String(error)));
361
+ }
362
+ }
363
+ });
364
+ }
365
+ // Also trigger legacy callbacks for backwards compatibility
366
+ if (event === 'tickerUpdate' && this.tickerDataCallback) {
367
+ this.tickerDataCallback(data);
368
+ }
369
+ if (event === 'optionUpdate' && this.optionDataCallback) {
370
+ this.optionDataCallback(data);
371
+ }
372
+ }
373
+ // ==================== Callback Pattern (Legacy) ====================
374
+ /**
375
+ * Registers a callback to be invoked whenever ticker data is updated.
376
+ *
377
+ * @param callback - Function to call with the updated ticker data
378
+ *
379
+ * @deprecated Prefer using {@link on}('tickerUpdate', callback) for new code.
380
+ * The event emitter pattern supports multiple listeners and provides
381
+ * better lifecycle management.
382
+ *
383
+ * @remarks
384
+ * Only one callback can be registered at a time. Calling this method again
385
+ * will replace the previous callback. For multiple listeners, use {@link on}.
386
+ *
387
+ * @example
388
+ * ```typescript
389
+ * client.onTickerDataChange((ticker) => {
390
+ * console.log(`${ticker.symbol} updated: ${ticker.price}`);
391
+ * });
392
+ * ```
393
+ */
394
+ onTickerDataChange(callback) {
395
+ this.tickerDataCallback = callback;
396
+ }
397
+ /**
398
+ * Registers a callback to be invoked whenever option data is updated.
399
+ *
400
+ * @param callback - Function to call with the updated option data
401
+ *
402
+ * @deprecated Prefer using {@link on}('optionUpdate', callback) for new code.
403
+ * The event emitter pattern supports multiple listeners and provides
404
+ * better lifecycle management.
405
+ *
406
+ * @remarks
407
+ * Only one callback can be registered at a time. Calling this method again
408
+ * will replace the previous callback. For multiple listeners, use {@link on}.
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * client.onOptionDataChange((option) => {
413
+ * console.log(`${option.symbol} bid: ${option.bid}, ask: ${option.ask}`);
414
+ * });
415
+ * ```
416
+ */
417
+ onOptionDataChange(callback) {
418
+ this.optionDataCallback = callback;
419
+ }
420
+ // ==================== Utility Methods ====================
421
+ /**
422
+ * Returns the list of currently subscribed ticker symbols.
423
+ *
424
+ * @returns Array of ticker symbols currently subscribed to
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * const tickers = client.getSubscribedTickers();
429
+ * console.log(`Subscribed to: ${tickers.join(', ')}`);
430
+ * ```
431
+ */
432
+ getSubscribedTickers() {
433
+ return [...this.currentSubscribedTickers];
434
+ }
435
+ /**
436
+ * Returns the list of currently subscribed option symbols.
437
+ *
438
+ * @returns Array of option symbols (OCC format) currently subscribed to
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * const options = client.getSubscribedOptions();
443
+ * console.log(`Subscribed to ${options.length} options`);
444
+ * ```
445
+ */
446
+ getSubscribedOptions() {
447
+ return [...this.currentSubscribedOptions];
448
+ }
449
+ /**
450
+ * Returns whether the client is currently connected to a broker.
451
+ *
452
+ * @returns True if connected to a broker, false otherwise
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * if (client.isConnected()) {
457
+ * client.subscribeToTickers(['AAPL']);
458
+ * }
459
+ * ```
460
+ */
461
+ isConnected() {
462
+ return this.currentBroker !== null;
463
+ }
464
+ }
465
+ exports.FloeClient = FloeClient;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Event types emitted by TradierClient
3
+ */
4
+ type TradierClientEventType = 'tickerUpdate' | 'optionUpdate' | 'connected' | 'disconnected' | 'error';
5
+ /**
6
+ * Event listener callback type
7
+ */
8
+ type TradierEventListener<T> = (data: T) => void;
9
+ /**
10
+ * TradierClient handles real-time streaming connections to the Tradier API.
11
+ *
12
+ * @remarks
13
+ * This client manages WebSocket connections to Tradier's streaming API,
14
+ * normalizes incoming quote and trade data, and emits events for upstream
15
+ * consumption by the FloeClient.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const client = new TradierClient('your-api-key');
20
+ *
21
+ * client.on('tickerUpdate', (ticker) => {
22
+ * console.log(`${ticker.symbol}: ${ticker.spot}`);
23
+ * });
24
+ *
25
+ * await client.connect();
26
+ * client.subscribe(['QQQ', 'AAPL 240119C00500000']);
27
+ * ```
28
+ */
29
+ export declare class TradierClient {
30
+ /** Tradier API authentication token */
31
+ private authKey;
32
+ /** Current streaming session */
33
+ private streamSession;
34
+ /** WebSocket connection */
35
+ private ws;
36
+ /** Connection state */
37
+ private connected;
38
+ /** Currently subscribed symbols (tickers and options) */
39
+ private subscribedSymbols;
40
+ /** Cached ticker data (for merging quote and trade events) */
41
+ private tickerCache;
42
+ /** Cached option data (for merging quote and trade events) */
43
+ private optionCache;
44
+ /** Event listeners */
45
+ private eventListeners;
46
+ /** Reconnection attempt counter */
47
+ private reconnectAttempts;
48
+ /** Maximum reconnection attempts */
49
+ private readonly maxReconnectAttempts;
50
+ /** Reconnection delay in ms (doubles with each attempt) */
51
+ private readonly baseReconnectDelay;
52
+ /** Tradier API base URL */
53
+ private readonly apiBaseUrl;
54
+ /** Tradier WebSocket URL */
55
+ private readonly wsUrl;
56
+ /**
57
+ * Creates a new TradierClient instance.
58
+ *
59
+ * @param authKey - Tradier API access token
60
+ */
61
+ constructor(authKey: string);
62
+ /**
63
+ * Establishes a streaming connection to Tradier.
64
+ *
65
+ * @returns Promise that resolves when connected
66
+ * @throws {Error} If session creation or WebSocket connection fails
67
+ */
68
+ connect(): Promise<void>;
69
+ /**
70
+ * Disconnects from the Tradier streaming API.
71
+ */
72
+ disconnect(): void;
73
+ /**
74
+ * Subscribes to real-time updates for the specified symbols.
75
+ *
76
+ * @param symbols - Array of ticker symbols and/or OCC option symbols
77
+ */
78
+ subscribe(symbols: string[]): void;
79
+ /**
80
+ * Unsubscribes from real-time updates for the specified symbols.
81
+ *
82
+ * @param symbols - Array of symbols to unsubscribe from
83
+ *
84
+ * @remarks
85
+ * Note: Tradier's streaming API doesn't support unsubscription for individual
86
+ * symbols. This method removes them from local tracking. To fully unsubscribe,
87
+ * you would need to disconnect and reconnect with the new symbol list.
88
+ */
89
+ unsubscribe(symbols: string[]): void;
90
+ /**
91
+ * Returns whether the client is currently connected.
92
+ */
93
+ isConnected(): boolean;
94
+ /**
95
+ * Registers an event listener.
96
+ *
97
+ * @param event - Event type to listen for
98
+ * @param listener - Callback function
99
+ */
100
+ on<T>(event: TradierClientEventType, listener: TradierEventListener<T>): this;
101
+ /**
102
+ * Removes an event listener.
103
+ *
104
+ * @param event - Event type
105
+ * @param listener - Callback function to remove
106
+ */
107
+ off<T>(event: TradierClientEventType, listener: TradierEventListener<T>): this;
108
+ /**
109
+ * Creates a streaming session with Tradier API.
110
+ */
111
+ private createStreamSession;
112
+ /**
113
+ * Connects to the Tradier WebSocket.
114
+ */
115
+ private connectWebSocket;
116
+ /**
117
+ * Attempts to reconnect with exponential backoff.
118
+ */
119
+ private attemptReconnect;
120
+ /**
121
+ * Handles incoming WebSocket messages.
122
+ */
123
+ private handleMessage;
124
+ /**
125
+ * Handles quote events (bid/ask updates).
126
+ */
127
+ private handleQuoteEvent;
128
+ /**
129
+ * Handles trade events (last price/volume updates).
130
+ */
131
+ private handleTradeEvent;
132
+ /**
133
+ * Updates ticker data from a quote event.
134
+ */
135
+ private updateTickerFromQuote;
136
+ /**
137
+ * Updates ticker data from a trade event.
138
+ */
139
+ private updateTickerFromTrade;
140
+ /**
141
+ * Updates option data from a quote event.
142
+ */
143
+ private updateOptionFromQuote;
144
+ /**
145
+ * Updates option data from a trade event.
146
+ */
147
+ private updateOptionFromTrade;
148
+ /**
149
+ * Checks if a symbol is an OCC option symbol.
150
+ */
151
+ private isOptionSymbol;
152
+ /**
153
+ * Emits an event to all registered listeners.
154
+ */
155
+ private emit;
156
+ /**
157
+ * Simple sleep utility.
158
+ */
159
+ private sleep;
160
+ }
161
+ export {};