Skip to content

Commit

Permalink
feat: add bitget and bitget-futures support
Browse files Browse the repository at this point in the history
  • Loading branch information
thaaddeus committed Nov 5, 2024
1 parent 6b0dd20 commit c14fca0
Show file tree
Hide file tree
Showing 8 changed files with 733 additions and 9 deletions.
11 changes: 9 additions & 2 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export const EXCHANGES = [
'kucoin-futures',
'bitnomial',
'woo-x',
'blockchain-com'
'blockchain-com',
'bitget',
'bitget-futures'
] as const

const BINANCE_CHANNELS = ['trade', 'aggTrade', 'ticker', 'depth', 'depthSnapshot', 'bookTicker', 'recentTrades', 'borrowInterest'] as const
Expand Down Expand Up @@ -472,6 +474,9 @@ const KUCOIN_FUTURES_CHANNELS = [
'contractMarket/snapshot'
]

const BITGET_CHANNELS = ['trade', 'books1', 'books']
const BITGET_FUTURES_CHANNELS = ['trade', 'books1', 'books', 'ticker']

export const EXCHANGE_CHANNELS_INFO = {
bitmex: BITMEX_CHANNELS,
coinbase: COINBASE_CHANNELS,
Expand Down Expand Up @@ -527,5 +532,7 @@ export const EXCHANGE_CHANNELS_INFO = {
'blockchain-com': BLOCKCHAIN_COM_CHANNELS,
'binance-european-options': BINANCE_EUROPEAN_OPTIONS_CHANNELS,
'okex-spreads': OKEX_SPREADS_CHANNELS,
'kucoin-futures': KUCOIN_FUTURES_CHANNELS
'kucoin-futures': KUCOIN_FUTURES_CHANNELS,
bitget: BITGET_CHANNELS,
'bitget-futures': BITGET_FUTURES_CHANNELS
}
5 changes: 5 additions & 0 deletions src/handy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ export function* batch(symbols: string[], batchSize: number) {
yield symbols.slice(i, i + batchSize)
}
}
export function* batchObjects<T>(payload: T[], batchSize: number) {
for (let i = 0; i < payload.length; i += batchSize) {
yield payload.slice(i, i + batchSize)
}
}

export function parseμs(dateString: string): number {
// check if we have ISO 8601 format date string, e.g: 2019-06-01T00:03:03.1238784Z or 2020-07-22T00:09:16.836773Z
Expand Down
230 changes: 230 additions & 0 deletions src/mappers/bitget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { upperCaseSymbols } from '../handy'
import { BookChange, BookTicker, DerivativeTicker, Exchange, Trade } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'

export class BitgetTradesMapper implements Mapper<'bitget' | 'bitget-futures', Trade> {
constructor(private readonly _exchange: Exchange) {}

canHandle(message: BitgetTradeMessage) {
return message.arg.channel === 'trade' && message.action === 'update'
}

getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)

return [
{
channel: 'trade',
symbols
} as const
]
}

*map(message: BitgetTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
for (let trade of message.data) {
yield {
type: 'trade',
symbol: message.arg.instId,
exchange: this._exchange,
id: trade.tradeId,
price: Number(trade.price),
amount: Number(trade.size),
side: trade.side === 'buy' ? 'buy' : 'sell',
timestamp: new Date(Number(trade.ts)),
localTimestamp: localTimestamp
}
}
}
}

function mapPriceLevel(level: [string, string]) {
return {
price: Number(level[0]),
amount: Number(level[1])
}
}
export class BitgetBookChangeMapper implements Mapper<'bitget' | 'bitget-futures', BookChange> {
constructor(private readonly _exchange: Exchange) {}

canHandle(message: BitgetOrderbookMessage) {
return message.arg.channel === 'books' && (message.action === 'update' || message.action === 'snapshot')
}

getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)

return [
{
channel: 'books',
symbols
} as const
]
}

*map(message: BitgetOrderbookMessage, localTimestamp: Date): IterableIterator<BookChange> {
for (let orderbookData of message.data) {
yield {
type: 'book_change',
symbol: message.arg.instId,
exchange: this._exchange,
isSnapshot: message.action === 'snapshot',
bids: orderbookData.bids.map(mapPriceLevel),
asks: orderbookData.asks.map(mapPriceLevel),
timestamp: new Date(Number(orderbookData.ts)),
localTimestamp
}
}
}
}

