Skip to content

Commit d07bd3b

Browse files
committed
Add Bitget V3 normalized mappings
1 parent 9195ca8 commit d07bd3b

8 files changed

Lines changed: 733 additions & 22 deletions

File tree

ADD_NEW_EXCHANGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Mapper decisions to make explicit:
4848

4949
Normalized type semantics:
5050

51-
- **Trades**`side` is liquidity taker side: `buy` means the aggressor bought, `sell` means the aggressor sold. Invert maker-side flags when needed. Skip off-book maintenance events such as insurance fund or ADL unless the product contract explicitly requires them.
51+
- **Trades**`side` is liquidity taker side: `buy` means the aggressor bought, `sell` means the aggressor sold. Invert maker-side flags when needed. Skip off-book maintenance events such as insurance fund or ADL unless the product contract explicitly requires them. If a trade channel uses `snapshot` followed by `update`, map only `update`; the initial `snapshot` is recent-trade backfill and must have a test that emits nothing to avoid duplicate or stale trades after reconnect. Map trade `snapshot` only when the exchange sends trades exclusively as snapshots and there is no incremental update variant.
5252
- **Book changes**`book_change` is L2 market-by-price data. `isSnapshot=true` means consumers discard prior book state. `isSnapshot=false` means consumers apply absolute price-level amounts to the current book. `amount=0` removes the level.
5353
- **Book tickers**`book_ticker` comes from native top-of-book or BBO feeds. It is not `quotes`, which are computed from reconstructed L2 books.
5454
- **Derivative tickers** — keep `lastPrice`, `openInterest`, `indexPrice`, `markPrice`, funding fields, and predicted funding fields aligned with exchange meaning. `fundingTimestamp` is the next funding event timestamp.

src/consts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,8 @@ const KUCOIN_FUTURES_CHANNELS = [
514514
'contractMarket/snapshot'
515515
]
516516

517-
const BITGET_CHANNELS = ['trade', 'books1', 'books15']
518-
const BITGET_FUTURES_CHANNELS = ['trade', 'books1', 'books15', 'ticker']
517+
const BITGET_CHANNELS = ['trade', 'publicTrade', 'books1', 'books15', 'books']
518+
const BITGET_FUTURES_CHANNELS = ['trade', 'publicTrade', 'books1', 'books15', 'books', 'ticker', 'liquidation']
519519
const COINBASE_INTERNATIONAL_CHANNELS = ['INSTRUMENTS', 'MATCH', 'FUNDING', 'RISK', 'LEVEL1', 'LEVEL2', 'CANDLES_ONE_MINUTE']
520520

521521
const HYPERLIQUID_CHANNELS = ['l2Book', 'trades', 'activeAssetCtx', 'activeSpotAssetCtx', 'bbo']

src/mappers/bitget.ts

Lines changed: 237 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { upperCaseSymbols } from '../handy.ts'
2-
import { BookChange, BookTicker, DerivativeTicker, Exchange, Trade } from '../types.ts'
1+
import { asNumberIfValid, upperCaseSymbols } from '../handy.ts'
2+
import { BookChange, BookTicker, DerivativeTicker, Exchange, Liquidation, Trade } from '../types.ts'
33
import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
44

55
export class BitgetTradesMapper implements Mapper<'bitget' | 'bitget-futures', Trade> {
@@ -155,6 +155,182 @@ export class BitgetDerivativeTickerMapper implements Mapper<'bitget-futures', De
155155
}
156156
}
157157

