tardis-dev 14.1.5 → 14.2.0

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.
@@ -1,5 +1,5 @@
1
- import { asNumberIfValid, upperCaseSymbols } from '../handy'
2
- import { BookChange, OptionSummary, Trade } from '../types'
1
+ import { asNumberIfValid, lowerCaseSymbols, upperCaseSymbols } from '../handy'
2
+ import { BookChange, BookTicker, OptionSummary, Trade } from '../types'
3
3
  import { Mapper } from './mapper'
4
4
 
5
5
  // https://binance-docs.github.io/apidocs/voptions/en/#websocket-market-streams
@@ -41,6 +41,43 @@ export class BinanceEuropeanOptionsTradesMapper implements Mapper<'binance-europ
41
41
  }
42
42
  }
43
43
 
44
+ export class BinanceEuropeanOptionsTradesMapperV2 implements Mapper<'binance-european-options', Trade> {
45
+ canHandle(message: BinanceResponse<any>) {
46
+ if (message.stream === undefined) {
47
+ return false
48
+ }
49
+
50
+ return message.stream.endsWith('@optionTrade')
51
+ }
52
+
53
+ getFilters(symbols?: string[]) {
54
+ symbols = lowerCaseSymbols(symbols)
55
+
56
+ return [
57
+ {
58
+ channel: 'optionTrade',
59
+ symbols
60
+ } as const
61
+ ]
62
+ }
63
+
64
+ *map(binanceTradeResponse: BinanceResponse<BinanceOptionsTradeDataV2>, localTimestamp: Date) {
65
+ const trade: Trade = {
66
+ type: 'trade',
67
+ symbol: binanceTradeResponse.data.s,
68
+ exchange: 'binance-european-options',
69
+ id: String(binanceTradeResponse.data.t),
70
+ price: Number(binanceTradeResponse.data.p),
71
+ amount: Number(binanceTradeResponse.data.q),
72
+ side: binanceTradeResponse.data.m ? 'sell' : 'buy',
73
+ timestamp: new Date(binanceTradeResponse.data.T),
74
+ localTimestamp: localTimestamp
75
+ }
76
+
77
+ yield trade
78
+ }
79
+ }
80
+
44
81
  export class BinanceEuropeanOptionsBookChangeMapper implements Mapper<'binance-european-options', BookChange> {
45
82
  canHandle(message: BinanceResponse<any>) {
46
83
  if (message.stream === undefined) {
@@ -83,6 +120,90 @@ export class BinanceEuropeanOptionsBookChangeMapper implements Mapper<'binance-e
83
120
  }
84
121
  }
85
122
 
123
+ export class BinanceEuropeanOptionsBookChangeMapperV2 implements Mapper<'binance-european-options', BookChange> {
124
+ canHandle(message: BinanceResponse<any>) {
125
+ if (message.stream === undefined) {
126
+ return false
127
+ }
128
+
129
+ return message.stream.includes('@depth20')
130
+ }
131
+
132
+ getFilters(symbols?: string[]) {
133
+ symbols = lowerCaseSymbols(symbols)
134
+
135
+ return [
136
+ {
137
+ channel: 'depth20',
138
+ symbols
139
+ } as const
140
+ ]
141
+ }
142
+
143
+ *map(message: BinanceResponse<BinanceOptionsDepthDataV2>, localTimestamp: Date) {
144
+ const bookChange: BookChange = {
145
+ type: 'book_change',
146
+ symbol: message.data.s,
147
+ exchange: 'binance-european-options',
148
+ isSnapshot: true,
149
+ bids: message.data.b.map(this.mapBookLevel),
150
+ asks: message.data.a.map(this.mapBookLevel),
151
+ timestamp: new Date(message.data.T),
152
+ localTimestamp
153
+ }
154
+
155
+ yield bookChange
156
+ }
157
+
158
+ protected mapBookLevel(level: BinanceBookLevel) {
159
+ const price = Number(level[0])
160
+ const amount = Number(level[1])
161
+ return { price, amount }
162
+ }
163
+ }
164
+
165
+ export class BinanceEuropeanOptionsBookTickerMapper implements Mapper<'binance-european-options', BookTicker> {
166
+ canHandle(message: BinanceResponse<any>) {
167
+ if (message.stream === undefined) {
168
+ return false
169
+ }
170
+
171
+ return message.stream.endsWith('@bookTicker')
172
+ }
173
+
174
+ getFilters(symbols?: string[]) {
175
+ symbols = lowerCaseSymbols(symbols)
176
+
177
+ return [
178
+ {
179
+ channel: 'bookTicker',
180
+ symbols
181
+ } as const
182
+ ]
183
+ }
184
+
185
+ *map(message: BinanceResponse<BinanceOptionsBookTickerData>, localTimestamp: Date) {
186
+ const bestBidPrice = Number(message.data.b)
187
+ const bestBidAmount = Number(message.data.B)
188
+ const bestAskPrice = Number(message.data.a)
189
+ const bestAskAmount = Number(message.data.A)
190
+
191
+ const bookTicker: BookTicker = {
192
+ type: 'book_ticker',
193
+ symbol: message.data.s,
194
+ exchange: 'binance-european-options',
195
+ bidPrice: bestBidPrice > 0 ? bestBidPrice : undefined,
196
+ bidAmount: bestBidAmount > 0 ? bestBidAmount : undefined,
197
+ askPrice: bestAskPrice > 0 ? bestAskPrice : undefined,
198
+ askAmount: bestAskAmount > 0 ? bestAskAmount : undefined,
199
+ timestamp: new Date(message.data.T),
200
+ localTimestamp
201
+ }
202
+
203
+ yield bookTicker
204
+ }
205
+ }
206
+
86
207
  export class BinanceEuropeanOptionSummaryMapper implements Mapper<'binance-european-options', OptionSummary> {
87
208
  private readonly _indexPrices = new Map<string, number>()
88
209
  private readonly _openInterests = new Map<string, number>()
@@ -233,6 +354,165 @@ export class BinanceEuropeanOptionSummaryMapper implements Mapper<'binance-europ
233
354
  }
234
355
  }
