@fullstackcraftllc/floe 0.0.3 → 0.0.4

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 CHANGED
@@ -30,6 +30,28 @@ The same library that is used in Full Stack Craft's various fintech products inc
30
30
  - 💪 **Type-Safe** - Full TypeScript support
31
31
  - ⚡ **Zero Dependencies** - Lightweight and fast
32
32
 
33
+ ## Broker Support Roadmap
34
+
35
+ Due to the overwhelming variety of how broker APIs structure their data (and how they make it available), there is a wide variety of how much support we can provide out-of-the-box for different brokers, summarized in this table:
36
+
37
+ | Broker | Black-Scholes | Greeks | Open Interest Based Exposures | Options-Book Based Exposures | Implied PDF Calculations |
38
+ |-----------------------|--------------|--------|-------------------------------|------------------------------|-------------------------|
39
+ | Tradier (via WebSocket) | ✅ | ✅ | ✅ | ✅ | Coming soon |
40
+ | Schwab (via WebSocket) | Coming soon | Coming soon | Coming soon | Coming soon | Coming soon |
41
+ | Tastytrade (via WebSocket - DXLink Streamer) | Coming soon | Coming soon | Coming soon | Coming soon | Coming soon |
42
+ | TradeStation (via HTTP Streaming) | Coming soon | Coming soon | Coming soon | Coming soon | Coming soon |
43
+
44
+ Ideally all aspects of `floe` will be available for all brokers, but this will take time to determine as we work through the various data structures and formats that each broker provides.
45
+
46
+ ## Unsupported Brokers
47
+
48
+ The following brokers have no public API:
49
+
50
+ - Fidelity
51
+ - Robinhood
52
+
53
+ If your broker is not listed above, you can still use `floe` by normalizing your broker's data structures to match the expected input types. With options, you can get quite far with `floe` just by having the market price for the underlying and each option. (From those alone you can back out the IV, greeks, and exposures.)
54
+
33
55
  ## Installation
34
56
 
