Skip to content

Commit

Permalink
feat: add coinbase-international support
Browse files Browse the repository at this point in the history
  • Loading branch information
thaaddeus committed Nov 8, 2024
1 parent 7954256 commit 2d10ad6
Show file tree
Hide file tree
Showing 8 changed files with 623 additions and 9 deletions.
4 changes: 3 additions & 1 deletion src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const EXCHANGES = [
'bitfinex-derivatives',
'bitfinex',
'coinbase',
'coinbase-international',
'cryptofacilities',
'kraken',
'bitstamp',
Expand Down Expand Up @@ -476,10 +477,11 @@ const KUCOIN_FUTURES_CHANNELS = [

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

const COINBASE_INTERNATIONAL_CHANNELS = ['INSTRUMENTS', 'MATCH', 'FUNDING', 'RISK', 'LEVEL1', 'LEVEL2', 'CANDLES_ONE_MINUTE']
export const EXCHANGE_CHANNELS_INFO = {
bitmex: BITMEX_CHANNELS,
coinbase: COINBASE_CHANNELS,
'coinbase-international': COINBASE_INTERNATIONAL_CHANNELS,
deribit: DERIBIT_CHANNELS,
cryptofacilities: CRYPTOFACILITIES_CHANNELS,
bitstamp: BITSTAMP_CHANNELS,
Expand Down
322 changes: 322 additions & 0 deletions src/mappers/coinbaseinternational.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import { addMinutes, upperCaseSymbols } from '../handy'
import { BookChange, BookPriceLevel, BookTicker, DerivativeTicker, Trade } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'

export const coinbaseInternationalTradesMapper: Mapper<'coinbase-international', Trade> = {
canHandle(message: CoinbaseInternationalTradeMessage) {
return message.channel === 'MATCH' && message.type === 'UPDATE'
},

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

return [
{
channel: 'MATCH',
symbols
}
]
},

*map(message: CoinbaseInternationalTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
yield {
type: 'trade',
symbol: message.product_id,
exchange: 'coinbase-international',
id: message.match_id,
price: Number(message.trade_price),
amount: Number(message.trade_qty),
side: message.aggressor_side === 'SELL' ? 'sell' : message.aggressor_side === 'BUY' ? 'buy' : 'unknown',
timestamp: new Date(message.time),
localTimestamp: localTimestamp
}
}
}

const mapUpdateBookLevel = (level: CoinbaseInternationalUpdateBookLevel) => {
const price = Number(level[1])
const amount = Number(level[2])

return { price, amount }
}

const mapSnapshotBookLevel = (level: CoinbaseInternationalSnapshotBookLevel) => {
const price = Number(level[0])
const amount = Number(level[1])

return { price, amount }
}

const validAmountsOnly = (level: BookPriceLevel) => {
if (Number.isNaN(level.amount)) {
return false
}
if (level.amount < 0) {
return false
}

return true
}

export class CoinbaseInternationalBookChangMapper implements Mapper<'coinbase-international', BookChange> {
canHandle(message: CoinbaseInternationalLevel2Snapshot | CoinbaseInternationalLevel2Update) {
return message.channel === 'LEVEL2' && (message.type === 'SNAPSHOT' || message.type === 'UPDATE')
}

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

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

*map(
message: CoinbaseInternationalLevel2Snapshot | CoinbaseInternationalLevel2Update,
localTimestamp: Date
): IterableIterator<BookChange> {
if (message.type === 'SNAPSHOT') {
let timestamp
if (message.time !== undefined) {
timestamp = new Date(message.time)
if (timestamp.valueOf() < 0) {
timestamp = localTimestamp
}
} else {
timestamp = localTimestamp
}

yield {
type: 'book_change',
symbol: message.product_id,
exchange: 'coinbase-international',
isSnapshot: true,
bids: message.bids.map(mapSnapshotBookLevel).filter(validAmountsOnly),
asks: message.asks.map(mapSnapshotBookLevel).filter(validAmountsOnly),
timestamp,
localTimestamp
}
} else {
let timestamp = new Date(message.time)

yield {
type: 'book_change',
symbol: message.product_id,
exchange: 'coinbase-international',
isSnapshot: false,
bids: message.changes.filter((c) => c[0] === 'BUY').map(mapUpdateBookLevel),
asks: message.changes.filter((c) => c[0] === 'SELL').map(mapUpdateBookLevel),
timestamp,
localTimestamp: localTimestamp
}
}
}
}

export const coinbaseInternationalBookTickerMapper: Mapper<'coinbase-international', BookTicker> = {
canHandle(message: CoinbaseInternationalLevel1Message) {
return message.channel === 'LEVEL1' && (message.type === 'SNAPSHOT' || message.type === 'UPDATE')
},

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

return [
{
channel: 'LEVEL1',
symbols
}
]
},

*map(message: CoinbaseInternationalLevel1Message, localTimestamp: Date): IterableIterator<BookTicker> {
let timestamp = new Date(message.time)

if (message.time === undefined || timestamp.valueOf() < 0) {
timestamp = localTimestamp
}

yield {
type: 'book_ticker',
symbol: message.product_id,
exchange: 'coinbase-international',
askAmount: message.ask_qty !== undefined ? Number(message.ask_qty) : undefined,
askPrice: message.ask_price !== undefined ? Number(message.ask_price) : undefined,
bidPrice: message.bid_price !== undefined ? Number(message.bid_price) : undefined,
bidAmount: message.bid_qty !== undefined ? Number(message.bid_qty) : undefined,
timestamp,
localTimestamp: localTimestamp
}
}
}

export class CoinbaseInternationalDerivativeTickerMapper implements Mapper<'coinbase-international', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()

canHandle(message: CoinbaseInternationalTradeMessage | CoinbaseInternationalRiskMessage | CoinbaseInternationalFundingMessage) {
// perps only
if (message.product_id === undefined || message.product_id.endsWith('-PERP') === false) {
return false
}

if (message.channel === 'MATCH' && message.type === 'UPDATE') {
return true
}

if (message.channel === 'FUNDING' && message.type === 'UPDATE') {
return true
}

if (message.channel === 'RISK') {
return true
}

return false
}

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

return [
{
channel: 'MATCH',
symbols
} as const,
{
channel: 'RISK',
symbols
} as const,
{
channel: 'FUNDING',
symbols
} as const
]
}

