Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions cryptofeed/exchanges/bybit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@
from yapic import json

from cryptofeed.connection import AsyncConnection, RestEndpoint, Routes, WebsocketEndpoint
from cryptofeed.defines import BID, ASK, BUY, BYBIT, CANCELLED, CANCELLING, CANDLES, FAILED, FILLED, FUNDING, L2_BOOK, LIMIT, LIQUIDATIONS, MAKER, MARKET, OPEN, PARTIAL, SELL, SUBMITTING, TAKER, TRADES, OPEN_INTEREST, INDEX, ORDER_INFO, FILLS, FUTURES, PERPETUAL, SPOT, TICKER
from cryptofeed.defines import BID, ASK, BUY, BYBIT, CANCELLED, CANCELLING, CANDLES, FAILED, FILLED, FUNDING, L1_BOOK, L2_BOOK, LIMIT, LIQUIDATIONS, MAKER, MARKET, OPEN, PARTIAL, SELL, SUBMITTING, TAKER, TRADES, OPEN_INTEREST, INDEX, ORDER_INFO, FILLS, FUTURES, PERPETUAL, SPOT, TICKER
from cryptofeed.feed import Feed
from cryptofeed.types import OrderBook, Trade, Index, OpenInterest, Funding, OrderInfo, Fill, Candle, Liquidation, Ticker
from cryptofeed.types import OrderBook, Trade, Index, OpenInterest, Funding, OrderInfo, Fill, Candle, Liquidation, Ticker, L1Book

LOG = logging.getLogger('feedhandler')


class Bybit(Feed):
id = BYBIT
websocket_channels = {
L1_BOOK: '',
L2_BOOK: '', # Assigned in self.subscribe
TRADES: 'publicTrade',
FILLS: 'execution',
Expand All @@ -39,8 +40,8 @@ class Bybit(Feed):
TICKER: 'tickers'
}
websocket_endpoints = [
WebsocketEndpoint('wss://stream.bybit.com/v5/public/linear', instrument_filter=('TYPE', (FUTURES, PERPETUAL)), channel_filter=(websocket_channels[L2_BOOK], websocket_channels[TRADES], websocket_channels[INDEX], websocket_channels[OPEN_INTEREST], websocket_channels[FUNDING], websocket_channels[CANDLES], websocket_channels[LIQUIDATIONS], websocket_channels[TICKER]), sandbox='wss://stream-testnet.bybit.com/v5/public/linear', options={'compression': None}),
WebsocketEndpoint('wss://stream.bybit.com/v5/public/spot', instrument_filter=('TYPE', (SPOT)), channel_filter=(websocket_channels[L2_BOOK], websocket_channels[TRADES], websocket_channels[CANDLES],), sandbox='wss://stream-testnet.bybit.com/v5/public/spot', options={'compression': None}),
WebsocketEndpoint('wss://stream.bybit.com/v5/public/linear', instrument_filter=('TYPE', (FUTURES, PERPETUAL)), channel_filter=(websocket_channels[L1_BOOK], websocket_channels[L2_BOOK], websocket_channels[TRADES], websocket_channels[INDEX], websocket_channels[OPEN_INTEREST], websocket_channels[FUNDING], websocket_channels[CANDLES], websocket_channels[LIQUIDATIONS], websocket_channels[TICKER]), sandbox='wss://stream-testnet.bybit.com/v5/public/linear', options={'compression': None}),
WebsocketEndpoint('wss://stream.bybit.com/v5/public/spot', instrument_filter=('TYPE', (SPOT)), channel_filter=(websocket_channels[L1_BOOK], websocket_channels[L2_BOOK], websocket_channels[TRADES], websocket_channels[CANDLES],), sandbox='wss://stream-testnet.bybit.com/v5/public/spot', options={'compression': None}),
WebsocketEndpoint('wss://stream.bybit.com/realtime_private', channel_filter=(websocket_channels[ORDER_INFO], websocket_channels[FILLS]), instrument_filter=('QUOTE', ('USDT',)), sandbox='wss://stream-testnet.bybit.com/realtime_private', options={'compression': None}),
]
rest_endpoints = [
Expand Down Expand Up @@ -231,6 +232,8 @@ async def message_handler(self, msg: str, conn, timestamp: float):
LOG.error("%s: Error from exchange %s", conn.uuid, msg)
elif msg["topic"].startswith('publicTrade'):
await self._trade(msg, timestamp, market)
elif msg["topic"].startswith('orderbook.1'):
await self._top_of_book(msg, timestamp, market)
elif msg["topic"].startswith('orderbook'):
await self._book(msg, timestamp, market)
elif msg['topic'].startswith('kline'):
Expand Down Expand Up @@ -279,6 +282,13 @@ async def subscribe(self, connection: AsyncConnection):
PERPETUAL: "orderbook.200",
}
sub = [f"{l2_book_channel[sym.type]}.{pair}"]
elif self.exchange_channel_to_std(chan) == L1_BOOK:
l1_book_channel = {
SPOT: "orderbook.1",
FUTURES: "orderbook.1",
PERPETUAL: "orderbook.1",
}
sub = [f"{l1_book_channel[sym.type]}.{pair}"]
else:
sub = [f"{chan}.{pair}"]

