Skip to content

Commit ca663a6

Browse files
committed
L1BOOK support BYBIT
1 parent 71a985c commit ca663a6

8 files changed

Lines changed: 488 additions & 51 deletions

File tree

cryptofeed/exchanges/bybit.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@
1717
from yapic import json
1818

1919
from cryptofeed.connection import AsyncConnection, RestEndpoint, Routes, WebsocketEndpoint
20-
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
20+
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
2121
from cryptofeed.feed import Feed
22-
from cryptofeed.types import OrderBook, Trade, Index, OpenInterest, Funding, OrderInfo, Fill, Candle, Liquidation, Ticker
22+
from cryptofeed.types import OrderBook, Trade, Index, OpenInterest, Funding, OrderInfo, Fill, Candle, Liquidation, Ticker, L1Book
2323

2424
LOG = logging.getLogger('feedhandler')
2525

2626

2727
class Bybit(Feed):
2828
id = BYBIT
2929
websocket_channels = {
30+
L1_BOOK: '',
3031
L2_BOOK: '', # Assigned in self.subscribe
3132
TRADES: 'publicTrade',
3233
FILLS: 'execution',
@@ -39,8 +40,8 @@ class Bybit(Feed):
3940
TICKER: 'tickers'
4041
}
4142
websocket_endpoints = [
42-
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}),
43-
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}),
43+
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}),
44+
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}),
4445
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}),
4546
]
4647
rest_endpoints = [
@@ -231,6 +232,8 @@ async def message_handler(self, msg: str, conn, timestamp: float):
231232
LOG.error("%s: Error from exchange %s", conn.uuid, msg)
232233
elif msg["topic"].startswith('publicTrade'):
233234
await self._trade(msg, timestamp, market)
235+
elif msg["topic"].startswith('orderbook.1'):
236+
await self._top_of_book(msg, timestamp, market)
234237
elif msg["topic"].startswith('orderbook'):
235238
await self._book(msg, timestamp, market)
236239
elif msg['topic'].startswith('kline'):
@@ -279,6 +282,13 @@ async def subscribe(self, connection: AsyncConnection):
279282
PERPETUAL: "orderbook.200",
280283
}
281284
sub = [f"{l2_book_channel[sym.type]}.{pair}"]
285+
elif self.exchange_channel_to_std(chan) == L1_BOOK:
286+
l1_book_channel = {
287+
SPOT: "orderbook.1",
288+
FUTURES: "orderbook.1",
289+
PERPETUAL: "orderbook.1",
290+
}
291+
sub = [f"{l1_book_channel[sym.type]}.{pair}"]
282292
else:
283293
sub = [f"{chan}.{pair}"]
284294

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

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

