tardis-dev 14.1.4 → 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
+ }
@@ -106,10 +106,10 @@ export class CryptoComBookTickerMapper implements Mapper<'crypto-com' | 'crypto-
106
106
  symbol: message.result.instrument_name,
107
107
  exchange: this._exchange,
108
108
 
109
- askAmount: undefined,
109
+ askAmount: item.ks !== undefined && item.ks !== null ? Number(item.ks) : undefined,
110
110
  askPrice: item.k !== undefined && item.k !== null ? Number(item.k) : undefined,
111
111
  bidPrice: item.b !== undefined && item.b !== null ? Number(item.b) : undefined,
112
- bidAmount: undefined,
112
+ bidAmount: item.bs !== undefined && item.bs !== null ? Number(item.bs) : undefined,
113
113
  timestamp: new Date(item.t),
114
114
  localTimestamp: localTimestamp
115
115
  }
@@ -344,7 +344,9 @@ type CryptoComTickerMessage =
344
344
  {
345
345
  i: 'GODS_USDT'
346
346
  b: 0.4262
347
+ bs?: 0.1
347
348
  k: 0.4272
349
+ ks?: 0.2
348
350
  a: 0.4272
349
351
  t: 1659311999946
350
352
  v: 100623.01
@@ -359,31 +361,89 @@ type CryptoComTickerMessage =
359
361
  }
360
362
  | CryptoComDerivativesTickerMessage
361
363
 
362
- type CryptoComDerivativesTickerMessage = {
363
- id: -1
364
- code: 0
365
- method: 'subscribe'
366
- result: {
367
- channel: 'ticker'
368
- instrument_name: 'BTCUSD-PERP'
369
- subscription: 'ticker.BTCUSD-PERP'
370
- data: [
371
- {
372
- h: '32222.5'
373
- l: '30240.0'
374
- a: '31611.0'
375
- c: '0.0320'
376
- b: '31613.0'
377
- k: '31613.5'
378
- i: 'BTCUSD-PERP'
379
- v: '13206.4884'
380
- vv: '433945264.39'
381
- oi: '318.5162'
382
- t: 1653992543383
364
+ type CryptoComDerivativesTickerMessage =
365
+ | {
366
+ id: -1
367
+ code: 0
368
+ method: 'subscribe'
369
+ result: {
370
+ channel: 'ticker'
371
+ instrument_name: 'BTCUSD-PERP'
372
+ subscription: 'ticker.BTCUSD-PERP'
373
+ data: [
374
+ {
375
+ h: '32222.5'
376
+ l: '30240.0'
377
+ a: '31611.0'
378
+ c: '0.0320'
379
+ b: '31613.0'
380
+ bs?: '0.1000'
381
+ k: '31613.5'
382
+ ks?: '0.2000'
383
+ i: 'BTCUSD-PERP'
384
+ v: '13206.4884'
385
+ vv: '433945264.39'
386
+ oi: '318.5162'
387
+ t: 1653992543383
388
+ }
389
+ ]
383
390
  }
384
- ]
385
- }
386
- }
391
+ }
392
+ | {
393
+ id: 2
394
+ method: 'subscribe'
395
+ code: 0
396
+ result: {
397
+ instrument_name: 'ESUSD-PERP'
398
+ subscription: 'ticker.ESUSD-PERP'
399
+ channel: 'ticker'
400
+ data: [
401
+ {
402
+ h: '0.09625'
403
+ l: '0.09230'
404
+ a: '0.09481'
405
+ c: '-0.0038'
406
+ b: '0.09451'
407
+ bs: '1'
408
+ k: '0.09452'
409
+ ks: '8461'
410
+ i: 'ESUSD-PERP'
411
+ v: '115'
412
+ vv: '10.84'
413
+ oi: '78522'
414
+ t: 1765238404604
415
+ }
416
+ ]
417
+ }
418
+ }
419
+ | {
420
+ id: -1
421
+ code: 0
422
+ method: 'subscribe'
423
+ result: {
424
+ channel: 'ticker'
425
+ instrument_name: 'MATIC_USD'
426
+ subscription: 'ticker.MATIC_USD'
427
+ id: 1
428
+ data: [
429
+ {
430
+ h: '1.24383'
431
+ l: '1.18086'
432
+ a: '1.19604'
433
+ c: '-0.0315'
434
+ b: '1.19591'
435
+ bs: '0.1'
436
+ k: '1.19643'
437
+ ks: '0.7'
438
+ i: 'MATIC_USD'
439
+ v: '854908.9'
440
+ vv: '1043976.96'
441
+ oi: '0'
442
+ t: 1677628802241
443
+ }
444
+ ]
445
+ }
446
+ }
387
447
 
388
448
  type CryptoComIndexMessage = {
389
449
  id: -1
@@ -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> => {