tardis-dev 16.3.1 → 16.4.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.
Files changed (41) hide show
  1. package/dist/consts.d.ts +2 -1
  2. package/dist/consts.d.ts.map +1 -1
  3. package/dist/consts.js +5 -2
  4. package/dist/consts.js.map +1 -1
  5. package/dist/handy.d.ts +9 -0
  6. package/dist/handy.d.ts.map +1 -1
  7. package/dist/handy.js +15 -0
  8. package/dist/handy.js.map +1 -1
  9. package/dist/mappers/bullish.d.ts +161 -0
  10. package/dist/mappers/bullish.d.ts.map +1 -0
  11. package/dist/mappers/bullish.js +218 -0
  12. package/dist/mappers/bullish.js.map +1 -0
  13. package/dist/mappers/bybit.d.ts +2 -2
  14. package/dist/mappers/bybitspot.d.ts +1 -1
  15. package/dist/mappers/cryptocom.d.ts +1 -1
  16. package/dist/mappers/huobi.d.ts +3 -3
  17. package/dist/mappers/index.d.ts +6 -0
  18. package/dist/mappers/index.d.ts.map +1 -1
  19. package/dist/mappers/index.js +10 -4
  20. package/dist/mappers/index.js.map +1 -1
  21. package/dist/realtimefeeds/bullish.d.ts +18 -0
  22. package/dist/realtimefeeds/bullish.d.ts.map +1 -0
  23. package/dist/realtimefeeds/bullish.js +113 -0
  24. package/dist/realtimefeeds/bullish.js.map +1 -0
  25. package/dist/realtimefeeds/index.d.ts.map +1 -1
  26. package/dist/realtimefeeds/index.js +3 -1
  27. package/dist/realtimefeeds/index.js.map +1 -1
  28. package/dist/types.d.ts +1 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/worker.d.ts.map +1 -1
  31. package/dist/worker.js +52 -43
  32. package/dist/worker.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/consts.ts +6 -2
  35. package/src/handy.ts +17 -0
  36. package/src/mappers/bullish.ts +374 -0
  37. package/src/mappers/index.ts +16 -4
  38. package/src/realtimefeeds/bullish.ts +140 -0
  39. package/src/realtimefeeds/index.ts +3 -1
  40. package/src/types.ts +2 -1
  41. package/src/worker.ts +82 -54