*map(
message: CoinbaseInternationalTradeMessage | CoinbaseInternationalRiskMessage | CoinbaseInternationalFundingMessage,
localTimestamp: Date
): IterableIterator<DerivativeTicker> {
if (message.channel === 'MATCH') {
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.product_id, 'coinbase-international')
pendingTickerInfo.updateLastPrice(Number(message.trade_price))

return
}
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.product_id, 'coinbase-international')

if (message.channel === 'RISK') {
pendingTickerInfo.updateIndexPrice(Number(message.index_price))
pendingTickerInfo.updateMarkPrice(Number(message.mark_price))
pendingTickerInfo.updateOpenInterest(Number(message.open_interest))
}

if (message.channel === 'FUNDING') {
let nextFundingTime = new Date(message.time)
if (message.is_final === false) {
// If the field is_final is false, the message indicates the predicted funding rate for the next funding interval.
// https://docs.cdp.coinbase.com/intx/docs/websocket-channels#funding-channel
nextFundingTime.setUTCMinutes(0, 0, 0)
nextFundingTime = addMinutes(nextFundingTime, 60)

pendingTickerInfo.updateFundingTimestamp(nextFundingTime)
}

pendingTickerInfo.updateFundingRate(Number(message.funding_rate))
}

pendingTickerInfo.updateTimestamp(new Date(message.time))

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

// TODO: real-time

type CoinbaseInternationalTradeMessage = {
sequence: 80
match_id: '374491377330814981'
trade_price: '0.009573'
trade_qty: '1651'
aggressor_side: 'BUY' | 'SELL' | 'OPENING_FILL'
channel: 'MATCH'
type: 'UPDATE'
time: '2024-10-30T10:55:02.069Z'
product_id: 'MEW-PERP'
}

type CoinbaseInternationalSnapshotBookLevel = [string, string]

type CoinbaseInternationalLevel2Snapshot = {
sequence: 81053126
bids: CoinbaseInternationalSnapshotBookLevel[]
asks: CoinbaseInternationalSnapshotBookLevel[]
channel: 'LEVEL2'
type: 'SNAPSHOT'
time: '2024-11-06T23:59:59.812Z'
product_id: 'BB-PERP'
}

type CoinbaseInternationalUpdateBookLevel = ['BUY' | 'SELL', string, string]

type CoinbaseInternationalLevel2Update = {
sequence: 162
changes: CoinbaseInternationalUpdateBookLevel[]
channel: 'LEVEL2'
type: 'UPDATE'
time: '2024-10-30T10:55:02.348Z'
product_id: 'NOT-PERP'
}