158+
export class BitgetV3TradesMapper implements Mapper<'bitget' | 'bitget-futures', Trade> {
159+
constructor(private readonly _exchange: Exchange) {}
160+
161+
canHandle(message: BitgetV3TradeMessage) {
162+
return message.arg.topic === 'publicTrade' && message.action === 'update'
163+
}
164+
165+
getFilters(symbols?: string[]) {
166+
symbols = upperCaseSymbols(symbols)
167+
168+
return [
169+
{
170+
channel: 'publicTrade',
171+
symbols
172+
} as const
173+
]
174+
}
175+
176+
*map(message: BitgetV3TradeMessage, localTimestamp: Date): IterableIterator<Trade> {
177+
for (const trade of message.data) {
178+
yield {
179+
type: 'trade',
180+
symbol: message.arg.symbol,
181+
exchange: this._exchange,
182+
id: trade.i,
183+
price: Number(trade.p),
184+
amount: Number(trade.v),
185+
side: trade.S === 'buy' ? 'buy' : 'sell',
186+
timestamp: new Date(Number(trade.T)),
187+
localTimestamp
188+
}
189+
}
190+
}
191+
}
192+
193+
export class BitgetV3BookChangeMapper implements Mapper<'bitget' | 'bitget-futures', BookChange> {
194+
constructor(private readonly _exchange: Exchange) {}
195+
196+
canHandle(message: BitgetV3OrderbookMessage) {
197+
return message.arg.topic === 'books' && (message.action === 'snapshot' || message.action === 'update')
198+
}
199+
200+
getFilters(symbols?: string[]) {
201+
symbols = upperCaseSymbols(symbols)
202+
203+
return [
204+
{
205+
channel: 'books',
206+
symbols
207+
} as const
208+
]
209+
}
210+
211+
*map(message: BitgetV3OrderbookMessage, localTimestamp: Date): IterableIterator<BookChange> {
212+
for (const orderbookData of message.data) {
213+
yield {
214+
type: 'book_change',
215+
symbol: message.arg.symbol,
216+
exchange: this._exchange,
217+
isSnapshot: message.action === 'snapshot',
218+
bids: orderbookData.b.map(mapPriceLevel),
219+
asks: orderbookData.a.map(mapPriceLevel),
220+
timestamp: new Date(Number(orderbookData.ts)),
221+
localTimestamp
222+
}
223+
}
224+
}
225+
}
226+
227+
export class BitgetV3BookTickerMapper implements Mapper<'bitget' | 'bitget-futures', BookTicker> {
228+
constructor(private readonly _exchange: Exchange) {}
229+
230+
canHandle(message: BitgetV3BBoMessage) {
231+
return message.arg.topic === 'books1' && message.action === 'snapshot'
232+
}
233+
234+
getFilters(symbols?: string[]) {
235+
symbols = upperCaseSymbols(symbols)
236+
237+
return [
238+
{
239+
channel: 'books1',
240+
symbols
241+
} as const
242+
]
243+
}
244+
245+
*map(message: BitgetV3BBoMessage, localTimestamp: Date): IterableIterator<BookTicker> {
246+
for (const bboMessage of message.data) {
247+
yield {
248+
type: 'book_ticker',
249+
symbol: message.arg.symbol,
250+
exchange: this._exchange,
251+
askAmount: bboMessage.a[0] ? asNumberIfValid(bboMessage.a[0][1]) : undefined,
252+
askPrice: bboMessage.a[0] ? asNumberIfValid(bboMessage.a[0][0]) : undefined,
253+
bidPrice: bboMessage.b[0] ? asNumberIfValid(bboMessage.b[0][0]) : undefined,
254+
bidAmount: bboMessage.b[0] ? asNumberIfValid(bboMessage.b[0][1]) : undefined,
255+
timestamp: new Date(Number(bboMessage.ts)),
256+
localTimestamp
257+
}
258+
}
259+
}
260+
}
261+
262+
export class BitgetV3DerivativeTickerMapper implements Mapper<'bitget-futures', DerivativeTicker> {
263+
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
264+
265+
canHandle(message: BitgetV3TickerMessage) {
266+
return message.arg.topic === 'ticker' && (message.action === 'snapshot' || message.action === 'update')
267+
}
268+
269+
getFilters(symbols?: string[]) {
270+
symbols = upperCaseSymbols(symbols)
271+
272+
return [
273+
{
274+
channel: 'ticker',
275+
symbols
276+
} as const
277+
]
278+
}
279+
280+
*map(message: BitgetV3TickerMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
281+
for (const tickerMessage of message.data) {
282+
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.arg.symbol, 'bitget-futures')
283+
284+
pendingTickerInfo.updateIndexPrice(Number(tickerMessage.indexPrice))
285+
pendingTickerInfo.updateMarkPrice(Number(tickerMessage.markPrice))
286+
pendingTickerInfo.updateOpenInterest(Number(tickerMessage.openInterest))
287+
pendingTickerInfo.updateLastPrice(Number(tickerMessage.lastPrice))
288+
pendingTickerInfo.updateTimestamp(new Date(Number(message.ts)))
289+
290+
if (tickerMessage.nextFundingTime !== '' && tickerMessage.nextFundingTime !== '0') {
291+
pendingTickerInfo.updateFundingTimestamp(new Date(Number(tickerMessage.nextFundingTime)))
292+
pendingTickerInfo.updateFundingRate(Number(tickerMessage.fundingRate))
293+
}
294+
295+
if (pendingTickerInfo.hasChanged()) {
296+
yield pendingTickerInfo.getSnapshot(localTimestamp)
297+
}
298+
}
299+
}
300+
}
301+
302+
export class BitgetV3LiquidationsMapper implements Mapper<'bitget-futures', Liquidation> {
303+
canHandle(message: BitgetV3LiquidationMessage) {
304+
return message.arg.topic === 'liquidation' && message.action === 'update'
305+
}
306+
307+
getFilters() {
308+
return [
309+
{
310+
channel: 'liquidation',
311+
symbols: undefined
312+
} as const
313+
]
314+
}
315+
316+
*map(message: BitgetV3LiquidationMessage, localTimestamp: Date): IterableIterator<Liquidation> {
317+
for (const liquidation of message.data) {
318+
yield {
319+
type: 'liquidation',
320+
symbol: liquidation.symbol,
321+
exchange: 'bitget-futures',
322+
id: undefined,
323+
price: Number(liquidation.price),
324+
amount: Number(liquidation.amount),
325+
// Bitget side is position side, normalized side is the liquidated aggressor side.
326+
side: liquidation.side === 'buy' ? 'sell' : 'buy',
327+
timestamp: new Date(Number(liquidation.ts)),
328+
localTimestamp
329+
}
330+
}
331+
}
332+
}
333+
158334
type BitgetTradeMessage = {
159335
action: 'update'
160336
arg: { instType: 'SPOT'; channel: 'trade'; instId: 'OPUSDT' }
@@ -214,3 +390,62 @@ type BitgetTickerMessage = {
214390
]
215391
ts: 1730332823220
216392
}
393+
394+
type BitgetV3TradeMessage = {
395+
action: 'snapshot' | 'update'
396+
arg: { instType: string; topic: 'publicTrade'; symbol: string }
397+
data: { i: string; p: string; v: string; S: 'buy' | 'sell'; T: string; L: string; isRPI?: string }[]
398+
ts: number
399+
}
400+
401+
type BitgetV3BookLevel = [string, string]
402+
403+
type BitgetV3OrderbookMessage = {
404+
action: 'snapshot' | 'update'
405+
arg: { instType: string; topic: 'books'; symbol: string }
406+
data: { a: BitgetV3BookLevel[]; b: BitgetV3BookLevel[]; checksum: number; seq: number; pseq: number; ts: string }[]
407+
ts: number
408+
}
409+
410+
type BitgetV3BBoMessage = {
411+
action: 'snapshot'
412+
arg: { instType: string; topic: 'books1'; symbol: string }
413+
data: { a: BitgetV3BookLevel[]; b: BitgetV3BookLevel[]; checksum: number; seq: number; pseq: number; ts: string }[]
414+
ts: number
415+
}
416+
417+
type BitgetV3TickerMessage = {
418+
action: 'snapshot' | 'update'
419+
arg: { instType: string; topic: 'ticker'; symbol: string }
420+
data: [
421+
{
422+
highPrice24h: string
423+
lowPrice24h: string
424+
openPrice24h: string
425+
lastPrice: string
426+
turnover24h: string
427+
volume24h: string
428+
bid1Price: string
429+
ask1Price: string
430+
bid1Size: string
431+
ask1Size: string
432+
price24hPcnt: string
433+
indexPrice: string
434+
markPrice: string
435+
fundingRate: string
436+
openInterest: string
437+
deliveryTime: string
438+
deliveryStartTime: string
439+
deliveryStatus: string
440+
nextFundingTime: string
441+
}
442+
]
443+
ts: number
444+
}
445+
446+
type BitgetV3LiquidationMessage = {
447+
action: 'update'
448+
arg: { instType: string; topic: 'liquidation' }
449+
data: { symbol: string; side: 'buy' | 'sell'; price: string; amount: string; ts: string }[]
450+
ts: number
451+
}