@@ -0,0 +1,374 @@
1
+ import { asNumberIfValid, asNumberOrUndefined } from '../handy.ts'
2
+ import { BookChange, BookPriceLevel, BookTicker, DerivativeTicker, OptionSummary, Trade } from '../types.ts'
3
+ import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
4
+
5
+ export class BullishTradesMapper implements Mapper<'bullish', Trade> {
6
+ canHandle(message: BullishMessage): message is BullishAnonymousTradeUpdateMessage {
7
+ return message.dataType === 'V1TAAnonymousTradeUpdate' && message.type === 'update'
8
+ }
9
+
10
+ getFilters(symbols?: string[]) {
11
+ return [
12
+ {
13
+ channel: 'V1TAAnonymousTradeUpdate' as const,
14
+ symbols
15
+ }
16
+ ]
17
+ }
18
+
19
+ *map(message: BullishAnonymousTradeUpdateMessage, localTimestamp: Date): IterableIterator<Trade> {
20
+ for (const trade of message.data.trades) {
21
+ yield {
22
+ type: 'trade',
23
+ symbol: trade.symbol,
24
+ exchange: 'bullish',
25
+ id: trade.tradeId,
26
+ price: Number(trade.price),
27
+ amount: Number(trade.quantity),
28
+ side: this.mapBullishTradeSide(trade.side, trade.isTaker),
29
+ timestamp: new Date(trade.createdAtDatetime),
30
+ localTimestamp
31
+ }
32
+ }
33
+ }
34
+
35
+ private mapBullishTradeSide(side: BullishTradeSide, isTaker: boolean): 'buy' | 'sell' {
36
+ if (isTaker) {
37
+ // Bullish side already represents the taker/aggressor side.
38
+ return side === 'BUY' ? 'buy' : 'sell'
39
+ }
40
+
41
+ // Bullish side represents the maker/resting order, so the taker was on the opposite side.
42
+ return side === 'BUY' ? 'sell' : 'buy'
43
+ }
44
+ }
45
+
46
+ export class BullishBookChangeMapper implements Mapper<'bullish', BookChange> {
47
+ canHandle(message: BullishMessage): message is BullishLevel2Message {
48
+ return message.dataType === 'V1TALevel2'
49
+ }
50
+
51
+ getFilters(symbols?: string[]) {
52
+ return [
53
+ {
54
+ channel: 'V1TALevel2' as const,
55
+ symbols
56
+ }
57
+ ]
58
+ }
59
+
60
+ *map(message: BullishLevel2Message, localTimestamp: Date): IterableIterator<BookChange> {
61
+ yield {
62
+ type: 'book_change',
63
+ symbol: message.data.symbol,
64
+ exchange: 'bullish',
65
+ isSnapshot: message.type === 'snapshot',
66
+ bids: this.mapLevels(message.data.bids),
67
+ asks: this.mapLevels(message.data.asks),
68
+ timestamp: new Date(message.data.datetime),
69
+ localTimestamp
70
+ }
71
+ }
72
+
73
+ private mapLevels(levels: string[]): BookPriceLevel[] {
74
+ const result = new Array<BookPriceLevel>(levels.length / 2)
75
+ for (let index = 0, resultIndex = 0; index < levels.length; index += 2, resultIndex++) {
76
+ result[resultIndex] = {
77
+ price: Number(levels[index]),
78
+ amount: Number(levels[index + 1])
79
+ }
80
+ }
81
+
82
+ return result
83
+ }
84
+ }
85
+
86
+ export class BullishBookTickerMapper implements Mapper<'bullish', BookTicker> {
87
+ canHandle(message: BullishMessage): message is BullishLevel1Message {
88
+ return message.dataType === 'V1TALevel1' && (message.type === 'snapshot' || message.type === 'update')
89
+ }
90
+
91
+ getFilters(symbols?: string[]) {
92
+ return [
93
+ {
94
+ channel: 'V1TALevel1' as const,
95
+ symbols
96
+ }
97
+ ]
98
+ }
99
+
100
+ *map(message: BullishLevel1Message, localTimestamp: Date): IterableIterator<BookTicker> {
101
+ yield {
102
+ type: 'book_ticker',
103
+ symbol: message.data.symbol,
104
+ exchange: 'bullish',
105
+ bidPrice: asNumberIfValid(message.data.bid[0]),
106
+ bidAmount: asNumberIfValid(message.data.bid[1]),
107
+ askPrice: asNumberIfValid(message.data.ask[0]),
108
+ askAmount: asNumberIfValid(message.data.ask[1]),
109
+ timestamp: new Date(message.data.datetime),
110
+ localTimestamp
111
+ }
112
+ }
113
+ }
114
+
115
+ export class BullishDerivativeTickerMapper implements Mapper<'bullish', DerivativeTicker> {
116
+ private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
117
+ private readonly indexPrices = new Map<string, { price: number; timestamp: Date }>()
118
+
119
+ canHandle(message: BullishMessage): message is BullishDerivativeTickerMessage | BullishIndexPriceMessage {
120
+ if (message.dataType === 'V1TAIndexPrice' && (message.type === 'snapshot' || message.type === 'update')) {
121
+ return true
122
+ }
123
+
124
+ if (message.dataType === 'V1TATickerResponse' && (message.type === 'snapshot' || message.type === 'update')) {
125
+ const tickerMessage = message as BullishTickerMessage
126
+
127
+ return tickerMessage.data.symbol.endsWith('-PERP') || /-\d{8}$/.test(tickerMessage.data.symbol)
128
+ }
129
+
130
+ return false
131
+ }
132
+
133
+ getFilters(symbols?: string[]) {
134
+ return [
135
+ {
136
+ channel: 'V1TATickerResponse' as const,
137
+ symbols
138
+ },
139
+ {
140
+ channel: 'V1TAIndexPrice' as const,
141
+ symbols: symbols === undefined ? undefined : [...new Set(symbols.map((symbol) => symbol.split('-')[0]))]
142
+ }
143
+ ]
144
+ }
145
+
146
+ *map(message: BullishDerivativeTickerMessage | BullishIndexPriceMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
147
+ if (message.dataType === 'V1TAIndexPrice') {
148
+ const price = asNumberIfValid(message.data.price)
149
+ if (price !== undefined) {
150
+ this.indexPrices.set(message.data.assetSymbol, {
151
+ price,
152
+ timestamp: new Date(message.data.updatedAtDatetime)
153
+ })
154
+ }
155
+
156
+ return
157
+ }
158
+
159
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.data.symbol, 'bullish')
160
+ const indexAsset = message.data.symbol.split('-')[0]
161
+ const indexPrice = this.indexPrices.get(indexAsset)
162
+
163
+ pendingTickerInfo.updateLastPrice(asNumberIfValid(message.data.last))
164
+ pendingTickerInfo.updateMarkPrice(asNumberIfValid(message.data.markPrice))
165
+ pendingTickerInfo.updateFundingRate(asNumberOrUndefined(message.data.fundingRate))
166
+ pendingTickerInfo.updateOpenInterest(asNumberOrUndefined(message.data.openInterest))
167
+ pendingTickerInfo.updateIndexPrice(indexPrice?.price)
168
+
169
+ if (pendingTickerInfo.hasChanged()) {
170
+ pendingTickerInfo.updateTimestamp(new Date(message.data.createdAtDatetime))
171
+ yield pendingTickerInfo.getSnapshot(localTimestamp)
172
+ }
173
+ }
174
+ }
175
+
176
+ export class BullishOptionSummaryMapper implements Mapper<'bullish', OptionSummary> {
177
+ private readonly indexPrices = new Map<string, { price: number; timestamp: Date }>()
178
+
179
+ canHandle(message: BullishMessage): message is BullishOptionTickerMessage | BullishIndexPriceMessage {
180
+ if (message.dataType === 'V1TAIndexPrice' && (message.type === 'snapshot' || message.type === 'update')) {
181
+ return true
182
+ }
183
+
184
+ if (message.dataType === 'V1TATickerResponse' && (message.type === 'snapshot' || message.type === 'update')) {
185
+ const tickerMessage = message as BullishTickerMessage
186
+
187
+ return tickerMessage.data.symbol.endsWith('-C') || tickerMessage.data.symbol.endsWith('-P')
188
+ }
189
+
190
+ return false
191
+ }
192
+
193
+ getFilters(symbols?: string[]) {
194
+ return [
195
+ {
196
+ channel: 'V1TATickerResponse' as const,
197
+ symbols
198
+ },
199
+ {
200
+ channel: 'V1TAIndexPrice' as const,
201
+ symbols: symbols === undefined ? undefined : [...new Set(symbols.map((symbol) => symbol.split('-')[0]))]
202
+ }
203
+ ]
204
+ }
205
+
206
+ *map(message: BullishOptionTickerMessage | BullishIndexPriceMessage, localTimestamp: Date): IterableIterator<OptionSummary> {
207
+ if (message.dataType === 'V1TAIndexPrice') {
208
+ const price = asNumberIfValid(message.data.price)
209
+ if (price !== undefined) {
210
+ this.indexPrices.set(message.data.assetSymbol, {
211
+ price,
212
+ timestamp: new Date(message.data.updatedAtDatetime)
213
+ })
214
+ }
215
+
216
+ return
217
+ }
218
+
219
+ const [indexAsset, , dateText, strikePriceText, optionType] = message.data.symbol.split('-')
220
+ const indexPrice = this.indexPrices.get(indexAsset)
221
+
222
+ const expirationDate = new Date(`${dateText.slice(0, 4)}-${dateText.slice(4, 6)}-${dateText.slice(6, 8)}Z`)
223
+ expirationDate.setUTCHours(8)
224
+
225
+ yield {
226
+ type: 'option_summary',
227
+ symbol: message.data.symbol,
228
+ exchange: 'bullish',
229
+ optionType: optionType === 'P' ? 'put' : 'call',
230
+ strikePrice: Number(strikePriceText),
231
+ expirationDate,
232
+ bestBidPrice: asNumberIfValid(message.data.bestBid),
233
+ bestBidAmount: asNumberIfValid(message.data.bidVolume),
234
+ bestBidIV: undefined,
235
+ bestAskPrice: asNumberIfValid(message.data.bestAsk),
236
+ bestAskAmount: asNumberIfValid(message.data.askVolume),
237
+ bestAskIV: undefined,
238
+ lastPrice: asNumberIfValid(message.data.last),
239
+ openInterest: asNumberOrUndefined(message.data.openInterest),
240
+ markPrice: asNumberOrUndefined(message.data.markPrice),
241
+ markIV: asNumberIfValid(message.data.impliedVolatility),
242
+ delta: asNumberOrUndefined(message.data.delta),
243
+ gamma: asNumberOrUndefined(message.data.gamma),
244
+ vega: asNumberOrUndefined(message.data.vega),
245
+ theta: asNumberOrUndefined(message.data.theta),
246
+ rho: undefined,
247
+ underlyingPrice: indexPrice?.price,
248
+ underlyingIndex: indexAsset,
249
+ timestamp: new Date(message.data.createdAtDatetime),
250
+ localTimestamp
251
+ }
252
+ }
253
+ }
254
+
255
+ type BullishMessage = BullishDataMessage<string, unknown>
256
+ type BullishDataMessage<TDataType extends string, TData> = {
257
+ type: BullishMessageRole
258
+ dataType: TDataType
259
+ data: TData
260
+ }
261
+ type BullishMessageRole = 'snapshot' | 'update'
262
+
263
+ type BullishAnonymousTradeUpdateMessage = BullishDataMessage<'V1TAAnonymousTradeUpdate', BullishAnonymousTradeUpdateData>
264
+ type BullishLevel2Message = BullishDataMessage<'V1TALevel2', BullishLevel2Data>
265
+ type BullishLevel1Message = BullishDataMessage<'V1TALevel1', BullishLevel1Data>
266
+ type BullishTickerMessage = BullishDataMessage<'V1TATickerResponse', BullishTickerData>
267
+ type BullishDerivativeTickerMessage = BullishDataMessage<'V1TATickerResponse', BullishDerivativeTickerData>
268
+ type BullishOptionTickerMessage = BullishDataMessage<'V1TATickerResponse', BullishOptionTickerData>
269
+ type BullishIndexPriceMessage = BullishDataMessage<'V1TAIndexPrice', BullishIndexPriceData>
270
+
271
+ type BullishAnonymousTradeUpdateData = {
272
+ symbol: string
273
+ createdAtTimestamp: string
274
+ publishedAtTimestamp: string
275
+ trades: BullishAnonymousTrade[]
276
+ }
277
+ type BullishAnonymousTrade = {
278
+ symbol: string
279
+ tradeId: string
280
+ price: string
281
+ quantity: string
282
+ side: BullishTradeSide
283
+ isTaker: boolean
284
+ createdAtTimestamp: string
285
+ publishedAtTimestamp: string
286
+ lastUpdatedTimestamp: string
287
+ createdAtDatetime: string
288
+ }
289
+ type BullishTradeSide = 'BUY' | 'SELL'
290
+
291
+ type BullishLevel2Data = {
292
+ timestamp: string
293
+ bids: string[]
294
+ asks: string[]
295
+ publishedAtTimestamp: string
296
+ datetime: string
297
+ sequenceNumberRange: [number, number]
298
+ symbol: string
299
+ }
300
+
301
+ type BullishLevel1Data = {
302
+ timestamp: string
303
+ bid: [string, string]
304
+ ask: [string, string]
305
+ publishedAtTimestamp: string
306
+ datetime: string
307
+ sequenceNumber: string
308
+ symbol: string
309
+ }
310
+
311
+ type BullishIndexPriceData = {
312
+ price: string
313
+ assetSymbol: string
314
+ updatedAtDatetime: string
315
+ updatedAtTimestamp: string
316
+ }
317
+
318
+ type BullishTickerData = BullishSpotTickerData | BullishDerivativeTickerData | BullishOptionTickerData
319
+
320
+ type BullishSpotTickerData = BullishTickerDataBase
321
+
322
+ type BullishDerivativeTickerData = BullishTickerDataBase & {
323
+ markPrice: string | null
324
+ fundingRate?: string | null
325
+ openInterest: string | null
326
+ openInterestUSD: string | null
327
+ }
328
+
329
+ type BullishOptionTickerData = BullishTickerDataBase & {
330
+ markPrice: string | null
331
+ openInterest: string | null
332
+ openInterestUSD: string | null
333
+ delta: string | null
334
+ gamma: string | null
335
+ theta: string | null
336
+ vega: string | null
337
+ impliedVolatility: string | null
338
+ }
339
+
340
+ type BullishTickerDataBase = {
341
+ askVolume: string | null
342
+ average: string | null
343
+ baseVolume: string
344
+ bestAsk?: string | null
345
+ bestBid?: string | null
346
+ bidVolume?: string | null
347
+ change: string
348
+ close: string | null
349
+ createdAtTimestamp: string
350
+ publishedAtTimestamp: string
351
+ high: string | null
352
+ last: string | null
353
+ lastTradeDatetime: string | null
354
+ lastTradeSize: string
355
+ low: string | null
356
+ open: string | null
357
+ percentage: string
358
+ quoteVolume: string
359
+ symbol: string
360
+ type: 'ticker'
361
+ vwap: string | null
362
+ currentPrice: string | null
363
+ ammData: BullishTickerAmmData[] | null
364
+ createdAtDatetime: string
365
+ otcBaseVolume: string
366
+ }
367
+
368
+ type BullishTickerAmmData = {
369
+ feeTierId: string
370
+ tierPrice: string
371
+ currentPrice: string
372
+ bidSpreadFee: string
373
+ askSpreadFee: string
374
+ }
@@ -48,6 +48,13 @@ import {
48
48
  import { BitnomialBookChangMapper, bitnomialTradesMapper } from './bitnomial.ts'
49
49
  import { BitstampBookChangeMapper, bitstampTradesMapper } from './bitstamp.ts'
50
50
  import { BlockchainComBookChangeMapper, BlockchainComTradesMapper } from './blockchaincom.ts'
51
+ import {
52
+ BullishBookChangeMapper,
53
+ BullishBookTickerMapper,
54
+ BullishDerivativeTickerMapper,
55
+ BullishOptionSummaryMapper,
56
+ BullishTradesMapper
57
+ } from './bullish.ts'
51
58
  import {
52
59
  BybitBookChangeMapper,
53
60
  BybitDerivativeTickerMapper,
@@ -342,7 +349,8 @@ const tradesMappers = {
342
349
  shouldUseBitgetV3Mappers(localTimestamp) ? new BitgetV3TradesMapper('bitget-futures') : new BitgetTradesMapper('bitget-futures'),
343
350
  'coinbase-international': () => coinbaseInternationalTradesMapper,
344
351
  hyperliquid: () => new HyperliquidTradesMapper(),
345
- lighter: () => new LighterTradesMapper()
352
+ lighter: () => new LighterTradesMapper(),
353
+ bullish: () => new BullishTradesMapper()
346
354
  }
347
355
 
348
356
  const bookChangeMappers = {
@@ -442,7 +450,8 @@ const bookChangeMappers = {
442
450
  : new BitgetBookChangeMapper('bitget-futures'),
443
451
  'coinbase-international': () => new CoinbaseInternationalBookChangMapper(),
444
452
  hyperliquid: () => new HyperliquidBookChangeMapper(),
445
- lighter: () => new LighterBookChangeMapper()
453
+ lighter: () => new LighterBookChangeMapper(),
454
+ bullish: () => new BullishBookChangeMapper()
446
455
  }
447
456
 
448
457
  const derivativeTickersMappers = {
@@ -480,7 +489,8 @@ const derivativeTickersMappers = {
480
489
  shouldUseBitgetV3Mappers(localTimestamp) ? new BitgetV3DerivativeTickerMapper() : new BitgetDerivativeTickerMapper(),
481
490
  'coinbase-international': () => new CoinbaseInternationalDerivativeTickerMapper(),
482
491
  hyperliquid: () => new HyperliquidDerivativeTickerMapper(),
483
- lighter: () => new LighterDerivativeTickerMapper()
492
+ lighter: () => new LighterDerivativeTickerMapper(),
493
+ bullish: () => new BullishDerivativeTickerMapper()
484
494
  }
485
495
 
486
496
  const optionsSummaryMappers = {
@@ -492,7 +502,8 @@ const optionsSummaryMappers = {
492
502
  'binance-european-options': (localTimestamp: Date) =>
493
503
  shouldUseBinanceEuropeanOptionsV2Mappers(localTimestamp)
494
504
  ? new BinanceEuropeanOptionSummaryMapperV2()
495
- : new BinanceEuropeanOptionSummaryMapper()
505
+ : new BinanceEuropeanOptionSummaryMapper(),
506
+ bullish: () => new BullishOptionSummaryMapper()
496
507
  }
497
508
 
498
509
  const liquidationsMappers = {
@@ -589,6 +600,7 @@ const bookTickersMappers = {
589
600
  'coinbase-international': () => coinbaseInternationalBookTickerMapper,
590
601
  hyperliquid: () => new HyperliquidBookTickerMapper(),
591
602
  lighter: () => new LighterBookTickerMapper(),
603
+ bullish: () => new BullishBookTickerMapper(),
592
604
  'binance-european-options': () => new BinanceEuropeanOptionsBookTickerMapper()
593
605
  }
594
606
 
@@ -0,0 +1,140 @@
1
+ import { Filter } from '../types.ts'
2
+ import { MultiConnectionRealTimeFeedBase, RealTimeFeedBase } from './realtimefeed.ts'
3
+
4
+ type BullishSubscriptionTarget = {
5
+ readonly topic: string
6
+ readonly path: string
7
+ readonly symbolParam?: 'symbol' | 'assetSymbol'
8
+ }
9
+
10
+ const BULLISH_SUBSCRIPTION_TARGETS: Record<string, BullishSubscriptionTarget> = {
11
+ V1TALevel2: {
12
+ topic: 'l2Orderbook',
13
+ path: '/trading-api/v1/market-data/orderbook',
14
+ symbolParam: 'symbol'
15
+ },
16
+ V1TALevel1: {
17
+ topic: 'l1Orderbook',
18
+ path: '/trading-api/v1/market-data/orderbook',
19
+ symbolParam: 'symbol'
20
+ },
21
+ V1TAAnonymousTradeUpdate: {
22
+ topic: 'anonymousTrades',
23
+ path: '/trading-api/v1/market-data/trades',
24
+ symbolParam: 'symbol'
25
+ },
26
+ V1TATickerResponse: {
27
+ topic: 'tick',
28
+ path: '/trading-api/v1/market-data/tick',
29
+ symbolParam: 'symbol'
30
+ },
31
+ V1TAIndexPrice: {
32
+ topic: 'indexPrice',
33
+ path: '/trading-api/v1/index-data',
34
+ symbolParam: 'assetSymbol'
35
+ }
36
+ }
37
+
38
+ function getBullishSubscriptionTarget(channel: string) {
39
+ const target = BULLISH_SUBSCRIPTION_TARGETS[channel]
40
+ if (target === undefined) {
41
+ throw new Error(`BullishRealTimeFeed unsupported channel ${channel}`)
42
+ }
43
+
44
+ return target
45
+ }
46
+
47
+ export class BullishRealTimeFeed extends MultiConnectionRealTimeFeedBase {
48
+ protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
49
+ for (const [path, pathFilters] of this.groupByPath(filters)) {
50
+ yield new BullishSingleConnectionRealTimeFeed(exchange, path, pathFilters, timeoutIntervalMS, onError)
51
+ }
52
+ }
53
+
54
+ private groupByPath(filters: Filter<string>[]) {
55
+ const filtersByPath = new Map<string, Filter<string>[]>()
56
+
57
+ for (const filter of filters) {
58
+ const target = getBullishSubscriptionTarget(filter.channel)
59
+ const pathFilters = filtersByPath.get(target.path) ?? []
60
+ pathFilters.push(filter)
61
+ filtersByPath.set(target.path, pathFilters)
62
+ }
63
+
64
+ return filtersByPath
65
+ }
66
+ }
67
+
68
+ export class BullishSingleConnectionRealTimeFeed extends RealTimeFeedBase {
69
+ protected wssURL = 'wss://api.exchange.bullish.com'
70
+ private nextMessageId = 1
71
+
72
+ constructor(
73
+ exchange: string,
74
+ private readonly path: string,
75
+ filters: Filter<string>[],
76
+ timeoutIntervalMS: number | undefined,
77
+ onError?: (error: Error) => void
78
+ ) {
79
+ super(exchange, filters, timeoutIntervalMS, onError)
80
+ }
81
+
82
+ protected async getWebSocketUrl() {
83
+ const baseURL = await super.getWebSocketUrl()
84
+ return `${baseURL.replace(/\/$/, '')}${this.path}`
85
+ }
86
+
87
+ protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
88
+ return filters.flatMap((filter) => {
89
+ const target = getBullishSubscriptionTarget(filter.channel)
90
+
91
+ if (target.symbolParam == null) {
92
+ return [
93
+ {
94
+ jsonrpc: '2.0',
95
+ type: 'command',
96
+ method: 'subscribe',
97
+ id: (this.nextMessageId++).toString(),
98
+ params: {
99
+ topic: target.topic
100
+ }
101
+ }
102
+ ]
103
+ }
104
+
105
+ if (!filter.symbols || filter.symbols.length === 0) {
106
+ throw new Error('BullishRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
107
+ }
108
+
109
+ return filter.symbols.map((symbol) => {
110
+ return {
111
+ jsonrpc: '2.0',
112
+ type: 'command',
113
+ method: 'subscribe',
114
+ id: (this.nextMessageId++).toString(),
115
+ params: {
116
+ topic: target.topic,
117
+ [target.symbolParam!]: symbol
118
+ }
119
+ }
120
+ })
121
+ })
122
+ }
123
+
124
+ protected messageIsError(message: any): boolean {
125
+ return message.error !== undefined && message.error !== null
126
+ }
127
+
128
+ protected sendCustomPing = () => {
129
+ this.send({
130
+ jsonrpc: '2.0',
131
+ type: 'command',
132
+ method: 'keepalivePing',
133
+ id: (this.nextMessageId++).toString()
134
+ })
135
+ }
136
+
137
+ protected messageIsHeartbeat(message: any) {
138
+ return message.dataType === 'V1TAHeartbeat' || message.result?.message === 'Keep alive pong'
139
+ }
140
+ }
@@ -53,6 +53,7 @@ import { BitgetFuturesRealTimeFeed, BitgetRealTimeFeed } from './bitget.ts'
53
53
  import { CoinbaseInternationalRealTimeFeed } from './coinbaseinternational.ts'
54
54
  import { HyperliquidRealTimeFeed } from './hyperliquid.ts'
55
55
  import { LighterRealTimeFeed } from './lighter.ts'
56
+ import { BullishRealTimeFeed } from './bullish.ts'
56
57
 
57
58
  export * from './realtimefeed.ts'
58
59
 
@@ -116,7 +117,8 @@ const realTimeFeedsMap: {
116
117
  'bitget-futures': BitgetFuturesRealTimeFeed,
117
118
  'coinbase-international': CoinbaseInternationalRealTimeFeed,
118
119
  hyperliquid: HyperliquidRealTimeFeed,
119
- lighter: LighterRealTimeFeed
120
+ lighter: LighterRealTimeFeed,
121
+ bullish: BullishRealTimeFeed
120
122
  }
121
123
 
122
124
  export function getRealTimeFeedFactory(exchange: Exchange): RealTimeFeed {
package/src/types.ts CHANGED
@@ -27,7 +27,8 @@ export type Trade = {
27
27
  readonly id: string | undefined
28
28
  readonly price: number
29
29
  readonly amount: number
30
- readonly side: 'buy' | 'sell' | 'unknown' // liquidity taker side (aggressor)
30
+ /** liquidity taker side (aggressor), not the resting maker order side */
31
+ readonly side: 'buy' | 'sell' | 'unknown'
31
32
  readonly timestamp: Date
32
33
  readonly localTimestamp: Date
33
34
  }