type CoinbaseInternationalLevel1Message =
| {
sequence: 65960075
bid_price: '27.03'
bid_qty: '24.404'
ask_price: '27.037'
ask_qty: '32.302'
channel: 'LEVEL1'
type: 'SNAPSHOT'
time: '2024-11-07T00:00:00.121Z'
product_id: 'AVAX-PERP'
}
| {
sequence: 120100774
bid_price: '2719.96'
bid_qty: '0.3676'
ask_price: '2720.25'
ask_qty: '0.919'
channel: 'LEVEL1'
type: 'UPDATE'
time: '2024-11-07T00:00:59.979Z'
product_id: 'ETH-USDC'
}

type CoinbaseInternationalRiskMessage = {
sequence: 108523490
limit_up: '0.5107'
limit_down: '0.4621'
index_price: '0.4864755122500001'
mark_price: '0.4863'
settlement_price: '0.4864'
open_interest: '153090'
channel: 'RISK'
type: 'UPDATE'
time: '2024-11-07T00:00:59.950Z'
product_id: 'ENA-PERP'
}

type CoinbaseInternationalFundingMessage = {
sequence: 108521023
funding_rate: '0.000009'
is_final: false
channel: 'FUNDING'
type: 'UPDATE'
time: '2024-11-07T00:00:51.068Z'
product_id: 'DEGEN-PERP'
}
18 changes: 14 additions & 4 deletions src/mappers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ import {
} from './bybit'
import { BybitSpotBookChangeMapper, BybitSpotBookTickerMapper, BybitSpotTradesMapper } from './bybitspot'
import { CoinbaseBookChangMapper, coinbaseBookTickerMapper, coinbaseTradesMapper } from './coinbase'
import {
CoinbaseInternationalBookChangMapper,
coinbaseInternationalBookTickerMapper,
CoinbaseInternationalDerivativeTickerMapper,
coinbaseInternationalTradesMapper
} from './coinbaseinternational'
import { coinflexBookChangeMapper, CoinflexDerivativeTickerMapper, coinflexTradesMapper } from './coinflex'
import { CryptoComBookChangeMapper, CryptoComBookTickerMapper, CryptoComDerivativeTickerMapper, CryptoComTradesMapper } from './cryptocom'
import {
Expand Down Expand Up @@ -274,7 +280,8 @@ const tradesMappers = {
'binance-european-options': () => new BinanceEuropeanOptionsTradesMapper(),
'okex-spreads': () => new OkexSpreadsTradesMapper(),
bitget: () => new BitgetTradesMapper('bitget'),
'bitget-futures': () => new BitgetTradesMapper('bitget-futures')
'bitget-futures': () => new BitgetTradesMapper('bitget-futures'),
'coinbase-international': () => coinbaseInternationalTradesMapper
}

const bookChangeMappers = {
Expand Down Expand Up @@ -364,7 +371,8 @@ const bookChangeMappers = {
'binance-european-options': () => new BinanceEuropeanOptionsBookChangeMapper(),
'okex-spreads': () => new OkexSpreadsBookChangeMapper(),
bitget: () => new BitgetBookChangeMapper('bitget'),
'bitget-futures': () => new BitgetBookChangeMapper('bitget-futures')
'bitget-futures': () => new BitgetBookChangeMapper('bitget-futures'),
'coinbase-international': () => new CoinbaseInternationalBookChangMapper()
}

const derivativeTickersMappers = {
Expand Down Expand Up @@ -399,7 +407,8 @@ const derivativeTickersMappers = {
'crypto-com': () => new CryptoComDerivativeTickerMapper('crypto-com'),
'woo-x': () => new WooxDerivativeTickerMapper(),
'kucoin-futures': () => new KucoinFuturesDerivativeTickerMapper(),
'bitget-futures': () => new BitgetDerivativeTickerMapper()
'bitget-futures': () => new BitgetDerivativeTickerMapper(),
'coinbase-international': () => new CoinbaseInternationalDerivativeTickerMapper()
}

const optionsSummaryMappers = {
Expand Down Expand Up @@ -493,7 +502,8 @@ const bookTickersMappers = {
'okex-spreads': () => new OkexSpreadsBookTickerMapper(),
'kucoin-futures': () => new KucoinFuturesBookTickerMapper(),
bitget: () => new BitgetBookTickerMapper('bitget'),
'bitget-futures': () => new BitgetBookTickerMapper('bitget-futures')
'bitget-futures': () => new BitgetBookTickerMapper('bitget-futures'),
'coinbase-international': () => coinbaseInternationalBookTickerMapper
}

export const normalizeTrades = <T extends keyof typeof tradesMappers>(exchange: T, localTimestamp: Date): Mapper<T, Trade> => {
Expand Down
Loading

0 comments on commit 2d10ad6

Please sign in to comment.