235
356
 
357
+ export class BinanceEuropeanOptionSummaryMapperV2 implements Mapper<'binance-european-options', OptionSummary> {
358
+ private readonly _lastPrices = new Map<string, number>()
359
+ private readonly _openInterests = new Map<string, number>()
360
+
361
+ canHandle(message: BinanceResponse<any>) {
362
+ if (message.stream === undefined) {
363
+ return false
364
+ }
365
+
366
+ return (
367
+ message.stream.endsWith('@optionMarkPrice') ||
368
+ message.stream.endsWith('@optionTicker') ||
369
+ message.stream.includes('@optionOpenInterest')
370
+ )
371
+ }
372
+
373
+ getFilters(symbols?: string[]) {
374
+ symbols = lowerCaseSymbols(symbols)
375
+
376
+ const underlyings =
377
+ symbols !== undefined
378
+ ? symbols.map((s) => {
379
+ const symbolParts = s.split('-')
380
+ return `${symbolParts[0]}usdt`
381
+ })
382
+ : undefined
383
+
384
+ return [
385
+ {
386
+ channel: 'optionMarkPrice',
387
+ symbols: underlyings
388
+ } as const,
389
+ {
390
+ channel: 'optionTicker',
391
+ symbols
392
+ } as const,
393
+ {
394
+ channel: 'optionOpenInterest',
395
+ symbols: underlyings
396
+ } as const
397
+ ]
398
+ }
399
+
400
+ *map(
401
+ message: BinanceResponse<BinanceOptionsMarkPriceData[] | BinanceOptionsTickerData | BinanceOptionsOpenInterestDataV2[]>,
402
+ localTimestamp: Date
403
+ ) {
404
+ // Handle optionTicker messages to track last prices
405
+ if (message.stream.endsWith('@optionTicker')) {
406
+ const tickerData = message.data as BinanceOptionsTickerData
407
+ const lastPrice = Number(tickerData.c)
408
+ if (lastPrice > 0) {
409
+ this._lastPrices.set(tickerData.s, lastPrice)
410
+ }
411
+ return
412
+ }
413
+
414
+ // Handle optionOpenInterest messages to track open interest
415
+ if (message.stream.includes('@optionOpenInterest')) {
416
+ const openInterestArray = message.data as BinanceOptionsOpenInterestDataV2[]
417
+ for (let oi of openInterestArray) {
418
+ const openInterest = Number(oi.o)
419
+ if (openInterest >= 0) {
420
+ this._openInterests.set(oi.s, openInterest)
421
+ }
422
+ }
423
+ return
424
+ }
425
+
426
+ // optionMarkPrice contains all data needed: greeks, IV, best bid/ask, mark price, and index price
427
+ const markPriceArray = message.data as BinanceOptionsMarkPriceData[]
428
+ for (let markData of markPriceArray) {
429
+ const [base, expiryPart, strikePrice, optionType] = markData.s.split('-')
430
+
431
+ const expirationDate = new Date(`20${expiryPart.slice(0, 2)}-${expiryPart.slice(2, 4)}-${expiryPart.slice(4, 6)}Z`)
432
+ expirationDate.setUTCHours(8)
433
+
434
+ const isPut = optionType === 'P'
435
+ const underlyingIndex = `${base}USDT`
436
+
437
+ let bestBidPrice = asNumberIfValid(markData.bo)
438
+ if (bestBidPrice === 0) {
439
+ bestBidPrice = undefined
440
+ }
441
+
442
+ let bestBidAmount = asNumberIfValid(markData.bq)
443
+ if (bestBidAmount === 0) {
444
+ bestBidAmount = undefined
445
+ }
446
+
447
+ let bestAskPrice = asNumberIfValid(markData.ao)
448
+ if (bestAskPrice === 0) {
449
+ bestAskPrice = undefined
450
+ }
451
+
452
+ let bestAskAmount = asNumberIfValid(markData.aq)
453
+ if (bestAskAmount === 0) {
454
+ bestAskAmount = undefined
455
+ }
456
+
457
+ let bestBidIV = bestBidPrice !== undefined ? asNumberIfValid(markData.b) : undefined
458
+ if (bestBidIV === -1) {
459
+ bestBidIV = undefined
460
+ }
461
+
462
+ let bestAskIV = bestAskPrice !== undefined ? asNumberIfValid(markData.a) : undefined
463
+ if (bestAskIV === -1) {
464
+ bestAskIV = undefined
465
+ }
466
+
467
+ const markPrice = asNumberIfValid(markData.mp)
468
+ const markIV = asNumberIfValid(markData.vo)
469
+ const delta = asNumberIfValid(markData.d)
470
+ const gamma = asNumberIfValid(markData.g)
471
+ const vega = asNumberIfValid(markData.v)
472
+ const theta = asNumberIfValid(markData.t)
473
+ const underlyingPrice = asNumberIfValid(markData.i) // Index price is included in mark price data
474
+
475
+ const optionSummary: OptionSummary = {
476
+ type: 'option_summary',
477
+ symbol: markData.s,
478
+ exchange: 'binance-european-options',
479
+ optionType: isPut ? 'put' : 'call',
480
+ strikePrice: Number(strikePrice),
481
+ expirationDate,
482
+
483
+ bestBidPrice,
484
+ bestBidAmount,
485
+ bestBidIV,
486
+
487
+ bestAskPrice,
488
+ bestAskAmount,
489
+ bestAskIV,
490
+
491
+ lastPrice: this._lastPrices.get(markData.s),
492
+
493
+ openInterest: this._openInterests.get(markData.s),
494
+
495
+ markPrice,
496
+ markIV,
497
+
498
+ delta,
499
+ gamma,
500
+ vega,
501
+ theta,
502
+ rho: undefined,
503
+
504
+ underlyingPrice,
505
+ underlyingIndex,
506
+
507
+ timestamp: new Date(markData.E),
508
+ localTimestamp: localTimestamp
509
+ }
510
+
511
+ yield optionSummary
512
+ }
513
+ }
514
+ }
515
+
236
516
  type BinanceResponse<T> = {
237
517
  stream: string
238
518
  data: T
@@ -301,3 +581,72 @@ type BinanceOptionsIndexData = { e: 'index'; E: 1696118400040; s: 'BNBUSDT'; p:
301
581
  type BinanceOptionsOpenInterestData = { e: 'openInterest'; E: 1696118400042; s: 'XRP-231006-0.46-P'; o: '39480.0'; h: '20326.64319' }
302
582
 
303
583
  type BinanceBookLevel = [string, string]
584
+
585
+ // V2 Types for new format (Dec 17, 2025+)
586
+ type BinanceOptionsTradeDataV2 = {
587
+ e: 'trade'
588
+ E: number // event time
589
+ T: number // trade completed time
590
+ s: string // option symbol
591
+ t: number // trade ID
592
+ p: string // price
593
+ q: string // quantity
594
+ X: 'MARKET' | 'BLOCK' // trade type
595
+ S: 'BUY' | 'SELL' // direction
596
+ m: boolean // is buyer market maker
597
+ }
598
+
599
+ type BinanceOptionsDepthDataV2 = {
600
+ e: 'depthUpdate'
601
+ E: number // event time
602
+ T: number // transaction time
603
+ s: string // symbol
604
+ U: number // first update ID
605
+ u: number // final update ID
606
+ pu: number // previous final update ID
607
+ b: [string, string][] // bids
608
+ a: [string, string][] // asks
609
+ }
610
+
611
+ type BinanceOptionsBookTickerData = {
612
+ e: 'bookTicker'
613
+ u: number // order book update ID
614
+ s: string // symbol
615
+ b: string // best bid price
616
+ B: string // best bid qty
617
+ a: string // best ask price
618
+ A: string // best ask qty
619
+ T: number // transaction time
620
+ E: number // event time
621
+ }
622
+
623
+ type BinanceOptionsOpenInterestDataV2 = {
624
+ e: 'openInterest'
625
+ E: number // event time
626
+ s: string // symbol
627
+ o: string // open interest (quantity)
628
+ h: string // open interest in notional value (USD)
629
+ }
630
+
631
+ type BinanceOptionsMarkPriceData = {
632
+ s: string // option symbol
633
+ mp: string // mark price
634
+ E: number // event time
635
+ e: 'markPrice'
636
+ i: string // index price
637
+ P: string // premium
638
+ bo: string // best bid price
639
+ ao: string // best ask price
640
+ bq: string // best bid quantity
641
+ aq: string // best ask quantity
642
+ b: string // bid IV
643
+ a: string // ask IV
644
+ hl: string // high limit price
645
+ ll: string // low limit price
646
+ vo: string // mark IV
647
+ rf: string // risk free rate
648
+ d: string // delta
649
+ t: string // theta
650
+ g: string // gamma
651
+ v: string // vega
652
+ }
@@ -12,8 +12,12 @@ import {
12
12
  import { binanceDexBookChangeMapper, binanceDexBookTickerMapper, binanceDexTradesMapper } from './binancedex'
13
13
  import {
14
14
  BinanceEuropeanOptionsBookChangeMapper,
15
+ BinanceEuropeanOptionsBookChangeMapperV2,
16
+ BinanceEuropeanOptionsBookTickerMapper,
15
17
  BinanceEuropeanOptionsTradesMapper,
16
- BinanceEuropeanOptionSummaryMapper
18
+ BinanceEuropeanOptionsTradesMapperV2,
19
+ BinanceEuropeanOptionSummaryMapper,
20
+ BinanceEuropeanOptionSummaryMapperV2
17
21
  } from './binanceeuropeanoptions'
18
22
  import { BinanceOptionsBookChangeMapper, BinanceOptionsTradesMapper, BinanceOptionSummaryMapper } from './binanceoptions'
19
23
  import {
@@ -221,6 +225,12 @@ const shouldUseOKXTradesAllChannel = (localTimestamp: Date) => {
221
225
  return isRealTime(localTimestamp) || localTimestamp.valueOf() >= new Date('2023-10-19T00:00:00.000Z').valueOf()
222
226
  }
223
227
 
228
+ const BINANCE_EUROPEAN_OPTIONS_V2_API_SWITCH_DATE = new Date('2025-12-17T00:00:00.000Z')
229
+
230
+ const shouldUseBinanceEuropeanOptionsV2Mappers = (localTimestamp: Date) => {
231
+ return isRealTime(localTimestamp) || localTimestamp.valueOf() >= BINANCE_EUROPEAN_OPTIONS_V2_API_SWITCH_DATE.valueOf()
232
+ }
233
+
224
234
  const tradesMappers = {
225
235
  bitmex: () => bitmexTradesMapper,
226
236
  binance: () => new BinanceTradesMapper('binance'),
@@ -296,7 +306,10 @@ const tradesMappers = {
296
306
  'woo-x': () => wooxTradesMapper,
297
307
  'blockchain-com': () => new BlockchainComTradesMapper(),
298
308
  'bybit-options': () => new BybitV5TradesMapper('bybit-options'),
299
- 'binance-european-options': () => new BinanceEuropeanOptionsTradesMapper(),
309
+ 'binance-european-options': (localTimestamp: Date) =>
310
+ shouldUseBinanceEuropeanOptionsV2Mappers(localTimestamp)
311
+ ? new BinanceEuropeanOptionsTradesMapperV2()
312
+ : new BinanceEuropeanOptionsTradesMapper(),
300
313
  'okex-spreads': () => new OkexSpreadsTradesMapper(),
301
314
  bitget: () => new BitgetTradesMapper('bitget'),
302
315
  'bitget-futures': () => new BitgetTradesMapper('bitget-futures'),
@@ -390,7 +403,10 @@ const bookChangeMappers = {
390
403
  'woo-x': () => new WooxBookChangeMapper(),
391
404
  'blockchain-com': () => new BlockchainComBookChangeMapper(),
392
405
  'bybit-options': () => new BybitV5BookChangeMapper('bybit-options', 25),
393
- 'binance-european-options': () => new BinanceEuropeanOptionsBookChangeMapper(),
406
+ 'binance-european-options': (localTimestamp: Date) =>
407
+ shouldUseBinanceEuropeanOptionsV2Mappers(localTimestamp)
408
+ ? new BinanceEuropeanOptionsBookChangeMapperV2()
409
+ : new BinanceEuropeanOptionsBookChangeMapper(),
394
410
  'okex-spreads': () => new OkexSpreadsBookChangeMapper(),
395
411
  bitget: () => new BitgetBookChangeMapper('bitget'),
396
412
  'bitget-futures': () => new BitgetBookChangeMapper('bitget-futures'),
@@ -442,7 +458,10 @@ const optionsSummaryMappers = {
442
458
  'binance-options': () => new BinanceOptionSummaryMapper(),
443
459
  'huobi-dm-options': () => new HuobiOptionsSummaryMapper(),
444
460
  'bybit-options': () => new BybitV5OptionSummaryMapper(),
445
- 'binance-european-options': () => new BinanceEuropeanOptionSummaryMapper()
461
+ 'binance-european-options': (localTimestamp: Date) =>
462
+ shouldUseBinanceEuropeanOptionsV2Mappers(localTimestamp)
463
+ ? new BinanceEuropeanOptionSummaryMapperV2()
464
+ : new BinanceEuropeanOptionSummaryMapper()
446
465
  }
447
466
 
448
467
  const liquidationsMappers = {
@@ -532,7 +551,8 @@ const bookTickersMappers = {
532
551
  bitget: () => new BitgetBookTickerMapper('bitget'),
533
552
  'bitget-futures': () => new BitgetBookTickerMapper('bitget-futures'),
534
553
  'coinbase-international': () => coinbaseInternationalBookTickerMapper,
535
- hyperliquid: () => new HyperliquidBookTickerMapper()
554
+ hyperliquid: () => new HyperliquidBookTickerMapper(),
555
+ 'binance-european-options': () => new BinanceEuropeanOptionsBookTickerMapper()
536
556
  }
537
557
 
538
558
  export const normalizeTrades = <T extends keyof typeof tradesMappers>(exchange: T, localTimestamp: Date): Mapper<T, Trade> => {
@@ -1,9 +1,54 @@
1
1
  import { onlyUnique } from '../handy'
2
2
  import { Filter } from '../types'
3
- import { RealTimeFeedBase } from './realtimefeed'
3
+ import { MultiConnectionRealTimeFeedBase, RealTimeFeedBase } from './realtimefeed'
4
4
 
5
- export class BinanceEuropeanOptionsRealTimeFeed extends RealTimeFeedBase {
6
- protected wssURL = 'wss://nbstream.binance.com/eoptions/stream'
5
+ export class BinanceEuropeanOptionsRealTimeFeed extends MultiConnectionRealTimeFeedBase {
6
+ protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
7
+ // V2 API uses two separate WebSocket endpoints:
8
+ // 1. Public path: for optionTrade, depth20, bookTicker, optionTicker
9
+ // 2. Market path: for optionIndexPrice, optionMarkPrice, optionOpenInterest
10
+
11
+ const publicChannels = ['optionTrade', 'depth20', 'bookTicker', 'optionTicker']
12
+ const marketChannels = ['optionIndexPrice', 'optionMarkPrice', 'optionOpenInterest']
13
+
14
+ const publicFilters = filters.filter((f) => publicChannels.includes(f.channel))
15
+ const marketFilters = filters.filter((f) => marketChannels.includes(f.channel))
16
+
17
+ if (publicFilters.length > 0) {
18
+ yield new BinanceEuropeanOptionsSingleFeed(
19
+ 'wss://fstream.binance.com/public/stream',
20
+ exchange,
21
+ publicFilters,
22
+ filters, // Pass all filters so we can look up optionTicker when processing optionOpenInterest
23
+ timeoutIntervalMS,
24
+ onError
25
+ )
26
+ }
27
+
28
+ if (marketFilters.length > 0) {
29
+ yield new BinanceEuropeanOptionsSingleFeed(
30
+ 'wss://fstream.binance.com/market/stream',
31
+ exchange,
32
+ marketFilters,
33
+ filters, // Pass all filters so we can look up optionTicker when processing optionOpenInterest
34
+ timeoutIntervalMS,
35
+ onError
36
+ )
37
+ }
38
+ }
39
+ }
40
+
41
+ class BinanceEuropeanOptionsSingleFeed extends RealTimeFeedBase {
42
+ constructor(
43
+ protected wssURL: string,
44
+ exchange: string,
45
+ filters: Filter<string>[],
46
+ private readonly _allFilters: Filter<string>[],
47
+ timeoutIntervalMS: number | undefined,
48
+ onError?: (error: Error) => void
49
+ ) {
50
+ super(exchange, filters, timeoutIntervalMS, onError)
51
+ }
7
52
 
8
53
  protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
9
54
  const payload = filters.map((filter, index) => {
@@ -15,29 +60,59 @@ export class BinanceEuropeanOptionsRealTimeFeed extends RealTimeFeedBase {
15
60
  method: 'SUBSCRIBE',
16
61
  params: filter.symbols
17
62
  .map((symbol) => {
18
- if (filter.channel === 'depth100') {
19
- return [`${symbol}@${filter.channel}@100ms`]
63
+ const lowerSymbol = symbol.toLowerCase()
64
+
65
+ // Public path channels - use lowercase symbol directly
66
+ if (filter.channel === 'optionTrade') {
67
+ return [`${lowerSymbol}@${filter.channel}`]
20
68
  }
21
69
 
22
- if (filter.channel === 'openInterest') {
23
- const matchingTickerChannel = filters.find((f) => f.channel === 'ticker')
70
+ if (filter.channel === 'depth20') {
71
+ return [`${lowerSymbol}@${filter.channel}@100ms`]
72
+ }
73
+
74
+ if (filter.channel === 'bookTicker') {
75
+ return [`${lowerSymbol}@${filter.channel}`]
76
+ }
77
+
78
+ if (filter.channel === 'optionTicker') {
79
+ return [`${lowerSymbol}@${filter.channel}`]
80
+ }
81
+
82
+ // Market path channels - use lowercase underlying
83
+ if (filter.channel === 'optionIndexPrice' || filter.channel === 'optionMarkPrice') {
84
+ // Symbol is the underlying (e.g., 'btcusdt')
85
+ return [`${lowerSymbol}@${filter.channel}`]
86
+ }
87
+
88
+ if (filter.channel === 'optionOpenInterest') {
89
+ // Need to extract expirations from option symbols
90
+ // The symbol here is the underlying (e.g., 'btcusdt')
91
+ // We need to find all option symbols that match this underlying to extract expirations
92
+
93
+ // Look for optionTicker filter in all filters to get actual option symbols
94
+ const optionTickerFilter = this._allFilters.find((f) => f.channel === 'optionTicker')
95
+
96
+ if (optionTickerFilter !== undefined) {
97
+ // Extract expirations from option symbols that match this underlying
98
+ const underlyingBase = lowerSymbol.replace('usdt', '').toUpperCase()
24
99
 
25
- if (matchingTickerChannel !== undefined) {
26
- const expirations = matchingTickerChannel
27
- .symbols!.filter((s) => s.startsWith(symbol))
100
+ const expirations = optionTickerFilter
101
+ .symbols!.filter((s) => s.toUpperCase().startsWith(underlyingBase + '-'))
28
102
  .map((s) => {
29
103
  const symbolParts = s.split('-')
30
- return `${symbolParts[1]}`
104
+ return symbolParts[1] // Extract expiration (e.g., '251219')
31
105
  })
32
106
  .filter(onlyUnique)
107
+ .map((exp) => exp.toLowerCase())
33
108
 
34
109
  return expirations.map((expiration) => {
35
- return `${symbol}@${filter.channel}@${expiration}`
110
+ return `${lowerSymbol}@${filter.channel}@${expiration}`
36
111
  })
37
112
  }
38
113
  }
39
114
 
40
- return [`${symbol}@${filter.channel}`]
115
+ return [`${lowerSymbol}@${filter.channel}`]
41
116
  })
42
117
  .flatMap((s) => s),
43
118
  id: index + 1