export class BitgetBookTickerMapper implements Mapper<'bitget' | 'bitget-futures', BookTicker> {
constructor(private readonly _exchange: Exchange) {}

canHandle(message: BitgetBBoMessage) {
return message.arg.channel === 'books1' && message.action === 'snapshot'
}

getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)

return [
{
channel: `books1` as const,
symbols
}
]
}

*map(message: BitgetBBoMessage, localTimestamp: Date): IterableIterator<BookTicker> {
for (const bboMessage of message.data) {
const ticker: BookTicker = {
type: 'book_ticker',
symbol: message.arg.instId,
exchange: this._exchange,

askAmount: Number(bboMessage.asks[0][1]),
askPrice: Number(bboMessage.asks[0][0]),

bidPrice: Number(bboMessage.bids[0][0]),
bidAmount: Number(bboMessage.bids[0][1]),
timestamp: new Date(Number(bboMessage.ts)),
localTimestamp: localTimestamp
}

yield ticker
}
}
}

export class BitgetDerivativeTickerMapper implements Mapper<'bitget-futures', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()

canHandle(message: BitgetTickerMessage) {
return message.arg.channel === 'ticker' && message.action === 'snapshot'
}

getFilters(symbols?: string[]) {
return [
{
channel: 'ticker',
symbols
} as const
]
}