src/mappers/index.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,17 @@ import {
2727
BitfinexTradesMapper
2828
} from './bitfinex.ts'
2929
import { BitflyerBookChangeMapper, bitflyerBookTickerMapper, bitflyerTradesMapper } from './bitflyer.ts'
30-
import { BitgetBookChangeMapper, BitgetBookTickerMapper, BitgetDerivativeTickerMapper, BitgetTradesMapper } from './bitget.ts'
30+
import {
31+
BitgetBookChangeMapper,
32+
BitgetBookTickerMapper,
33+
BitgetDerivativeTickerMapper,
34+
BitgetTradesMapper,
35+
BitgetV3BookChangeMapper,
36+
BitgetV3BookTickerMapper,
37+
BitgetV3DerivativeTickerMapper,
38+
BitgetV3LiquidationsMapper,
39+
BitgetV3TradesMapper
40+
} from './bitget.ts'
3141
import {
3242
BitmexBookChangeMapper,
3343
BitmexDerivativeTickerMapper,
@@ -208,6 +218,12 @@ const shouldUseBybitAllLiquidationFeed = (localTimestamp: Date) => {
208218
return isRealTime(localTimestamp) || localTimestamp.valueOf() >= BYBIT_V5_API_ALL_LIQUIDATION_SUPPORT_DATE.valueOf()
209219
}
210220

221+
const BITGET_V3_API_SWITCH_DATE = new Date('2026-04-28T00:00:00.000Z')
222+
223+
const shouldUseBitgetV3Mappers = (localTimestamp: Date) => {
224+
return isRealTime(localTimestamp) || localTimestamp.valueOf() >= BITGET_V3_API_SWITCH_DATE.valueOf()
225+
}
226+
211227
const OKCOIN_V5_API_SWITCH_DATE = new Date('2023-04-27T00:00:00.000Z')
212228
const shouldUseOkcoinV5Mappers = (localTimestamp: Date) => {
213229
return isRealTime(localTimestamp) || localTimestamp.valueOf() >= OKCOIN_V5_API_SWITCH_DATE.valueOf()
@@ -320,8 +336,10 @@ const tradesMappers = {
320336
? new BinanceEuropeanOptionsTradesMapperV2()
321337
: new BinanceEuropeanOptionsTradesMapper(),
322338
'okex-spreads': () => new OkexSpreadsTradesMapper(),
323-
bitget: () => new BitgetTradesMapper('bitget'),
324-
'bitget-futures': () => new BitgetTradesMapper('bitget-futures'),
339+
bitget: (localTimestamp: Date) =>
340+
shouldUseBitgetV3Mappers(localTimestamp) ? new BitgetV3TradesMapper('bitget') : new BitgetTradesMapper('bitget'),
341+
'bitget-futures': (localTimestamp: Date) =>
342+
shouldUseBitgetV3Mappers(localTimestamp) ? new BitgetV3TradesMapper('bitget-futures') : new BitgetTradesMapper('bitget-futures'),
325343
'coinbase-international': () => coinbaseInternationalTradesMapper,
326344
hyperliquid: () => new HyperliquidTradesMapper(),
327345
lighter: () => new LighterTradesMapper()
@@ -416,8 +434,12 @@ const bookChangeMappers = {
416434
? new BinanceEuropeanOptionsBookChangeMapperV2()
417435
: new BinanceEuropeanOptionsBookChangeMapper(),
418436
'okex-spreads': () => new OkexSpreadsBookChangeMapper(),
419-
bitget: () => new BitgetBookChangeMapper('bitget'),
420-
'bitget-futures': () => new BitgetBookChangeMapper('bitget-futures'),
437+
bitget: (localTimestamp: Date) =>
438+
shouldUseBitgetV3Mappers(localTimestamp) ? new BitgetV3BookChangeMapper('bitget') : new BitgetBookChangeMapper('bitget'),
439+
'bitget-futures': (localTimestamp: Date) =>
440+
shouldUseBitgetV3Mappers(localTimestamp)
441+
? new BitgetV3BookChangeMapper('bitget-futures')
442+
: new BitgetBookChangeMapper('bitget-futures'),
421443
'coinbase-international': () => new CoinbaseInternationalBookChangMapper(),
422444
hyperliquid: () => new HyperliquidBookChangeMapper(),
423445
lighter: () => new LighterBookChangeMapper()
@@ -454,7 +476,8 @@ const derivativeTickersMappers = {
454476
'crypto-com': () => new CryptoComDerivativeTickerMapper('crypto-com'),
455477
'woo-x': () => new WooxDerivativeTickerMapper(),
456478
'kucoin-futures': () => new KucoinFuturesDerivativeTickerMapper(),
457-
'bitget-futures': () => new BitgetDerivativeTickerMapper(),
479+
'bitget-futures': (localTimestamp: Date) =>
480+
shouldUseBitgetV3Mappers(localTimestamp) ? new BitgetV3DerivativeTickerMapper() : new BitgetDerivativeTickerMapper(),
458481
'coinbase-international': () => new CoinbaseInternationalDerivativeTickerMapper(),
459482
hyperliquid: () => new HyperliquidDerivativeTickerMapper(),
460483
lighter: () => new LighterDerivativeTickerMapper()
@@ -496,7 +519,8 @@ const liquidationsMappers = {
496519
? new OkexV5LiquidationsMapper('okex-futures')
497520
: new OkexLiquidationsMapper('okex-futures', 'futures'),
498521
'okex-swap': (localTimestamp: Date) =>
499-
shouldUseOkexV5Mappers(localTimestamp) ? new OkexV5LiquidationsMapper('okex-swap') : new OkexLiquidationsMapper('okex-swap', 'swap')
522+
shouldUseOkexV5Mappers(localTimestamp) ? new OkexV5LiquidationsMapper('okex-swap') : new OkexLiquidationsMapper('okex-swap', 'swap'),
523+
'bitget-futures': () => new BitgetV3LiquidationsMapper()
500524
}
501525

502526
const bookTickersMappers = {
@@ -556,8 +580,12 @@ const bookTickersMappers = {
556580
'gate-io': () => new GateIOV4BookTickerMapper('gate-io'),
557581
'okex-spreads': () => new OkexSpreadsBookTickerMapper(),
558582
'kucoin-futures': () => new KucoinFuturesBookTickerMapper(),
559-
bitget: () => new BitgetBookTickerMapper('bitget'),
560-
'bitget-futures': () => new BitgetBookTickerMapper('bitget-futures'),
583+
bitget: (localTimestamp: Date) =>
584+
shouldUseBitgetV3Mappers(localTimestamp) ? new BitgetV3BookTickerMapper('bitget') : new BitgetBookTickerMapper('bitget'),
585+
'bitget-futures': (localTimestamp: Date) =>
586+
shouldUseBitgetV3Mappers(localTimestamp)
587+
? new BitgetV3BookTickerMapper('bitget-futures')
588+
: new BitgetBookTickerMapper('bitget-futures'),
561589
'coinbase-international': () => coinbaseInternationalBookTickerMapper,
562590
hyperliquid: () => new HyperliquidBookTickerMapper(),
563591
lighter: () => new LighterBookTickerMapper(),

0 commit comments

Comments
 (0)