35
57
  ```bash
@@ -5,7 +5,9 @@ import { NormalizedOption, NormalizedTicker } from "../types";
5
5
  */
6
6
  export declare enum Broker {
7
7
  /** Tradier brokerage API */
8
- TRADIER = "tradier"
8
+ TRADIER = "tradier",
9
+ /** TastyTrade brokerage API (uses DxLink WebSocket) */
10
+ TASTYTRADE = "tastytrade"
9
11
  }
10
12
  /**
11
13
  * Event types emitted by the FloeClient.
@@ -69,12 +71,10 @@ export declare class FloeClient {
69
71
  private currentSubscribedTickers;
70
72
  /** List of option symbols (OCC format) currently subscribed to */
71
73
  private currentSubscribedOptions;
72
- /** Cache of the latest normalized ticker data */
73
- private normalizedTickers;
74
- /** Cache of the latest normalized option data */
75
- private normalizedOptions;
76
74
  /** Tradier broker client instance */
77
75
  private tradierClient;
76
+ /** TastyTrade broker client instance */
77
+ private tastyTradeClient;
78
78
  /** Event listeners registry for the EventEmitter pattern */
79
79
  private eventListeners;
80
80
  /** Callback for ticker data changes (legacy callback pattern) */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FloeClient = exports.Broker = void 0;
4
4
  const TradierClient_1 = require("./brokers/TradierClient");
5
+ const TastyTradeClient_1 = require("./brokers/TastyTradeClient");
5
6
  /**
6
7
  * Supported broker integrations for the FloeClient.
7
8
  * @enum {string}
@@ -10,7 +11,8 @@ var Broker;
10
11
  (function (Broker) {
11
12
  /** Tradier brokerage API */
12
13
  Broker["TRADIER"] = "tradier";
13
- // Future brokers can be added here
14
+ /** TastyTrade brokerage API (uses DxLink WebSocket) */
15
+ Broker["TASTYTRADE"] = "tastytrade";
14
16
  })(Broker || (exports.Broker = Broker = {}));
15
17
  /**
16
18
  * FloeClient provides a unified, broker-agnostic interface for subscribing to
@@ -61,12 +63,10 @@ class FloeClient {
61
63
  this.currentSubscribedTickers = [];
62
64
  /** List of option symbols (OCC format) currently subscribed to */
63
65
  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
66
  /** Tradier broker client instance */
69
67
  this.tradierClient = null;
68
+ /** TastyTrade broker client instance */
69
+ this.tastyTradeClient = null;
70
70
  /** Event listeners registry for the EventEmitter pattern */
71
71
  this.eventListeners = new Map();
72
72
  /** Callback for ticker data changes (legacy callback pattern) */
@@ -119,6 +119,27 @@ class FloeClient {
119
119
  // Connect to the streaming API
120
120
  await this.tradierClient.connect();
121
121
  break;
122
+ case Broker.TASTYTRADE:
123
+ // For TastyTrade, authKey is the session token
124
+ this.tastyTradeClient = new TastyTradeClient_1.TastyTradeClient({
125
+ sessionToken: authKey,
126
+ });
127
+ // Wire up TastyTradeClient events to FloeClient events
128
+ this.tastyTradeClient.on('tickerUpdate', (ticker) => {
129
+ this.emit('tickerUpdate', ticker);
130
+ });
131
+ this.tastyTradeClient.on('optionUpdate', (option) => {
132
+ this.emit('optionUpdate', option);
133
+ });
134
+ this.tastyTradeClient.on('error', (error) => {
135
+ this.emit('error', error);
136
+ });
137
+ this.tastyTradeClient.on('disconnected', () => {
138
+ this.emit('disconnected', { broker, reason: 'DxLink WebSocket disconnected' });
139
+ });
140
+ // Connect to the streaming API
141
+ await this.tastyTradeClient.connect();
142
+ break;
122
143
  default:
123
144
  throw new Error(`Unsupported broker: ${broker}`);
124
145
  }
@@ -140,6 +161,10 @@ class FloeClient {
140
161
  this.tradierClient.disconnect();
141
162
  this.tradierClient = null;
142
163
  }
164
+ if (this.tastyTradeClient) {
165
+ this.tastyTradeClient.disconnect();
166
+ this.tastyTradeClient = null;
167
+ }
143
168
  const broker = this.currentBroker;
144
169
  this.currentBroker = null;
145
170
  this.currentSubscribedTickers = [];
@@ -170,6 +195,9 @@ class FloeClient {
170
195
  case Broker.TRADIER:
171
196
  this.tradierClient?.subscribe(tickers);
172
197
  break;
198
+ case Broker.TASTYTRADE:
199
+ this.tastyTradeClient?.subscribe(tickers);
200
+ break;
173
201
  default:
174
202
  throw new Error(`Unsupported broker: ${this.currentBroker}`);
175
203
  }
@@ -204,6 +232,9 @@ class FloeClient {
204
232
  case Broker.TRADIER:
205
233
  this.tradierClient?.subscribe(symbols);
206
234
  break;
235
+ case Broker.TASTYTRADE:
236
+ this.tastyTradeClient?.subscribe(symbols);
237
+ break;
207
238
  default:
208
239
  throw new Error(`Unsupported broker: ${this.currentBroker}`);
209
240
  }
@@ -230,6 +261,9 @@ class FloeClient {
230
261
  case Broker.TRADIER:
231
262
  this.tradierClient?.unsubscribe(tickers);
232
263
  break;
264
+ case Broker.TASTYTRADE:
265
+ this.tastyTradeClient?.unsubscribe(tickers);
266
+ break;
233
267
  default:
234
268
  throw new Error(`Unsupported broker: ${this.currentBroker}`);
235
269
  }
@@ -256,6 +290,9 @@ class FloeClient {
256
290
  case Broker.TRADIER:
257
291
  this.tradierClient?.unsubscribe(symbols);
258
292
  break;
293
+ case Broker.TASTYTRADE:
294
+ this.tastyTradeClient?.unsubscribe(symbols);
295
+ break;
259
296
  default:
260
297
  throw new Error(`Unsupported broker: ${this.currentBroker}`);
261
298
  }
@@ -300,6 +337,9 @@ class FloeClient {
300
337
  case Broker.TRADIER:
301
338
  await this.tradierClient?.fetchOpenInterest(symbolsToFetch);
302
339
  break;
340
+ case Broker.TASTYTRADE:
341
+ await this.tastyTradeClient?.fetchOpenInterest(symbolsToFetch);
342
+ break;
303
343
  default:
304
344
  throw new Error(`Unsupported broker: ${this.currentBroker}`);
305
345
  }
@@ -320,6 +360,8 @@ class FloeClient {
320
360
  switch (this.currentBroker) {
321
361
  case Broker.TRADIER:
322
362
  return this.tradierClient?.getOption(occSymbol);
363
+ case Broker.TASTYTRADE:
364
+ return this.tastyTradeClient?.getOption(occSymbol);
323
365
  default:
324
366
  return undefined;
325
367
  }
@@ -341,6 +383,8 @@ class FloeClient {
341
383
  switch (this.currentBroker) {
342
384
  case Broker.TRADIER:
343
385
  return this.tradierClient?.getAllOptions() ?? new Map();
386
+ case Broker.TASTYTRADE:
387
+ return this.tastyTradeClient?.getAllOptions() ?? new Map();
344
388
  default:
345
389
  return new Map();
346
390
  }
@@ -0,0 +1,384 @@
1
+ import { NormalizedOption } from '../../types';
2
+ /**
3
+ * TastyTrade option chain item
4
+ */
5
+ interface TastyTradeOptionChainItem {
6
+ symbol: string;
7
+ 'instrument-type': string;
8
+ underlying: string;
9
+ strike: number;
10
+ 'expiration-date': string;
11
+ 'expiration-type': string;
12
+ 'option-type': 'C' | 'P';
13
+ 'root-symbol': string;
14
+ 'streamer-symbol': string;
15
+ bid?: number;
16
+ ask?: number;
17
+ 'bid-size'?: number;
18
+ 'ask-size'?: number;
19
+ last?: number;
20
+ volume?: number;
21
+ 'open-interest'?: number;
22
+ delta?: number;
23
+ gamma?: number;
24
+ theta?: number;
25
+ vega?: number;
26
+ 'implied-volatility'?: number;
27
+ }
28
+ /**
29
+ * Aggressor side of a trade
30
+ */
31
+ export type AggressorSide = 'buy' | 'sell' | 'unknown';
32
+ /**
33
+ * Intraday trade information with aggressor classification
34
+ */
35
+ export interface IntradayTrade {
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;
52
+ }
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
+ /**
62
+ * TastyTradeClient handles real-time streaming connections to the TastyTrade API
63
+ * via DxLink WebSockets.
64
+ *
65
+ * @remarks
66
+ * This client manages WebSocket connections to TastyTrade's DxLink streaming API,
67
+ * normalizes incoming quote and trade data, and emits events for upstream
68
+ * consumption by the FloeClient.
69
+ *
70
+ * Authentication flow:
71
+ * 1. Login to TastyTrade API to get session token (optional, can pass directly)
72
+ * 2. Use session token to get API quote token from /api-quote-tokens
73
+ * 3. Connect to DxLink WebSocket using the quote token
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const client = new TastyTradeClient({
78
+ * sessionToken: 'your-session-token'
79
+ * });
80
+ *
81
+ * client.on('tickerUpdate', (ticker) => {
82
+ * console.log(`${ticker.symbol}: ${ticker.spot}`);
83
+ * });
84
+ *
85
+ * await client.connect();
86
+ * client.subscribe(['SPY', '.SPXW231215C4500']); // Equity and option
87
+ * ```
88
+ */
89
+ export declare class TastyTradeClient {
90
+ /** TastyTrade session token */
91
+ private sessionToken;
92
+ /** DxLink API quote token */
93
+ private quoteToken;
94
+ /** DxLink WebSocket URL */
95
+ private dxLinkUrl;
96
+ /** WebSocket connection */
97
+ private ws;
98
+ /** Connection state */
99
+ private connected;
100
+ /** Authorization state */
101
+ private authorized;
102
+ /** Feed channel ID */
103
+ private feedChannelId;
104
+ /** Feed channel opened */
105
+ private feedChannelOpened;
106
+ /** Currently subscribed symbols */
107
+ private subscribedSymbols;
108
+ /** Map from streamer symbol to OCC symbol */
109
+ private streamerToOccMap;
110
+ /** Map from OCC symbol to streamer symbol */
111
+ 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
+ /** Keepalive interval handle */
131
+ private keepaliveInterval;
132
+ /** Keepalive timeout in seconds */
133
+ private readonly keepaliveTimeoutSeconds;
134
+ /** TastyTrade API base URL */
135
+ private readonly apiBaseUrl;
136
+ /** Whether to use sandbox environment */
137
+ private readonly sandbox;
138
+ /**
139
+ * Creates a new TastyTradeClient instance.
140
+ *
141
+ * @param options - Client configuration options
142
+ * @param options.sessionToken - TastyTrade session token (required)
143
+ * @param options.sandbox - Whether to use sandbox environment (default: false)
144
+ */
145
+ constructor(options: {
146
+ sessionToken: string;
147
+ sandbox?: boolean;
148
+ });
149
+ /**
150
+ * Creates a TastyTradeClient by logging in with username/password.
151
+ *
152
+ * @param username - TastyTrade username
153
+ * @param password - TastyTrade password
154
+ * @param options - Additional options
155
+ * @returns Promise resolving to configured TastyTradeClient
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * const client = await TastyTradeClient.fromCredentials(
160
+ * 'your-username',
161
+ * 'your-password'
162
+ * );
163
+ * await client.connect();
164
+ * ```
165
+ */
166
+ static fromCredentials(username: string, password: string, options?: {
167
+ sandbox?: boolean;
168
+ rememberMe?: boolean;
169
+ }): Promise<TastyTradeClient>;
170
+ /**
171
+ * Establishes a streaming connection to TastyTrade via DxLink.
172
+ *
173
+ * @returns Promise that resolves when connected and authorized
174
+ * @throws {Error} If token retrieval or WebSocket connection fails
175
+ */
176
+ connect(): Promise<void>;
177
+ /**
178
+ * Disconnects from the TastyTrade streaming API.
179
+ */
180
+ disconnect(): void;
181
+ /**
182
+ * Subscribes to real-time updates for the specified symbols.
183
+ *
184
+ * @param symbols - Array of ticker symbols and/or OCC option symbols
185
+ *
186
+ * @remarks
187
+ * For options, you can pass either:
188
+ * - OCC format symbols (e.g., 'SPY240119C00500000')
189
+ * - TastyTrade streamer symbols (e.g., '.SPXW240119C4500')
190
+ *
191
+ * The client will convert OCC symbols to streamer symbols automatically.
192
+ */
193
+ subscribe(symbols: string[]): void;
194
+ /**
195
+ * Unsubscribes from real-time updates for the specified symbols.
196
+ *
197
+ * @param symbols - Array of symbols to unsubscribe from
198
+ */
199
+ unsubscribe(symbols: string[]): void;
200
+ /**
201
+ * Returns whether the client is currently connected.
202
+ */
203
+ isConnected(): boolean;
204
+ /**
205
+ * Fetches options chain data from TastyTrade REST API.
206
+ *
207
+ * @param symbol - Underlying symbol (e.g., 'SPY')
208
+ * @returns Array of option chain items
209
+ */
210
+ fetchOptionsChain(symbol: string): Promise<TastyTradeOptionChainItem[]>;
211
+ /**
212
+ * Fetches open interest and other static data for subscribed options.
213
+ *
214
+ * @param occSymbols - Array of OCC option symbols to fetch data for
215
+ */
216
+ fetchOpenInterest(occSymbols: string[]): Promise<void>;
217
+ /**
218
+ * Returns cached option data for a symbol.
219
+ */
220
+ getOption(occSymbol: string): NormalizedOption | undefined;
221
+ /**
222
+ * Returns all cached options.
223
+ */
224
+ getAllOptions(): Map<string, NormalizedOption>;
225
+ /**
226
+ * Registers an event listener.
227
+ */
228
+ on<T>(event: TastyTradeClientEventType, listener: TastyTradeEventListener<T>): this;
229
+ /**
230
+ * Removes an event listener.
231
+ */
232
+ off<T>(event: TastyTradeClientEventType, listener: TastyTradeEventListener<T>): this;
233
+ /**
234
+ * Returns intraday trades for an option.
235
+ */
236
+ getIntradayTrades(occSymbol: string): IntradayTrade[];
237
+ /**
238
+ * Returns flow summary for an option.
239
+ */
240
+ getFlowSummary(occSymbol: string): {
241
+ buyVolume: number;
242
+ sellVolume: number;
243
+ unknownVolume: number;
244
+ netOIChange: number;
245
+ tradeCount: number;
246
+ };
247
+ /**
248
+ * Resets intraday tracking data.
249
+ */
250
+ resetIntradayData(occSymbols?: string[]): void;
251
+ /**
252
+ * Gets API quote token from TastyTrade.
253
+ */
254
+ private getQuoteToken;
255
+ /**
256
+ * Connects to DxLink WebSocket.
257
+ */
258
+ private connectWebSocket;
259
+ /**
260
+ * Sends SETUP message to DxLink.
261
+ */
262
+ private sendSetup;
263
+ /**
264
+ * Sends AUTH message to DxLink.
265
+ */
266
+ private sendAuth;
267
+ /**
268
+ * Opens a FEED channel.
269
+ */
270
+ private openFeedChannel;
271
+ /**
272
+ * Configures the feed channel with desired event fields.
273
+ */
274
+ private setupFeed;
275
+ /**
276
+ * Sends feed subscription message.
277
+ */
278
+ private sendFeedSubscription;
279
+ /**
280
+ * Gets streamer symbol from OCC or ticker symbol.
281
+ */
282
+ private getStreamerSymbol;
283
+ /**
284
+ * Converts streamer symbol back to OCC format.
285
+ */
286
+ private streamerSymbolToOCC;
287
+ /**
288
+ * Starts keepalive interval.
289
+ */
290
+ private startKeepalive;
291
+ /**
292
+ * Handles incoming WebSocket messages.
293
+ */
294
+ private handleMessage;
295
+ /**
296
+ * Handles AUTH_STATE message.
297
+ */
298
+ private handleAuthState;
299
+ /**
300
+ * Handles CHANNEL_OPENED message.
301
+ */
302
+ private handleChannelOpened;
303
+ /**
304
+ * Handles FEED_DATA message.
305
+ */
306
+ private handleFeedData;
307
+ /**
308
+ * Processes a single event from FEED_DATA.
309
+ */
310
+ private processEventData;
311
+ /**
312
+ * Handles Quote events.
313
+ */
314
+ private handleQuoteEvent;
315
+ /**
316
+ * Handles Trade events.
317
+ */
318
+ private handleTradeEvent;
319
+ /**
320
+ * Handles Greeks events.
321
+ */
322
+ private handleGreeksEvent;
323
+ /**
324
+ * Handles Summary events (includes open interest).
325
+ */
326
+ private handleSummaryEvent;
327
+ /**
328
+ * Updates ticker from Quote event.
329
+ */
330
+ private updateTickerFromQuote;
331
+ /**
332
+ * Updates ticker from Trade event.
333
+ */
334
+ private updateTickerFromTrade;
335
+ /**
336
+ * Updates option from Quote event.
337
+ */
338
+ private updateOptionFromQuote;
339
+ /**
340
+ * Updates option from Trade event.
341
+ */
342
+ private updateOptionFromTrade;
343
+ /**
344
+ * Determines aggressor side from trade price vs NBBO.
345
+ */
346
+ private determineAggressorSide;
347
+ /**
348
+ * Calculates estimated OI change from trade.
349
+ */
350
+ private calculateOIChangeFromTrade;
351
+ /**
352
+ * Calculates live open interest.
353
+ */
354
+ private calculateLiveOpenInterest;
355
+ /**
356
+ * Handles DxLink error messages.
357
+ */
358
+ private handleError;
359
+ /**
360
+ * Attempts to reconnect with exponential backoff.
361
+ */
362
+ private attemptReconnect;
363
+ /**
364
+ * Checks if symbol is an OCC option symbol.
365
+ */
366
+ private isOptionSymbol;
367
+ /**
368
+ * Sends a message to the WebSocket.
369
+ */
370
+ private sendMessage;
371
+ /**
372
+ * Emits an event to all listeners.
373
+ */
374
+ private emit;
375
+ /**
376
+ * Converts value to number, handling NaN and null.
377
+ */
378
+ private toNumber;
379
+ /**
380
+ * Sleep utility.
381
+ */
382
+ private sleep;
383
+ }
384
+ export {};