Skip to content

Commit

Permalink
feat: bitget adapter (#159)
Browse files Browse the repository at this point in the history
* chore(gemini adapter): correct comments

* chore(gemini adapter): linting

* chore(gemini adapter): linting

* feat(exchange adapters): add bitget adapter

* test(bitget adapter): fix test

* test(bitget adapter): correct test descriptions

* chore(bitget adapter): make linter happpyy again

* chore: update certificate

---------

Co-authored-by: nina / ნინა <barbakadzeninaa@gmail.com>
  • Loading branch information
tobikuhlmann and ninabarbakadze committed Feb 27, 2023
1 parent 220baea commit c87cedf
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/data_aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { KrakenAdapter } from './exchange_adapters/kraken'
import { KuCoinAdapter } from './exchange_adapters/kucoin'
import { BitstampAdapter } from './exchange_adapters/bitstamp'
import { MercadoAdapter } from './exchange_adapters/mercado'
import { BitgetAdapter } from './exchange_adapters/bitget'
import { MetricCollector } from './metric_collector'
import { PriceSource, WeightedPrice } from './price_source'
import {
Expand Down Expand Up @@ -66,6 +67,8 @@ function adapterFromExchangeName(name: Exchange, config: ExchangeAdapterConfig):
return new OKXAdapter(config)
case Exchange.WHITEBIT:
return new WhitebitAdapter(config)
case Exchange.BITGET:
return new BitgetAdapter(config)
}
}

Expand Down
104 changes: 104 additions & 0 deletions src/exchange_adapters/bitget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Exchange } from '../utils'
import { BaseExchangeAdapter, ExchangeDataType, Ticker, Trade } from './base'

export class BitgetAdapter extends BaseExchangeAdapter {
baseApiUrl = 'https://api.bitget.com/api'

readonly _exchangeName = Exchange.BITGET
// cloudflare cert
readonly _certFingerprint256 =
'3A:BB:E6:3D:AF:75:6C:50:16:B6:B8:5F:52:01:5F:D8:E8:AC:BE:27:7C:50:87:B1:27:A6:05:63:A8:41:ED:8A'

async fetchTicker(): Promise<Ticker> {
return this.parseTicker(
await this.fetchFromApi(
ExchangeDataType.TICKER,
`spot/v1/market/ticker?symbol=${this.pairSymbol}_SPBL`
)
)
}

async fetchTrades(): Promise<Trade[]> {
/**
* Trades are cool, but empty arrays are cooler.
* @bogdan, 01/2023
*/
return []
}

protected generatePairSymbol(): string {
const base = BaseExchangeAdapter.standardTokenSymbolMap.get(this.config.baseCurrency)
const quote = BaseExchangeAdapter.standardTokenSymbolMap.get(this.config.quoteCurrency)

return `${base}${quote}`
}

/**
* Parses the json responses from the ticker and summary endpoints into the
* standard format for a Ticker object
*
* @param pubtickerJson json response from the ticker endpoint
* spot/v1/market/ticker?symbol=/${this.pairSymbol}_SPBL
* https://api.bitget.com/api/spot/v1/market/ticker?symbol=BTCBRL_SPBL
* https://bitgetlimited.github.io/apidoc/en/spot/#get-single-ticker
*
* {"code":"00000",
* "data":
* {
* "baseVol":"9.18503", // (price symbol, e.g. "USD") The volume denominated in the price currency
* "buyOne":"121890", // buy one price
* "close":"121905", // Latest transaction price
* "quoteVol":"1119715.23314", // (price symbol, e.g. "USD") The volume denominated in the quantity currency
* "sellOne":"122012", // sell one price
* "symbol":"BTCBRL", // Symbol
* "ts":"1677490448241", // Timestamp
* },
* "msg":"success",
* "requestTime":"1677490448872" // Request status
* }
*/
parseTicker(pubtickerJson: any): Ticker {
const data = pubtickerJson.data || {}
const ticker = {
...this.priceObjectMetadata,
timestamp: Number(data.ts)!,
bid: this.safeBigNumberParse(data.buyOne)!,
ask: this.safeBigNumberParse(data.sellOne)!,
lastPrice: this.safeBigNumberParse(data.close)!,
baseVolume: this.safeBigNumberParse(data.baseVol)!,
quoteVolume: this.safeBigNumberParse(data.quoteVol)!,
}

this.verifyTicker(ticker)
return ticker
}

/**
* Checks if the orderbook for the relevant pair is live. If it's not, the price
* data from Ticker + Trade endpoints may be inaccurate.
*
* https://api.bitget.com/api/spot/v1/public/product?symbol=BTCBRL_SPBL
*
* API response example:
* {"code":"00000",
* "data":
* {
* "baseCoin":"BTC",
* "status":"online",
* symbol":"btcbrl_SPBL",
* },
* "msg":"success",
* "requestTime":"0"
* }
*
* @returns bool
*/
async isOrderbookLive(): Promise<boolean> {
const res = await this.fetchFromApi(
ExchangeDataType.ORDERBOOK_STATUS,
`spot/v1/public/product?symbol=${this.pairSymbol}_SPBL`
)

return res.data.status === 'online'
}
}
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export enum Exchange {
MERCADO = 'MERCADO',
OKX = 'OKX',
WHITEBIT = 'WHITEBIT',
BITGET = 'BITGET',
}

