@hackape/tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
361 lines (321 loc) • 10.2 kB
text/typescript
import got from 'got'
import { Exchange } from '../types'
import { ExchangeDetailsBase } from './exchangedetails'
type AvailableSymbol = ExchangeDetailsBase<any>['availableSymbols'][number]
const marketInfoAPIs: { [key in Exchange]?: string } = {
binance: 'https://api.binance.com/api/v3/exchangeInfo',
'binance-futures': 'https://fapi.binance.com/fapi/v1/exchangeInfo',
'binance-delivery': 'https://dapi.binance.com/dapi/v1/exchangeInfo',
okex: 'https://www.okex.com/api/spot/v3/instruments',
'okex-swap': 'https://www.okex.com/api/swap/v3/instruments',
'okex-futures': 'https://www.okex.com/api/futures/v3/instruments',
huobi: 'https://api.huobi.pro/v1/common/symbols',
'huobi-dm-swap': 'https://api.hbdm.com/swap-api/v1/swap_contract_info',
'huobi-dm': 'https://api.hbdm.com/api/v1/contract_contract_info'
}
async function getRawMarketInfo<T extends Exchange>(exchange: T) {
const url = marketInfoAPIs[exchange]
if (!url) return
const info: any = await got.get(url!).json()
return { exchange, info } as MarketInfoResponse<T>
}
export async function getMarketInfo(exchange: Exchange) {
const resp = await getRawMarketInfo(exchange)
if (!resp) return
const database = new MarketInfoDatabase()
switch (resp.exchange) {
case 'binance': {
const symbols = resp.info.symbols
symbols.forEach((symbol) => {
const id = symbol.symbol
database.add({
id,
type: 'spot',
base: symbol.baseAsset,
quote: symbol.quoteAsset,
precision: getBinancePrecisionFromFilters(symbol.filters)
})
})
break
}
case 'binance-futures': // all perpetual
case 'binance-delivery': {
// some perpetual, some delivery
const symbols: MergeArrayElement<typeof resp.info.symbols> = resp.info.symbols
symbols.forEach((symbol) => {
const id = symbol.symbol
let multiplier = 1
let contractType: 'perpetual' | 'future' = 'perpetual'
if ('contractSize' in symbol) {
multiplier = symbol.contractSize
contractType = symbol.contractType === 'PERPETUAL' ? 'perpetual' : 'future'
}
let contract: ContractInfo
if (exchange === 'binance-delivery') {
contract = {
isInverse: true,
unit: symbol.quoteAsset,
multiplier
}
} else {
contract = {
isInverse: false,
unit: symbol.baseAsset,
multiplier
}
}
database.add({
id,
type: contractType,
base: symbol.baseAsset,
quote: symbol.quoteAsset,
precision: {
price: symbol.pricePrecision,
amount: symbol.quantityPrecision
},
contract
})
})
break
}
case 'okex': {
const symbols = resp.info
symbols.forEach((symbol) => {
database.add({
id: symbol.instrument_id,
type: 'spot',
base: symbol.base_currency,
quote: symbol.quote_currency,
precision: {
price: getDecimalPlaces(symbol.tick_size),
amount: getDecimalPlaces(symbol.size_increment)
}
})
})
break
}
case 'okex-swap':
case 'okex-futures': {
const contractType = resp.exchange === 'okex-swap' ? 'perpetual' : 'future'
const symbols: MergeArrayElement<typeof resp.info> = resp.info
symbols.forEach((symbol) => {
const id = symbol.instrument_id
const contract: ContractInfo = {
isInverse: symbol.is_inverse === 'true',
multiplier: Number(symbol.contract_val),
unit: symbol.contract_val_currency
}
database.add({
id,
type: contractType,
base: symbol.base_currency,
quote: symbol.quote_currency,
precision: {
price: getDecimalPlaces(symbol.tick_size),
amount: 0 // okex contract is traded in "cont", which is integer
},
contract
})
})
break
}
case 'huobi': {
const symbols = resp.info.data
symbols.forEach((symbol) => {
database.add({
id: symbol.symbol,
type: 'spot',
base: symbol['base-currency'],
quote: symbol['quote-currency'],
precision: {
price: symbol['price-precision'],
amount: symbol['amount-precision']
}
})
})
break
}
case 'huobi-dm-swap':
case 'huobi-dm': {
const contractType = resp.exchange === 'huobi-dm' ? 'future' : 'perpetual'
const symbols: MergeArrayElement<typeof resp.info.data> = resp.info.data
symbols.forEach((symbol) => {
const id = getHuobiSymbolKey(symbol)
database.add({
id,
type: contractType,
base: symbol.symbol,
quote: 'USD',
precision: {
price: getDecimalPlaces(symbol.price_tick),
amount: 0
},
contract: {
isInverse: true,
multiplier: symbol.contract_size,
unit: 'USD'
}
})
})
break
}
}
return database
}
class MarketInfoDatabase {
db: Map<string, MarketInfoRecord> = new Map()
assetInfo: Map<string, { base: string; quote: string }> = new Map()
count = 0
add(record: MarketInfoRecord) {
this.count++
this.assetInfo.set(record.id.toLowerCase(), { base: record.base, quote: record.quote })
const key = `${record.base}_${record.quote}_${record.type}`.toLowerCase()
if (this.db.has(key)) {
const prevRecord = this.db.get(key)!
const isEqual = compareMarketInfoRecord(prevRecord, record)
if (!isEqual) {
throw Error('wierd case')
}
} else {
this.db.set(key, record)
}
}
// @ts-ignore
mixin(exchange: Exchange, symbol: AvailableSymbol) {
const assetInfo = this.assetInfo.get(symbol.id.toLowerCase())!
if (!assetInfo) {
console.warn(`cannot find asset info for ${exchange}:${symbol.id}`)
return symbol
}
const key = `${assetInfo.base}_${assetInfo.quote}_${symbol.type}`.toLowerCase()
const record = this.db.get(key)
if (record) {
symbol.market = {
base: record.base,
quote: record.quote,
precision: record.precision,
contract: record.contract
}
} else {
console.warn(`cannot find market info for ${exchange}:${symbol.id}`)
}
return symbol
}
}
function getBinancePrecisionFromFilters(filters: BinanceSpot.Symbol['filters']) {
const priceFilter = filters.find((filter) => filter.filterType === 'PRICE_FILTER')!
const lotSize = filters.find((filter) => filter.filterType === 'LOT_SIZE')!
const price = getDecimalPlaces(priceFilter.tickSize)
const amount = getDecimalPlaces(lotSize.stepSize)
return { price, amount }
}
function compareMarketInfoRecord(a: MarketInfoRecord, b: MarketInfoRecord) {
const symbolInfoIsEqual =
a.base === b.base &&
a.quote === b.quote &&
a.type === b.type &&
a.precision.price === b.precision.price &&
a.precision.amount === b.precision.amount
if (a.contract && b.contract) {
const contractInfoIsEqual =
a.contract.isInverse === b.contract.isInverse &&
a.contract.multiplier === b.contract.multiplier &&
a.contract.unit === b.contract.unit
return contractInfoIsEqual && symbolInfoIsEqual
} else {
// this is impossible
throw Error('missing contract field')
}
}
type RawMarketInfoMap = {
okex: OkexSpot.MarketInfo
'okex-swap': OkexSwap.MarketInfo
'okex-futures': OkexFutures.MarketInfo
huobi: HuobiSpot.MarketInfo
'huobi-dm': HuobiDm.MarketInfo
'huobi-dm-swap': HuobiDmSwap.MarketInfo
binance: BinanceSpot.MarketInfo
'binance-futures': BinanceFutures.MarketInfo
'binance-delivery': BinanceDelivery.MarketInfo
}
type MarketInfoResponse<T extends Exchange> = T extends keyof RawMarketInfoMap ? { exchange: T; info: RawMarketInfoMap[T] } : undefined
type ContractInfo = {
isInverse: boolean
multiplier: number
unit: string
}
export interface MarketInfo {
base: string
quote: string
precision: {
price: number
amount: number
}
contract?: ContractInfo
}
interface MarketInfoRecord extends MarketInfo {
id: string
type: 'spot' | 'perpetual' | 'future'
}
type MergeArrayElement<T> = Array<T extends Array<any> ? T[number] : never>
function getDecimalPlaces(x: number | string) {
if (typeof x === 'string') x = Number(x)
const str = String(x)
if (str.includes('e')) {
if (str.includes('e-')) {
return Number(str.split('e-')[1])
} else {
return 0
}
} else {
const decimals = str.split('.')[1]
return decimals ? decimals.length : 0
}
}
function getHuobiSymbolKey(symbol: HuobiDm.Symbol | HuobiDmSwap.Symbol) {
if ('contract_type' in symbol) {
switch (symbol.contract_type) {
case 'this_week':
return `${symbol.symbol}_CW`
case 'next_week':
return `${symbol.symbol}_NW`
case 'quarter':
return `${symbol.symbol}_CQ`
case 'next_quarter':
return `${symbol.symbol}_NQ`
}
} else {
return symbol.contract_code
}
}
/*
function getRecordKeyForSymbol(exchange: Exchange, symbol: AvailableSymbol) {
let symbolId = symbol.id
const type = symbol.type
switch (exchange) {
case 'binance-futures': {
// "btcusdt"
return `${symbolId.slice(0, -4)}_usdt_${type}`.toLowerCase()
}
case 'binance-delivery': {
// "btcusd_perp" or "btcusd_210326"
symbolId = symbolId.slice(0, symbolId.indexOf('_'))
return `${symbolId.slice(0, -3)}_usd_${type}`.toLowerCase()
}
case 'okex-swap':
case 'okex-futures': {
const parts = symbolId.split('-')
return `${parts[0]}_${parts[1]}_${type}`.toLowerCase()
}
case 'huobi-dm': {
// "BTC_CW"
return `${symbolId.slice(0, -3)}_usd_${type}`.toLowerCase()
}
case 'huobi-dm-swap': {
// "BTC-USD"
return `${symbolId.replace('-', '_')}_${type}`.toLowerCase()
}
}
return
}
*/