@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.
@@ -2,13 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TradierClient = void 0;
4
4
  const occ_1 = require("../../utils/occ");
5
- /**
6
- * Regex pattern to identify OCC option symbols
7
- * Matches both compact format (e.g., AAPL230120C00150000) and
8
- * padded format (e.g., 'AAPL 230120C00150000')
9
- * Pattern: 1-6 char root + YYMMDD + C/P + 8-digit strike
10
- */
11
- const OCC_OPTION_PATTERN = /^.{1,6}\d{6}[CP]\d{8}$/;
5
+ const BaseBrokerClient_1 = require("./BaseBrokerClient");
12
6
  /**
13
7
  * TradierClient handles real-time streaming connections to the Tradier API.
14
8
  *
@@ -19,7 +13,7 @@ const OCC_OPTION_PATTERN = /^.{1,6}\d{6}[CP]\d{8}$/;
19
13
  *
20
14
  * @example
21
15
  * ```typescript
22
- * const client = new TradierClient('your-api-key');
16
+ * const client = new TradierClient({ authToken: 'your-api-key' });
23
17
  *
24
18
  * client.on('tickerUpdate', (ticker) => {
25
19
  * console.log(`${ticker.symbol}: ${ticker.spot}`);
@@ -29,63 +23,28 @@ const OCC_OPTION_PATTERN = /^.{1,6}\d{6}[CP]\d{8}$/;
29
23
  * client.subscribe(['QQQ', 'AAPL 240119C00500000']);
30
24
  * ```
31
25
  */