export enum ExternalCurrency {
Expand Down Expand Up @@ -72,8 +73,10 @@ export enum OracleCurrencyPair {
USDBRL = 'USDBRL',
USDCUSD = 'USDCUSD',
USDCEUR = 'USDCEUR',
USDCBRL = 'USDCBRL',
USDCUSDT = 'USDCUSDT',
EURUSD = 'EURUSD',
BTCUSDC = 'BTCUSDC',
}

export const CoreCurrencyPair: OracleCurrencyPair[] = [
Expand Down Expand Up @@ -108,6 +111,8 @@ export const CurrencyPairBaseQuote: Record<
[OracleCurrencyPair.USDCUSDT]: { base: ExternalCurrency.USDC, quote: ExternalCurrency.USDT },
[OracleCurrencyPair.EURUSD]: { base: ExternalCurrency.EUR, quote: ExternalCurrency.USD },
[OracleCurrencyPair.BTCUSDT]: { base: ExternalCurrency.BTC, quote: ExternalCurrency.USDT },
[OracleCurrencyPair.USDCBRL]: { base: ExternalCurrency.USDC, quote: ExternalCurrency.BRL },
[OracleCurrencyPair.BTCUSDC]: { base: ExternalCurrency.BTC, quote: ExternalCurrency.USDC },
}

export enum AggregationMethod {
Expand Down
97 changes: 97 additions & 0 deletions test/exchange_adapters/bitget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import BigNumber from 'bignumber.js'
import { baseLogger } from '../../src/default_config'
import { ExchangeAdapterConfig } from '../../src/exchange_adapters/base'
import { BitgetAdapter } from '../../src/exchange_adapters/bitget'
import { Exchange, ExternalCurrency } from '../../src/utils'

describe('BitgetAdapter', () => {
let bitgetAdapter: BitgetAdapter
const config: ExchangeAdapterConfig = {
baseCurrency: ExternalCurrency.BTC,
baseLogger,
quoteCurrency: ExternalCurrency.BRL,
}
beforeEach(() => {
bitgetAdapter = new BitgetAdapter(config)
})
afterEach(() => {
jest.clearAllTimers()
jest.clearAllMocks()
})

const mockPubtickerJson = {
code: '00000',
data: {
baseVol: '9.18503', // (price symbol, e.g. "USD") The volume denominated in the price currency
buyOne: '121890', // buy one price = bid pice
close: '121905', // Latest transaction price
quoteVol: '1119715.23314', // (price symbol, e.g. "USD") The volume denominated in the quantity currency
sellOne: '122012', // sell one price = ask price
symbol: 'BTCBRL', // Symbol
ts: 1677490448241, // Timestamp
},
msg: 'success',
requestTime: '1677490448872', // Request status
}

describe('fetchTrades', () => {
it('returns an empty array', async () => {
const tradesFetched = await bitgetAdapter.fetchTrades()
expect(tradesFetched).toEqual([])
})
})

describe('parseTicker', () => {
it('handles a response that matches the documentation', () => {
const ticker = bitgetAdapter.parseTicker(mockPubtickerJson)
expect(ticker).toEqual({
source: Exchange.BITGET,
symbol: bitgetAdapter.standardPairSymbol,
ask: new BigNumber(122012),
baseVolume: new BigNumber(9.18503),
bid: new BigNumber(121890),
lastPrice: new BigNumber(121905),
quoteVolume: new BigNumber(1119715.23314),
timestamp: 1677490448241,
})
})

it('throws an error when a json field mapped to a required ticker field is missing', () => {
expect(() => {
bitgetAdapter.parseTicker({
...mockPubtickerJson,
data: {
sellOne: undefined,
buyOne: undefined,
close: undefined,
baseVol: undefined,
},
})
}).toThrowError('bid, ask, lastPrice, baseVolume not defined')
})
})

describe('isOrderbookLive', () => {
const mockStatusJson = {
code: '00000',
data: {
base_coin: 'BTC',
status: 'online',
symbol: 'btcbrl_SPBL',
},
msg: 'success',
requestTime: '0',
}

it("returns false when status isn't 'online'", async () => {
const response = { ...mockStatusJson, data: { status: 'closed' } }
jest.spyOn(bitgetAdapter, 'fetchFromApi').mockReturnValue(Promise.resolve(response))
expect(await bitgetAdapter.isOrderbookLive()).toEqual(false)
})

it("returns true when status is 'online'", async () => {
jest.spyOn(bitgetAdapter, 'fetchFromApi').mockReturnValue(Promise.resolve(mockStatusJson))
expect(await bitgetAdapter.isOrderbookLive()).toEqual(true)
})
})
})

0 comments on commit c87cedf

Please sign in to comment.