Expand Down Expand Up @@ -404,6 +414,46 @@ async def _book(self, msg: dict, timestamp: float, market: str):

await self.book_callback(L2_BOOK, self._l2_book[pair], timestamp, timestamp=self.timestamp_normalize(int(msg['ts'])), raw=msg, delta=delta)

async def _top_of_book(self, msg: dict, timestamp: float, market: str):
'''
{
'topic': 'orderbook.1.BTCUSDT',
'type': 'snapshot',
'ts': 1749822878982,
'data': {
's': 'BTCUSDT',
'b': [['104714.40', '0.727']],
'a': [['104714.50', '16.541']],
'u': 58267067,
'seq': 416909700197
},
'cts': 1749822878980
}
'''
pair = msg['topic'].split('.')[-1]
update_type = msg['type']
data = msg['data']

if market == 'spot':
pair = self.convert_to_spot_name(self, data['s'])
if not pair:
return

symbol = self.exchange_symbol_to_std_symbol(pair)

if update_type == 'snapshot':
l1 = L1Book(
self.id,
symbol,
Decimal(data['b'][0][0]) if 'b' in data else Decimal(0),
Decimal(data['b'][0][1]) if 'b' in data else Decimal(0),
Decimal(data['a'][0][0]) if 'a' in data else Decimal(0),
Decimal(data['a'][0][1]) if 'a' in data else Decimal(0),
int(msg['ts']),
raw=msg
)
await self.callback(L1_BOOK, l1, timestamp)