417+
async def _top_of_book(self, msg: dict, timestamp: float, market: str):
418+
'''
419+
{
420+
'topic': 'orderbook.1.BTCUSDT',
421+
'type': 'snapshot',
422+
'ts': 1749822878982,
423+
'data': {
424+
's': 'BTCUSDT',
425+
'b': [['104714.40', '0.727']],
426+
'a': [['104714.50', '16.541']],
427+
'u': 58267067,
428+
'seq': 416909700197
429+
},
430+
'cts': 1749822878980
431+
}
432+
'''
433+
pair = msg['topic'].split('.')[-1]
434+
update_type = msg['type']
435+
data = msg['data']
436+
437+
if market == 'spot':
438+
pair = self.convert_to_spot_name(self, data['s'])
439+
if not pair:
440+
return
441+
442+
symbol = self.exchange_symbol_to_std_symbol(pair)
443+
444+
if update_type == 'snapshot':
445+
l1 = L1Book(
446+
self.id,
447+
symbol,
448+
Decimal(data['b'][0][0]) if 'b' in data else Decimal(0),
449+
Decimal(data['b'][0][1]) if 'b' in data else Decimal(0),
450+
Decimal(data['a'][0][0]) if 'a' in data else Decimal(0),
451+
Decimal(data['a'][0][1]) if 'a' in data else Decimal(0),
452+
int(msg['ts']),
453+
raw=msg
454+
)
455+
await self.callback(L1_BOOK, l1, timestamp)
456+
407457
async def _ticker_open_interest_funding_index(self, msg: dict, timestamp: float, conn: AsyncConnection):
408458
'''
409459
{

cryptofeed/feed.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from cryptofeed.callback import Callback
1515
from cryptofeed.connection import AsyncConnection, HTTPAsyncConn, WSAsyncConn
1616
from cryptofeed.connection_handler import ConnectionHandler
17-
from cryptofeed.defines import BALANCES, CANDLES, FUNDING, INDEX, L2_BOOK, L3_BOOK, LIQUIDATIONS, OPEN_INTEREST, ORDER_INFO, POSITIONS, TICKER, TRADES, FILLS
17+
from cryptofeed.defines import BALANCES, CANDLES, FUNDING, INDEX, L1_BOOK, L2_BOOK, L3_BOOK, LIQUIDATIONS, OPEN_INTEREST, ORDER_INFO, POSITIONS, TICKER, TRADES, FILLS
1818
from cryptofeed.exceptions import BidAskOverlapping
1919
from cryptofeed.exchange import Exchange
2020
from cryptofeed.types import OrderBook
@@ -125,6 +125,7 @@ def __init__(self, candle_interval='1m', candle_closed_only=True, timeout=120, t
125125
self._l2_book = {}
126126
self.callbacks = {FUNDING: Callback(None),
127127
INDEX: Callback(None),
128+
L1_BOOK: Callback(None),
128129
L2_BOOK: Callback(None),
129130
L3_BOOK: Callback(None),
130131
LIQUIDATIONS: Callback(None),

cryptofeed/types.pyx

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,60 @@ cdef class Ticker:
132132
def __hash__(self):
133133
return hash(self.__repr__())
134134

135+
cdef class L1Book:
136+
cdef readonly str exchange
137+
cdef readonly str symbol
138+
cdef readonly object bid
139+
cdef readonly object bid_size
140+
cdef readonly object ask
141+
cdef readonly object ask_size
142+
cdef readonly object timestamp
143+
cdef readonly object raw
144+
145+
def __init__(self, exchange, symbol, bid, bid_size, ask, ask_size, timestamp, raw=None):
146+
assert isinstance(bid, Decimal)
147+
assert isinstance(bid_size, Decimal)
148+
assert isinstance(ask, Decimal)
149+
assert isinstance(ask_size, Decimal)
150+
assert timestamp is None or isinstance(timestamp, float)
151+
152+
self.exchange = exchange
153+
self.symbol = symbol
154+
self.bid = bid
155+
self.bid_size = bid_size
156+
self.ask = ask
157+
self.ask_size = ask_size
158+
self.timestamp = timestamp
159+
self.raw = raw
160+
161+
@staticmethod
162+
def from_dict(data: dict) -> L1Book:
163+
return L1Book(
164+
data['exchange'],
165+
data['symbol'],
166+
Decimal(data['bid']),
167+
Decimal(data['bid_size']),
168+
Decimal(data['ask']),
169+
Decimal(data['ask_size']),
170+
data['timestamp']
171+
)
172+
173+
cpdef dict to_dict(self, numeric_type=None, none_to=False):
174+
if numeric_type is None:
175+
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}
176+
else:
177+
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}
178+
return data if not none_to else convert_none_values(data, none_to)
179+
180+
def __repr__(self):
181+
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}"
182+
183+
def __eq__(self, cmp):
184+
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
185+
186+
def __hash__(self):
187+
return hash(self.__repr__())
188+
135189

136190
cdef class Liquidation:
137191
cdef readonly str exchange
@@ -623,49 +677,6 @@ cdef class Balance:
623677
def __hash__(self):
624678
return hash(self.__repr__())
625679

626-
627-
cdef class L1Book:
628-
cdef readonly str exchange
629-
cdef readonly str symbol
630-
cdef readonly object bid_price
631-
cdef readonly object bid_size
632-
cdef readonly object ask_price
633-
cdef readonly object ask_size
634-
cdef readonly double timestamp
635-
cdef readonly dict raw
636-
637-
def __init__(self, exchange, symbol, bid_price, bid_size, ask_price, ask_size, timestamp, raw=None):
638-
assert isinstance(bid_price, Decimal)
639-
assert isinstance(bid_size, Decimal)
640-
assert isinstance(ask_price, Decimal)
641-
assert isinstance(ask_size, Decimal)
642-
643-
self.exchange = exchange
644-
self.symbol = symbol
645-
self.bid_price = bid_price
646-
self.bid_size = bid_size
647-
self.ask_price = ask_price
648-
self.ask_size = ask_size
649-
self.timestamp = timestamp
650-
self.raw = raw
651-
652-
cpdef dict to_dict(self, numeric_type=None, none_to=False):
653-
if numeric_type is None:
654-
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}
655-
else:
656-
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}
657-
return data if not none_to else convert_none_values(data, none_to)
658-
659-
def __repr__(self):
660-
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}'
661-
662-
def __eq__(self, cmp):
663-
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
664-
665-
def __hash__(self):
666-
return hash(self.__repr__())
667-
668-
669680
cdef class Transaction:
670681
cdef readonly str exchange
671682
cdef readonly str currency

sample_data/BYBIT.0

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
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"}
2-
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"]}
2+
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"]}

0 commit comments

Comments
 (0)