32
- class TradierClient {
26
+ class TradierClient extends BaseBrokerClient_1.BaseBrokerClient {
33
27
  /**
34
28
  * Creates a new TradierClient instance.
35
29
  *
36
- * @param authToken - Tradier API auth token
37
- * @param options - Optional configuration options
30
+ * @param options - Client configuration options
31
+ * @param options.authToken - Tradier API auth token (required)
38
32
  * @param options.verbose - Whether to log verbose debug information (default: false)
39
33
  */
40
- constructor(authToken, options) {
34
+ constructor(options) {
35
+ super(options);
36
+ this.brokerName = 'Tradier';
41
37
  /** Current streaming session */
42
38
  this.streamSession = null;
43
39
  /** WebSocket connection */
44
40
  this.ws = null;
45
41
  /** Connection state */
46
42
  this.connected = false;
47
- /** Currently subscribed symbols (tickers and options) */
48
- this.subscribedSymbols = new Set();
49
- /** Cached ticker data (for merging quote and trade events) */
50
- this.tickerCache = new Map();
51
- /** Cached option data (for merging quote and trade events) */
52
- this.optionCache = new Map();
53
- /**
54
- * Base open interest from REST API - used as t=0 reference for live OI calculation
55
- * Key: OCC symbol, Value: open interest at start of day / time of fetch
56
- */
57
- this.baseOpenInterest = new Map();
58
- /**
59
- * Cumulative estimated OI change from intraday trades
60
- * Key: OCC symbol, Value: net estimated change (positive = more contracts opened)
61
- */
62
- this.cumulativeOIChange = new Map();
63
- /**
64
- * History of intraday trades with aggressor classification
65
- * Key: OCC symbol, Value: array of trades
66
- */
67
- this.intradayTrades = new Map();
68
- /** Event listeners */
69
- this.eventListeners = new Map();
70
- /** Reconnection attempt counter */
71
- this.reconnectAttempts = 0;
72
- /** Maximum reconnection attempts */
73
- this.maxReconnectAttempts = 5;
74
- /** Reconnection delay in ms (doubles with each attempt) */
75
- this.baseReconnectDelay = 1000;
76
43
  /** Tradier API base URL */
77
44
  this.apiBaseUrl = 'https://api.tradier.com/v1';
78
45
  /** Tradier WebSocket URL */
79
46
  this.wsUrl = 'wss://ws.tradier.com/v1/markets/events';
80
- this.authToken = authToken;
81
- this.verbose = options?.verbose ?? false;
82
- // Initialize event listener maps
83
- this.eventListeners.set('tickerUpdate', new Set());
84
- this.eventListeners.set('optionUpdate', new Set());
85
- this.eventListeners.set('optionTrade', new Set());
86
- this.eventListeners.set('connected', new Set());
87
- this.eventListeners.set('disconnected', new Set());
88
- this.eventListeners.set('error', new Set());
47
+ this.authToken = options.authToken;
89
48
  }
90
49
  // ==================== Public API ====================
91
50
  /**
@@ -317,49 +276,6 @@ class TradierClient {
317
276
  });
318
277
  await Promise.all(fetchPromises);
319
278
  }
320
- /**
321
- * Returns the cached option data for a symbol.
322
- *
323
- * @param occSymbol - OCC option symbol
324
- * @returns Cached option data or undefined
325
- */
326
- getOption(occSymbol) {
327
- return this.optionCache.get(occSymbol);
328
- }
329
- /**
330
- * Returns all cached options.
331
- *
332
- * @returns Map of OCC symbols to option data
333
- */
334
- getAllOptions() {
335
- return new Map(this.optionCache);
336
- }
337
- /**
338
- * Registers an event listener.
339
- *
340
- * @param event - Event type to listen for
341
- * @param listener - Callback function
342
- */
343
- on(event, listener) {
344
- const listeners = this.eventListeners.get(event);
345
- if (listeners) {
346
- listeners.add(listener);
347
- }
348
- return this;
349
- }
350
- /**
351
- * Removes an event listener.
352
- *
353
- * @param event - Event type
354
- * @param listener - Callback function to remove
355
- */
356
- off(event, listener) {
357
- const listeners = this.eventListeners.get(event);
358
- if (listeners) {
359
- listeners.delete(listener);
360
- }
361
- return this;
362
- }
363
279
  // ==================== Private Methods ====================
364
280
  /**
365
281
  * Creates a streaming session with Tradier API.
@@ -488,7 +404,7 @@ class TradierClient {
488
404
  handleQuoteEvent(event) {
489
405
  const { symbol } = event;
490
406
  const timestamp = parseInt(event.biddate, 10) || Date.now();
491
- const isOption = this.isOptionSymbol(symbol);
407
+ const isOption = this.isTradierOptionSymbol(symbol);
492
408
  if (isOption) {
493
409
  this.updateOptionFromQuote(symbol, event, timestamp);
494
410
  }
@@ -502,7 +418,7 @@ class TradierClient {
502
418
  handleTradeEvent(event) {
503
419
  const { symbol } = event;
504
420
  const timestamp = parseInt(event.date, 10) || Date.now();
505
- const isOption = this.isOptionSymbol(symbol);
421
+ const isOption = this.isTradierOptionSymbol(symbol);
506
422
  if (isOption) {
507
423
  this.updateOptionFromTrade(symbol, event, timestamp);
508
424
  }
@@ -517,7 +433,7 @@ class TradierClient {
517
433
  handleTimesaleEvent(event) {
518
434
  const { symbol } = event;
519
435
  const timestamp = parseInt(event.date, 10) || Date.now();
520
- const isOption = this.isOptionSymbol(symbol);
436
+ const isOption = this.isTradierOptionSymbol(symbol);
521
437
  if (isOption) {
522
438
  this.updateOptionFromTimesale(symbol, event, timestamp);
523
439
  }
@@ -529,113 +445,29 @@ class TradierClient {
529
445
  * Updates ticker data from a quote event.
530
446
  */
531
447
  updateTickerFromQuote(symbol, event, timestamp) {
532
- const existing = this.tickerCache.get(symbol);
533
- const ticker = {
534
- symbol,
535
- spot: (event.bid + event.ask) / 2,
536
- bid: event.bid,
537
- bidSize: event.bidsz,
538
- ask: event.ask,
539
- askSize: event.asksz,
540
- last: existing?.last ?? 0,
541
- volume: existing?.volume ?? 0,
542
- timestamp,
543
- };
544
- this.tickerCache.set(symbol, ticker);
545
- this.emit('tickerUpdate', ticker);
448
+ this.updateTickerFromQuoteData(symbol, event.bid, event.bidsz, event.ask, event.asksz, timestamp);
546
449
  }
547
450
  /**
548
451
  * Updates ticker data from a trade event.
549
452
  */
550
453
  updateTickerFromTrade(symbol, event, timestamp) {
551
- const existing = this.tickerCache.get(symbol);
552
454
  const last = parseFloat(event.last);
553
455
  const volume = parseInt(event.cvol, 10);
554
- const ticker = {
555
- symbol,
556
- spot: existing?.spot ?? last,
557
- bid: existing?.bid ?? 0,
558
- bidSize: existing?.bidSize ?? 0,
559
- ask: existing?.ask ?? 0,
560
- askSize: existing?.askSize ?? 0,
561
- last,
562
- volume,
563
- timestamp,
564
- };
565
- this.tickerCache.set(symbol, ticker);
566
- this.emit('tickerUpdate', ticker);
456
+ this.updateTickerFromTradeData(symbol, last, 0, volume, timestamp);
567
457
  }
568
458
  /**
569
459
  * Updates option data from a quote event.
570
460
  */
571
461
  updateOptionFromQuote(occSymbol, event, timestamp) {
572
- const existing = this.optionCache.get(occSymbol);
573
- // Parse OCC symbol to extract option details
574
- let parsed;
575
- try {
576
- parsed = (0, occ_1.parseOCCSymbol)(occSymbol);
577
- }
578
- catch {
579
- // Invalid OCC symbol, skip
580
- return;
581
- }
582
- const option = {
583
- occSymbol,
584
- underlying: parsed.symbol,
585
- strike: parsed.strike,
586
- expiration: parsed.expiration.toISOString().split('T')[0],
587
- expirationTimestamp: parsed.expiration.getTime(),
588
- optionType: parsed.optionType,
589
- bid: event.bid,
590
- bidSize: event.bidsz,
591
- ask: event.ask,
592
- askSize: event.asksz,
593
- mark: (event.bid + event.ask) / 2,
594
- last: existing?.last ?? 0,
595
- volume: existing?.volume ?? 0,
596
- openInterest: existing?.openInterest ?? 0,
597
- impliedVolatility: existing?.impliedVolatility ?? 0,
598
- timestamp,
599
- };
600
- this.optionCache.set(occSymbol, option);
601
- this.emit('optionUpdate', option);
462
+ this.updateOptionFromQuoteData(occSymbol, event.bid, event.bidsz, event.ask, event.asksz, timestamp, occ_1.parseOCCSymbol);
602
463
  }
603
464
  /**
604
465
  * Updates option data from a trade event.
605
466
  */
606
467
  updateOptionFromTrade(occSymbol, event, timestamp) {
607
- const existing = this.optionCache.get(occSymbol);
608
- // Parse OCC symbol to extract option details
609
- let parsed;
610
- try {
611
- parsed = (0, occ_1.parseOCCSymbol)(occSymbol);
612
- }
613
- catch {
614
- // Invalid OCC symbol, skip
615
- return;
616
- }
617
468
  const last = parseFloat(event.last);
618
469
  const volume = parseInt(event.cvol, 10);
619
- const option = {
620
- occSymbol,
621
- underlying: parsed.symbol,
622
- strike: parsed.strike,
623
- expiration: parsed.expiration.toISOString().split('T')[0],
624
- expirationTimestamp: parsed.expiration.getTime(),
625
- optionType: parsed.optionType,
626
- bid: existing?.bid ?? 0,
627
- bidSize: existing?.bidSize ?? 0,
628
- ask: existing?.ask ?? 0,
629
- askSize: existing?.askSize ?? 0,
630
- mark: existing?.mark ?? last,
631
- last,
632
- volume,
633
- openInterest: existing?.openInterest ?? 0,
634
- impliedVolatility: existing?.impliedVolatility ?? 0,
635
- timestamp,
636
- };
637
- this.optionCache.set(occSymbol, option);
638
- this.emit('optionUpdate', option);
470
+ this.updateOptionFromTradeData(occSymbol, last, 0, volume, timestamp, occ_1.parseOCCSymbol);
639
471
  }
640
472
  /**
641
473
  * Updates ticker data from a timesale event.
@@ -664,253 +496,19 @@ class TradierClient {
664
496
  /**
665
497
  * Updates option data from a timesale event.
666
498
  * Timesale events include bid/ask at the time of the trade, enabling aggressor side detection.
667
- *
668
- * This is the primary method for calculating live open interest:
669
- * - Aggressor side is determined by comparing trade price to NBBO
670
- * - Buy aggressor (lifting ask) typically indicates new long positions → OI increases
671
- * - Sell aggressor (hitting bid) typically indicates closing longs or new shorts → OI decreases
672
499
  */
673
500
  updateOptionFromTimesale(occSymbol, event, timestamp) {
674
- const existing = this.optionCache.get(occSymbol);
675
- // Parse OCC symbol to extract option details
676
- let parsed;
677
- try {
678
- parsed = (0, occ_1.parseOCCSymbol)(occSymbol);
679
- }
680
- catch {
681
- // Invalid OCC symbol, skip
682
- return;
683
- }
684
501
  const bid = parseFloat(event.bid);
685
502
  const ask = parseFloat(event.ask);
686
503
  const last = parseFloat(event.last);
687
504
  const size = parseInt(event.size, 10);
688
- // Determine aggressor side by comparing trade price to NBBO
689
- const aggressorSide = this.determineAggressorSide(last, bid, ask);
690
- // Calculate estimated OI change based on aggressor side
691
- // Buy aggressor (lifting the offer) → typically opening new long positions → +OI
692
- // Sell aggressor (hitting the bid) → typically closing longs or opening shorts → -OI
693
- const estimatedOIChange = this.calculateOIChangeFromTrade(aggressorSide, size, parsed.optionType);
694
- // Update cumulative OI change
695
- const currentChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
696
- this.cumulativeOIChange.set(occSymbol, currentChange + estimatedOIChange);
697
- if (this.verbose && estimatedOIChange !== 0) {
698
- const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
699
- const newLiveOI = Math.max(0, baseOI + currentChange + estimatedOIChange);
700
- console.log(`[Tradier:OI] ${occSymbol} trade: price=${last.toFixed(2)}, size=${size}, aggressor=${aggressorSide}, OI change=${estimatedOIChange > 0 ? '+' : ''}${estimatedOIChange}, liveOI=${newLiveOI} (base=${baseOI}, cumulative=${currentChange + estimatedOIChange})`);
701
- }
702
- // Record the trade for analysis
703
- const trade = {
704
- occSymbol,
705
- price: last,
706
- size,
707
- bid,
708
- ask,
709
- aggressorSide,
710
- timestamp,
711
- estimatedOIChange,
712
- };
713
- if (!this.intradayTrades.has(occSymbol)) {
714
- this.intradayTrades.set(occSymbol, []);
715
- }
716
- this.intradayTrades.get(occSymbol).push(trade);
717
- // Emit trade event with aggressor info
718
- this.emit('optionTrade', trade);
719
- const option = {
720
- occSymbol,
721
- underlying: parsed.symbol,
722
- strike: parsed.strike,
723
- expiration: parsed.expiration.toISOString().split('T')[0],
724
- expirationTimestamp: parsed.expiration.getTime(),
725
- optionType: parsed.optionType,
726
- bid,
727
- bidSize: existing?.bidSize ?? 0, // timesale doesn't include bid/ask size
728
- ask,
729
- askSize: existing?.askSize ?? 0,
730
- mark: (bid + ask) / 2,
731
- last,
732
- volume: (existing?.volume ?? 0) + size, // Accumulate volume
733
- openInterest: existing?.openInterest ?? 0,
734
- liveOpenInterest: this.calculateLiveOpenInterest(occSymbol),
735
- impliedVolatility: existing?.impliedVolatility ?? 0,
736
- timestamp,
737
- };
738
- this.optionCache.set(occSymbol, option);
739
- this.emit('optionUpdate', option);
740
- }
741
- /**
742
- * Determines the aggressor side of a trade by comparing trade price to NBBO.
743
- *
744
- * @param tradePrice - The executed trade price
745
- * @param bid - The bid price at time of trade
746
- * @param ask - The ask price at time of trade
747
- * @returns The aggressor side: 'buy' if lifting offer, 'sell' if hitting bid, 'unknown' if mid
748
- *
749
- * @remarks
750
- * The aggressor is the party that initiated the trade by crossing the spread:
751
- * - Buy aggressor: Buyer lifts the offer (trades at or above ask) → bullish intent
752
- * - Sell aggressor: Seller hits the bid (trades at or below bid) → bearish intent
753
- * - Unknown: Trade occurred mid-market (could be internalized, crossed, or negotiated)
754
- */
755
- determineAggressorSide(tradePrice, bid, ask) {
756
- // Use a small tolerance for floating point comparison (0.1% of spread)
757
- const spread = ask - bid;
758
- const tolerance = spread > 0 ? spread * 0.001 : 0.001;
759
- if (tradePrice >= ask - tolerance) {
760
- // Trade at or above ask → buyer lifted the offer
761
- return 'buy';
762
- }
763
- else if (tradePrice <= bid + tolerance) {
764
- // Trade at or below bid → seller hit the bid
765
- return 'sell';
766
- }
767
- else {
768
- // Trade mid-market - could be either side or internalized
769
- return 'unknown';
770
- }
771
- }
772
- /**
773
- * Calculates the estimated open interest change from a single trade.
774
- *
775
- * @param aggressorSide - The aggressor side of the trade
776
- * @param size - Number of contracts traded
777
- * @param optionType - Whether this is a call or put
778
- * @returns Estimated OI change (positive = OI increase, negative = OI decrease)
779
- *
780
- * @remarks
781
- * This uses a simplified heuristic based on typical market behavior:
782
- *
783
- * For CALLS:
784
- * - Buy aggressor (lifting offer) → typically bullish, opening new longs → +OI
785
- * - Sell aggressor (hitting bid) → typically closing longs or bearish new shorts → -OI
786
- *
787
- * For PUTS:
788
- * - Buy aggressor (lifting offer) → typically bearish/hedging, opening new longs → +OI
789
- * - Sell aggressor (hitting bid) → typically closing longs → -OI
790
- *
791
- * Note: This is an estimate. Without knowing if trades are opening or closing,
792
- * we use aggressor side as a proxy. SpotGamma and similar providers use
793
- * more sophisticated models that may incorporate position sizing, strike
794
- * selection patterns, and other heuristics.
795
- */
796
- calculateOIChangeFromTrade(aggressorSide, size, optionType) {
797
- if (aggressorSide === 'unknown') {
798
- // Mid-market trades are ambiguous - assume neutral impact on OI
799
- return 0;
800
- }
801
- // Simple heuristic: buy aggressor = new positions opening, sell aggressor = positions closing
802
- // This applies to both calls and puts since we're measuring contract count, not direction
803
- if (aggressorSide === 'buy') {
804
- return size; // New positions opening
805
- }
806
- else {
807
- return -size; // Positions closing
808
- }
809
- }
810
- /**
811
- * Calculates the live (intraday) open interest estimate for an option.
812
- *
813
- * @param occSymbol - OCC option symbol
814
- * @returns Live OI estimate = base OI + cumulative estimated changes
815
- *
816
- * @remarks
817
- * Live Open Interest = Base OI (from REST at t=0) + Cumulative OI Changes (from trades)
818
- *
819
- * This provides a real-time estimate of open interest that updates throughout
820
- * the trading day as trades occur. The accuracy depends on:
821
- * 1. The accuracy of aggressor side detection
822
- * 2. The assumption that aggressors are typically opening new positions
823
- *
824
- * The official OI is only updated overnight by the OCC clearing house,
825
- * so this estimate fills the gap during trading hours.
826
- */
827
- calculateLiveOpenInterest(occSymbol) {
828
- const baseOI = this.baseOpenInterest.get(occSymbol) ?? 0;
829
- const cumulativeChange = this.cumulativeOIChange.get(occSymbol) ?? 0;
830
- // Live OI cannot go negative
831
- return Math.max(0, baseOI + cumulativeChange);
832
- }
833
- /**
834
- * Returns the intraday trades for an option with aggressor classification.
835
- *
836
- * @param occSymbol - OCC option symbol
837
- * @returns Array of intraday trades, or empty array if none
838
- */
839
- getIntradayTrades(occSymbol) {
840
- return this.intradayTrades.get(occSymbol) ?? [];
841
- }
842
- /**
843
- * Returns summary statistics for intraday option flow.
844
- *
845
- * @param occSymbol - OCC option symbol
846
- * @returns Object with buy/sell volume, net OI change, and trade count
847
- */
848
- getFlowSummary(occSymbol) {
849
- const trades = this.intradayTrades.get(occSymbol) ?? [];
850
- let buyVolume = 0;
851
- let sellVolume = 0;
852
- let unknownVolume = 0;
853
- for (const trade of trades) {
854
- switch (trade.aggressorSide) {
855
- case 'buy':
856
- buyVolume += trade.size;
857
- break;
858
- case 'sell':
859
- sellVolume += trade.size;
860
- break;
861
- case 'unknown':
862
- unknownVolume += trade.size;
863
- break;
864
- }
865
- }
866
- return {
867
- buyVolume,
868
- sellVolume,
869
- unknownVolume,
870
- netOIChange: this.cumulativeOIChange.get(occSymbol) ?? 0,
871
- tradeCount: trades.length,
872
- };
873
- }
874
- /**
875
- * Resets intraday tracking data. Call this at market open or when re-fetching base OI.
876
- *
877
- * @param occSymbols - Optional specific symbols to reset. If not provided, resets all.
878
- */
879
- resetIntradayData(occSymbols) {
880
- const symbolsToReset = occSymbols ?? Array.from(this.intradayTrades.keys());
881
- for (const symbol of symbolsToReset) {
882
- this.intradayTrades.delete(symbol);
883
- this.cumulativeOIChange.set(symbol, 0);
884
- }
505
+ this.updateOptionFromTimesaleData(occSymbol, last, size, bid, ask, timestamp, occ_1.parseOCCSymbol);
885
506
  }
886
507
  /**
887
508
  * Checks if a symbol is an OCC option symbol.
888
509
  */
889
- isOptionSymbol(symbol) {
890
- return OCC_OPTION_PATTERN.test(symbol);
891
- }
892
- /**
893
- * Emits an event to all registered listeners.
894
- */
895
- emit(event, data) {
896
- const listeners = this.eventListeners.get(event);
897
- if (listeners) {
898
- listeners.forEach(listener => {
899
- try {
900
- listener(data);
901
- }
902
- catch (error) {
903
- // Don't let listener errors break the stream
904
- console.error('Event listener error:', error);
905
- }
906
- });
907
- }
908
- }
909
- /**
910
- * Simple sleep utility.
911
- */
912
- sleep(ms) {
913
- return new Promise(resolve => setTimeout(resolve, ms));
510
+ isTradierOptionSymbol(symbol) {
511
+ return BaseBrokerClient_1.OCC_OPTION_PATTERN.test(symbol);
914
512
  }
915
513
  }
916
514
  exports.TradierClient = TradierClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullstackcraftllc/floe",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Production-ready options analytics toolkit. Normalize broker data structures and calculate Black-Scholes, Greeks, and exposures with a clean, type-safe API. Built for trading platforms and fintech applications.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",