async def _ticker_open_interest_funding_index(self, msg: dict, timestamp: float, conn: AsyncConnection):
'''
{
Expand Down
3 changes: 2 additions & 1 deletion cryptofeed/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from cryptofeed.callback import Callback
from cryptofeed.connection import AsyncConnection, HTTPAsyncConn, WSAsyncConn
from cryptofeed.connection_handler import ConnectionHandler
from cryptofeed.defines import BALANCES, CANDLES, FUNDING, INDEX, L2_BOOK, L3_BOOK, LIQUIDATIONS, OPEN_INTEREST, ORDER_INFO, POSITIONS, TICKER, TRADES, FILLS
from cryptofeed.defines import BALANCES, CANDLES, FUNDING, INDEX, L1_BOOK, L2_BOOK, L3_BOOK, LIQUIDATIONS, OPEN_INTEREST, ORDER_INFO, POSITIONS, TICKER, TRADES, FILLS
from cryptofeed.exceptions import BidAskOverlapping
from cryptofeed.exchange import Exchange
from cryptofeed.types import OrderBook
Expand Down Expand Up @@ -125,6 +125,7 @@ def __init__(self, candle_interval='1m', candle_closed_only=True, timeout=120, t
self._l2_book = {}
self.callbacks = {FUNDING: Callback(None),
INDEX: Callback(None),
L1_BOOK: Callback(None),
L2_BOOK: Callback(None),
L3_BOOK: Callback(None),
LIQUIDATIONS: Callback(None),
Expand Down
97 changes: 54 additions & 43 deletions cryptofeed/types.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,60 @@ cdef class Ticker:
def __hash__(self):
return hash(self.__repr__())

cdef class L1Book:
cdef readonly str exchange
cdef readonly str symbol
cdef readonly object bid
cdef readonly object bid_size
cdef readonly object ask
cdef readonly object ask_size
cdef readonly object timestamp
cdef readonly object raw

def __init__(self, exchange, symbol, bid, bid_size, ask, ask_size, timestamp, raw=None):
assert isinstance(bid, Decimal)
assert isinstance(bid_size, Decimal)
assert isinstance(ask, Decimal)
assert isinstance(ask_size, Decimal)
assert timestamp is None or isinstance(timestamp, float)

self.exchange = exchange
self.symbol = symbol
self.bid = bid
self.bid_size = bid_size
self.ask = ask
self.ask_size = ask_size
self.timestamp = timestamp
self.raw = raw

@staticmethod
def from_dict(data: dict) -> L1Book:
return L1Book(
data['exchange'],
data['symbol'],
Decimal(data['bid']),
Decimal(data['bid_size']),
Decimal(data['ask']),
Decimal(data['ask_size']),
data['timestamp']
)

cpdef dict to_dict(self, numeric_type=None, none_to=False):
if numeric_type is None:
data = {'exchange': self.exchange, 'symbol': self.symbol, 'bid': self.bid, 'bid_size': self.bid_size, 'ask': self.ask, 'ask_size': self.ask_size, 'timestamp': self.timestamp}
else:
data = {'exchange': self.exchange, 'symbol': self.symbol, 'bid': numeric_type(self.bid), 'bid_size': numeric_type(self.bid_size), 'ask': numeric_type(self.ask), 'ask_size': numeric_type(self.ask_size), 'timestamp': self.timestamp}
return data if not none_to else convert_none_values(data, none_to)

def __repr__(self):
return f"exchange: {self.exchange} symbol: {self.symbol} bid: {self.bid} bid_size: {self.bid_size} ask: {self.ask} ask_size: {self.ask_size} timestamp: {self.timestamp}"

def __eq__(self, cmp):
return self.exchange == cmp.exchange and self.symbol == cmp.symbol and self.bid == cmp.bid and self.bid_size == cmp.bid_size and self.ask == cmp.ask and self.ask_size == cmp.ask_size and self.timestamp == cmp.timestamp

def __hash__(self):
return hash(self.__repr__())


cdef class Liquidation:
cdef readonly str exchange
Expand Down Expand Up @@ -623,49 +677,6 @@ cdef class Balance:
def __hash__(self):
return hash(self.__repr__())


cdef class L1Book:
cdef readonly str exchange
cdef readonly str symbol
cdef readonly object bid_price
cdef readonly object bid_size
cdef readonly object ask_price
cdef readonly object ask_size
cdef readonly double timestamp
cdef readonly dict raw

def __init__(self, exchange, symbol, bid_price, bid_size, ask_price, ask_size, timestamp, raw=None):
assert isinstance(bid_price, Decimal)
assert isinstance(bid_size, Decimal)
assert isinstance(ask_price, Decimal)
assert isinstance(ask_size, Decimal)

self.exchange = exchange
self.symbol = symbol
self.bid_price = bid_price
self.bid_size = bid_size
self.ask_price = ask_price
self.ask_size = ask_size
self.timestamp = timestamp
self.raw = raw

cpdef dict to_dict(self, numeric_type=None, none_to=False):
if numeric_type is None:
data = {'exchange': self.exchange, 'symbol': self.symbol, 'bid_price': self.bid_price, 'bid_size': self.bid_size, 'ask_price': self.ask_price, 'ask_size': self.ask_size, 'timestamp': self.timestamp}
else:
data = {'exchange': self.exchange, 'symbol': self.symbol, 'bid_price': numeric_type(self.bid_price), 'bid_size': numeric_type(self.bid_size), 'ask_price': numeric_type(self.ask_price), 'ask_size': numeric_type(self.ask_size), 'timestamp': self.timestamp}
return data if not none_to else convert_none_values(data, none_to)

def __repr__(self):
return f'exchange: {self.exchange} symbol: {self.symbol} bid_price: {self.bid_price} bid_size: {self.bid_size}, ask_price: {self.ask_price} ask_size: {self.ask_size} timestamp: {self.timestamp}'

def __eq__(self, cmp):
return self.exchange == cmp.exchange and self.symbol == cmp.symbol and self.bid_price == cmp.bid_price and self.bid_size == cmp.bid_size and self.ask_price == cmp.ask_price and self.ask_size == cmp.ask_size and self.timestamp == cmp.timestamp

def __hash__(self):
return hash(self.__repr__())


cdef class Transaction:
cdef readonly str exchange
cdef readonly str currency
Expand Down
2 changes: 1 addition & 1 deletion sample_data/BYBIT.0
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
https://api.bybit.com/v2/public/symbols -> 1618677785.481953: {"ret_code":0,"ret_msg":"OK","ext_code":"","ext_info":"","result":[{"name":"BTCUSD","alias":"BTCUSD","status":"Trading","base_currency":"BTC","quote_currency":"USD","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":100,"leverage_step":"0.01"},"price_filter":{"min_price":"0.5","max_price":"999999.5","tick_size":"0.5"},"lot_size_filter":{"max_trading_qty":1000000,"min_trading_qty":1,"qty_step":1}},{"name":"ETHUSD","alias":"ETHUSD","status":"Trading","base_currency":"ETH","quote_currency":"USD","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":50,"leverage_step":"0.01"},"price_filter":{"min_price":"0.05","max_price":"99999.95","tick_size":"0.05"},"lot_size_filter":{"max_trading_qty":1000000,"min_trading_qty":1,"qty_step":1}},{"name":"EOSUSD","alias":"EOSUSD","status":"Trading","base_currency":"EOS","quote_currency":"USD","price_scale":3,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":50,"leverage_step":"0.01"},"price_filter":{"min_price":"0.001","max_price":"1999.999","tick_size":"0.001"},"lot_size_filter":{"max_trading_qty":1000000,"min_trading_qty":1,"qty_step":1}},{"name":"XRPUSD","alias":"XRPUSD","status":"Trading","base_currency":"XRP","quote_currency":"USD","price_scale":4,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":50,"leverage_step":"0.01"},"price_filter":{"min_price":"0.0001","max_price":"199.9999","tick_size":"0.0001"},"lot_size_filter":{"max_trading_qty":1000000,"min_trading_qty":1,"qty_step":1}},{"name":"BTCUSDT","alias":"BTCUSDT","status":"Trading","base_currency":"BTC","quote_currency":"USDT","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":100,"leverage_step":"0.01"},"price_filter":{"min_price":"0.5","max_price":"999999.5","tick_size":"0.5"},"lot_size_filter":{"max_trading_qty":100,"min_trading_qty":0.001,"qty_step":0.001}},{"name":"BCHUSDT","alias":"BCHUSDT","status":"Trading","base_currency":"BCH","quote_currency":"USDT","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":50,"leverage_step":"0.01"},"price_filter":{"min_price":"0.5","max_price":"100000","tick_size":"0.05"},"lot_size_filter":{"max_trading_qty":600,"min_trading_qty":0.01,"qty_step":0.01}},{"name":"ETHUSDT","alias":"ETHUSDT","status":"Trading","base_currency":"ETH","quote_currency":"USDT","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":50,"leverage_step":"0.01"},"price_filter":{"min_price":"0.5","max_price":"100000","tick_size":"0.05"},"lot_size_filter":{"max_trading_qty":1000,"min_trading_qty":0.01,"qty_step":0.01}},{"name":"LTCUSDT","alias":"LTCUSDT","status":"Trading","base_currency":"LTC","quote_currency":"USDT","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":25,"leverage_step":"0.01"},"price_filter":{"min_price":"0.01","max_price":"20000","tick_size":"0.01"},"lot_size_filter":{"max_trading_qty":2000,"min_trading_qty":0.1,"qty_step":0.1}},{"name":"LINKUSDT","alias":"LINKUSDT","status":"Trading","base_currency":"LINK","quote_currency":"USDT","price_scale":3,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":25,"leverage_step":"0.01"},"price_filter":{"min_price":"0.001","max_price":"2000","tick_size":"0.001"},"lot_size_filter":{"max_trading_qty":10000,"min_trading_qty":0.1,"qty_step":0.1}},{"name":"XTZUSDT","alias":"XTZUSDT","status":"Trading","base_currency":"XTZ","quote_currency":"USDT","price_scale":3,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":25,"leverage_step":"0.01"},"price_filter":{"min_price":"0.001","max_price":"2000","tick_size":"0.001"},"lot_size_filter":{"max_trading_qty":20000,"min_trading_qty":0.1,"qty_step":0.1}},{"name":"ADAUSDT","alias":"ADAUSDT","status":"Trading","base_currency":"ADA","quote_currency":"USDT","price_scale":4,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":25,"leverage_step":"0.01"},"price_filter":{"min_price":"0.0001","max_price":"200","tick_size":"0.0001"},"lot_size_filter":{"max_trading_qty":240000,"min_trading_qty":1,"qty_step":1}},{"name":"DOTUSDT","alias":"DOTUSDT","status":"Trading","base_currency":"DOT","quote_currency":"USDT","price_scale":3,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":25,"leverage_step":"0.01"},"price_filter":{"min_price":"0.005","max_price":"10000","tick_size":"0.005"},"lot_size_filter":{"max_trading_qty":15000,"min_trading_qty":0.1,"qty_step":0.1}},{"name":"UNIUSDT","alias":"UNIUSDT","status":"Trading","base_currency":"UNI","quote_currency":"USDT","price_scale":4,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":25,"leverage_step":"0.01"},"price_filter":{"min_price":"0.0001","max_price":"1000","tick_size":"0.0001"},"lot_size_filter":{"max_trading_qty":10000,"min_trading_qty":0.1,"qty_step":0.1}},{"name":"BTCUSDU21","alias":"BTCUSD0924","status":"Trading","base_currency":"BTC","quote_currency":"USD","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":100,"leverage_step":"0.01"},"price_filter":{"min_price":"0.5","max_price":"999999.5","tick_size":"0.5"},"lot_size_filter":{"max_trading_qty":1000000,"min_trading_qty":1,"qty_step":1}},{"name":"BTCUSDM21","alias":"BTCUSD0625","status":"Trading","base_currency":"BTC","quote_currency":"USD","price_scale":2,"taker_fee":"0.00075","maker_fee":"-0.00025","leverage_filter":{"min_leverage":1,"max_leverage":100,"leverage_step":"0.01"},"price_filter":{"min_price":"0.5","max_price":"999999.5","tick_size":"0.5"},"lot_size_filter":{"max_trading_qty":1000000,"min_trading_qty":1,"qty_step":1}}],"time_now":"1618677785.322543"}
configuration: {"trades":["BCH-USDT-PERP","XTZ-USDT-PERP","UNI-USDT-PERP","EOS-USD-PERP","LTC-USDT-PERP","DOT-USDT-PERP","LINK-USDT-PERP","ADA-USDT-PERP","ETH-USDT-PERP","BTC-USD-PERP"],"l2_book":["BCH-USDT-PERP","XTZ-USDT-PERP","UNI-USDT-PERP","EOS-USD-PERP","LTC-USDT-PERP","DOT-USDT-PERP","LINK-USDT-PERP","ADA-USDT-PERP","ETH-USDT-PERP","BTC-USD-PERP"]}
configuration: {"trades":["BCH-USDT-PERP","XTZ-USDT-PERP","UNI-USDT-PERP","EOS-USD-PERP","LTC-USDT-PERP","DOT-USDT-PERP","LINK-USDT-PERP","ADA-USDT-PERP","ETH-USDT-PERP","BTC-USD-PERP"],"l1_book":["BAND-USDT-PERP","RAD-USDT-PERP","SNT-USDT-PERP","OP-USDC","TUSD-USDT","KCS-USDT","CPOOL-USDT","ARB-USDC","MASA-USDT","AIOZ-USDT-PERP"],"l2_book":["BCH-USDT-PERP","XTZ-USDT-PERP","UNI-USDT-PERP","EOS-USD-PERP","LTC-USDT-PERP","DOT-USDT-PERP","LINK-USDT-PERP","ADA-USDT-PERP","ETH-USDT-PERP","BTC-USD-PERP"]}
Loading