*map(message: BitgetTickerMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
for (const tickerMessage of message.data) {
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(tickerMessage.symbol, 'bitget-futures')

pendingTickerInfo.updateIndexPrice(Number(tickerMessage.indexPrice))
pendingTickerInfo.updateMarkPrice(Number(tickerMessage.markPrice))
pendingTickerInfo.updateOpenInterest(Number(tickerMessage.holdingAmount))
pendingTickerInfo.updateLastPrice(Number(tickerMessage.lastPr))

pendingTickerInfo.updateTimestamp(new Date(Number(tickerMessage.ts)))

if (tickerMessage.nextFundingTime !== '0') {
pendingTickerInfo.updateFundingTimestamp(new Date(Number(tickerMessage.nextFundingTime)))
pendingTickerInfo.updateFundingRate(Number(tickerMessage.fundingRate))
}

if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
}

type BitgetTradeMessage = {
action: 'update'
arg: { instType: 'SPOT'; channel: 'trade'; instId: 'OPUSDT' }
data: [{ ts: '1730332800983'; price: '1.717'; size: '56.16'; side: 'buy'; tradeId: '1235670816495050754' }]
ts: 1730332800989
}

type BitgetOrderbookMessage =
| {
action: 'snapshot'
arg: { instType: 'SPOT'; channel: 'books'; instId: 'SYLOUSDT' }
data: [
{
asks: [string, string][]
bids: [string, string][]
checksum: 0
ts: '1730331046984'
}
]
ts: 1730332800437
}
| {
action: 'update'
arg: { instType: 'SPOT'; channel: 'books'; instId: 'BANDUSDT' }
data: [
{
asks: [string, string][]
bids: [string, string][]
checksum: 79466786
ts: '1730332859977'
}
]
ts: 1730332859979
}

type BitgetBBoMessage = {
action: 'snapshot'
arg: { instType: 'SPOT'; channel: 'books1'; instId: 'METISUSDT' }
data: [{ asks: [['44.90', '0.6927']]; bids: [['44.82', '3.5344']]; checksum: 0; ts: '1730332859988' }]
ts: 1730332859989
}

type BitgetTickerMessage = {
action: 'snapshot'
arg: { instType: 'COIN-FUTURES'; channel: 'ticker'; instId: 'BTCUSD' }
data: [
{
instId: 'BTCUSD'
lastPr: '72331.5'
bidPr: '72331.5'
askPr: '72331.8'
bidSz: '7.296'
askSz: '0.02'
open24h: '72047.8'
high24h: '72934.8'
low24h: '71422.8'
change24h: '-0.00561'
fundingRate: '0.000116'
nextFundingTime: string
markPrice: string
indexPrice: string
holdingAmount: string
baseVolume: '7543.376'
quoteVolume: '544799876.924'
openUtc: '72335.3'
symbolType: '1'
symbol: 'BTCUSD'
deliveryPrice: '0'
ts: '1730332823217'
}
]
ts: 1730332823220
}
16 changes: 12 additions & 4 deletions src/mappers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
BitfinexTradesMapper
} from './bitfinex'
import { BitflyerBookChangeMapper, bitflyerBookTickerMapper, bitflyerTradesMapper } from './bitflyer'
import { BitgetBookChangeMapper, BitgetBookTickerMapper, BitgetDerivativeTickerMapper, BitgetTradesMapper } from './bitget'
import {
BitmexBookChangeMapper,
BitmexDerivativeTickerMapper,
Expand Down Expand Up @@ -271,7 +272,9 @@ const tradesMappers = {
'blockchain-com': () => new BlockchainComTradesMapper(),
'bybit-options': () => new BybitV5TradesMapper('bybit-options'),
'binance-european-options': () => new BinanceEuropeanOptionsTradesMapper(),
'okex-spreads': () => new OkexSpreadsTradesMapper()
'okex-spreads': () => new OkexSpreadsTradesMapper(),
bitget: () => new BitgetTradesMapper('bitget'),
'bitget-futures': () => new BitgetTradesMapper('bitget-futures')
}

const bookChangeMappers = {
Expand Down Expand Up @@ -359,7 +362,9 @@ const bookChangeMappers = {
'blockchain-com': () => new BlockchainComBookChangeMapper(),
'bybit-options': () => new BybitV5BookChangeMapper('bybit-options', 25),
'binance-european-options': () => new BinanceEuropeanOptionsBookChangeMapper(),
'okex-spreads': () => new OkexSpreadsBookChangeMapper()
'okex-spreads': () => new OkexSpreadsBookChangeMapper(),
bitget: () => new BitgetBookChangeMapper('bitget'),
'bitget-futures': () => new BitgetBookChangeMapper('bitget-futures')
}

const derivativeTickersMappers = {
Expand Down Expand Up @@ -393,7 +398,8 @@ const derivativeTickersMappers = {
'crypto-com-derivatives': () => new CryptoComDerivativeTickerMapper('crypto-com-derivatives'),
'crypto-com': () => new CryptoComDerivativeTickerMapper('crypto-com'),
'woo-x': () => new WooxDerivativeTickerMapper(),
'kucoin-futures': () => new KucoinFuturesDerivativeTickerMapper()
'kucoin-futures': () => new KucoinFuturesDerivativeTickerMapper(),
'bitget-futures': () => new BitgetDerivativeTickerMapper()
}

const optionsSummaryMappers = {
Expand Down Expand Up @@ -485,7 +491,9 @@ const bookTickersMappers = {
bybit: () => new BybitV5BookTickerMapper('bybit'),
'gate-io': () => new GateIOV4BookTickerMapper('gate-io'),
'okex-spreads': () => new OkexSpreadsBookTickerMapper(),
'kucoin-futures': () => new KucoinFuturesBookTickerMapper()
'kucoin-futures': () => new KucoinFuturesBookTickerMapper(),
bitget: () => new BitgetBookTickerMapper('bitget'),
'bitget-futures': () => new BitgetBookTickerMapper('bitget-futures')
}

export const normalizeTrades = <T extends keyof typeof tradesMappers>(exchange: T, localTimestamp: Date): Mapper<T, Trade> => {
Expand Down
59 changes: 59 additions & 0 deletions src/realtimefeeds/bitget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { batchObjects } from '../handy'
import { Filter } from '../types'
import { RealTimeFeedBase } from './realtimefeed'

abstract class BitgetRealTimeFeedBase extends RealTimeFeedBase {
protected throttleSubscribeMS = 100
protected readonly wssURL = 'wss://ws.bitget.com/v2/ws/public'

protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
const argsInputs = filters.flatMap((filter) => {
if (!filter.symbols || filter.symbols.length === 0) {
throw new Error('BitgetRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
}

return filter.symbols.map((symbol) => {
return {
instType: this.getInstType(symbol),
channel: filter.channel,
instId: symbol
}
})
})

const payload = [...batchObjects(argsInputs, 5)].map((args) => {
return {
op: 'subscribe',
args
}
})

return payload
}

protected messageIsError(message: any): boolean {
return message.event === 'error'
}

abstract getInstType(symbol: string): string
}

export class BitgetRealTimeFeed extends BitgetRealTimeFeedBase {
getInstType(_: string) {
return 'SPOT'
}
}

export class BitgetFuturesRealTimeFeed extends BitgetRealTimeFeedBase {
getInstType(symbol: string) {
if (symbol.endsWith('USDT')) {
return 'USDT-FUTURES'
}

if (symbol.endsWith('PERP')) {
return 'USDC-FUTURES'
}

return 'COIN-FUTURES'
}
}
Loading

0 comments on commit c14fca0

Please sign in to comment.