
✅ Coinbase Pro (completed in task 12) ✅ Kraken (completed in task 12) ✅ Bybit (completed in this task) ✅ OKX (completed in this task) ✅ Huobi (completed in this task)
321 lines
11 KiB
Python
321 lines
11 KiB
Python
"""
|
|
Unit tests for Bybit exchange connector.
|
|
"""
|
|
|
|
import asyncio
|
|
import pytest
|
|
from unittest.mock import Mock, AsyncMock, patch
|
|
from datetime import datetime, timezone
|
|
|
|
from ..connectors.bybit_connector import BybitConnector
|
|
from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel
|
|
|
|
|
|
class TestBybitConnector:
|
|
"""Test suite for Bybit connector."""
|
|
|
|
@pytest.fixture
|
|
def connector(self):
|
|
"""Create connector instance for testing."""
|
|
return BybitConnector(use_testnet=True)
|
|
|
|
def test_initialization(self, connector):
|
|
"""Test connector initializes correctly."""
|
|
assert connector.exchange_name == "bybit"
|
|
assert connector.use_testnet is True
|
|
assert connector.TESTNET_URL in connector.websocket_url
|
|
assert 'orderbook' in connector.message_handlers
|
|
assert 'publicTrade' in connector.message_handlers
|
|
|
|
def test_symbol_normalization(self, connector):
|
|
"""Test symbol normalization to Bybit format."""
|
|
# Test standard conversions (Bybit uses same format as Binance)
|
|
assert connector.normalize_symbol('BTCUSDT') == 'BTCUSDT'
|
|
assert connector.normalize_symbol('ETHUSDT') == 'ETHUSDT'
|
|
assert connector.normalize_symbol('btcusdt') == 'BTCUSDT'
|
|
|
|
# Test with separators
|
|
assert connector.normalize_symbol('BTC-USDT') == 'BTCUSDT'
|
|
assert connector.normalize_symbol('BTC/USDT') == 'BTCUSDT'
|
|
|
|
def test_message_type_detection(self, connector):
|
|
"""Test message type detection."""
|
|
# Test orderbook message
|
|
orderbook_message = {
|
|
'topic': 'orderbook.50.BTCUSDT',
|
|
'data': {'b': [], 'a': []}
|
|
}
|
|
assert connector._get_message_type(orderbook_message) == 'orderbook'
|
|
|
|
# Test trade message
|
|
trade_message = {
|
|
'topic': 'publicTrade.BTCUSDT',
|
|
'data': []
|
|
}
|
|
assert connector._get_message_type(trade_message) == 'publicTrade'
|
|
|
|
# Test operation message
|
|
op_message = {'op': 'subscribe', 'success': True}
|
|
assert connector._get_message_type(op_message) == 'subscribe'
|
|
|
|
# Test response message
|
|
response_message = {'success': True, 'ret_msg': 'OK'}
|
|
assert connector._get_message_type(response_message) == 'response'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subscription_methods(self, connector):
|
|
"""Test subscription and unsubscription methods."""
|
|
# Mock the _send_message method
|
|
connector._send_message = AsyncMock(return_value=True)
|
|
|
|
# Test order book subscription
|
|
await connector.subscribe_orderbook('BTCUSDT')
|
|
|
|
# Verify subscription was tracked
|
|
assert 'BTCUSDT' in connector.subscriptions
|
|
assert 'orderbook' in connector.subscriptions['BTCUSDT']
|
|
assert 'orderbook.50.BTCUSDT' in connector.subscribed_topics
|
|
|
|
# Verify correct message was sent
|
|
connector._send_message.assert_called()
|
|
call_args = connector._send_message.call_args[0][0]
|
|
assert call_args['op'] == 'subscribe'
|
|
assert 'orderbook.50.BTCUSDT' in call_args['args']
|
|
|
|
# Test trade subscription
|
|
await connector.subscribe_trades('ETHUSDT')
|
|
|
|
assert 'ETHUSDT' in connector.subscriptions
|
|
assert 'trades' in connector.subscriptions['ETHUSDT']
|
|
assert 'publicTrade.ETHUSDT' in connector.subscribed_topics
|
|
|
|
# Test unsubscription
|
|
await connector.unsubscribe_orderbook('BTCUSDT')
|
|
|
|
# Verify unsubscription
|
|
if 'BTCUSDT' in connector.subscriptions:
|
|
assert 'orderbook' not in connector.subscriptions['BTCUSDT']
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_orderbook_snapshot_parsing(self, connector):
|
|
"""Test parsing order book snapshot data."""
|
|
# Mock order book data from Bybit
|
|
mock_data = {
|
|
'u': 12345,
|
|
'ts': 1609459200000,
|
|
'b': [
|
|
['50000.00', '1.5'],
|
|
['49999.00', '2.0']
|
|
],
|
|
'a': [
|
|
['50001.00', '1.2'],
|
|
['50002.00', '0.8']
|
|
]
|
|
}
|
|
|
|
# Parse the data
|
|
orderbook = connector._parse_orderbook_snapshot(mock_data, 'BTCUSDT')
|
|
|
|
# Verify parsing
|
|
assert isinstance(orderbook, OrderBookSnapshot)
|
|
assert orderbook.symbol == 'BTCUSDT'
|
|
assert orderbook.exchange == 'bybit'
|
|
assert orderbook.sequence_id == 12345
|
|
|
|
# Verify bids
|
|
assert len(orderbook.bids) == 2
|
|
assert orderbook.bids[0].price == 50000.00
|
|
assert orderbook.bids[0].size == 1.5
|
|
|
|
# Verify asks
|
|
assert len(orderbook.asks) == 2
|
|
assert orderbook.asks[0].price == 50001.00
|
|
assert orderbook.asks[0].size == 1.2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_orderbook_update_handling(self, connector):
|
|
"""Test handling order book update messages."""
|
|
# Mock callback
|
|
callback_called = False
|
|
received_data = None
|
|
|
|
def mock_callback(data):
|
|
nonlocal callback_called, received_data
|
|
callback_called = True
|
|
received_data = data
|
|
|
|
connector.add_data_callback(mock_callback)
|
|
|
|
# Mock Bybit orderbook update message
|
|
update_message = {
|
|
'topic': 'orderbook.50.BTCUSDT',
|
|
'ts': 1609459200000,
|
|
'data': {
|
|
'u': 12345,
|
|
'b': [['50000.00', '1.5']],
|
|
'a': [['50001.00', '1.2']]
|
|
}
|
|
}
|
|
|
|
# Handle the message
|
|
await connector._handle_orderbook_update(update_message)
|
|
|
|
# Verify callback was called
|
|
assert callback_called
|
|
assert isinstance(received_data, OrderBookSnapshot)
|
|
assert received_data.symbol == 'BTCUSDT'
|
|
assert received_data.exchange == 'bybit'
|
|
assert received_data.sequence_id == 12345
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_handling(self, connector):
|
|
"""Test handling trade messages."""
|
|
# Mock callback
|
|
callback_called = False
|
|
received_trades = []
|
|
|
|
def mock_callback(data):
|
|
nonlocal callback_called
|
|
callback_called = True
|
|
received_trades.append(data)
|
|
|
|
connector.add_data_callback(mock_callback)
|
|
|
|
# Mock Bybit trade message
|
|
trade_message = {
|
|
'topic': 'publicTrade.BTCUSDT',
|
|
'ts': 1609459200000,
|
|
'data': [
|
|
{
|
|
'T': 1609459200000,
|
|
'p': '50000.50',
|
|
'v': '0.1',
|
|
'S': 'Buy',
|
|
'i': '12345'
|
|
}
|
|
]
|
|
}
|
|
|
|
# Handle the message
|
|
await connector._handle_trade_update(trade_message)
|
|
|
|
# Verify callback was called
|
|
assert callback_called
|
|
assert len(received_trades) == 1
|
|
|
|
trade = received_trades[0]
|
|
assert isinstance(trade, TradeEvent)
|
|
assert trade.symbol == 'BTCUSDT'
|
|
assert trade.exchange == 'bybit'
|
|
assert trade.price == 50000.50
|
|
assert trade.size == 0.1
|
|
assert trade.side == 'buy'
|
|
assert trade.trade_id == '12345'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_symbols(self, connector):
|
|
"""Test getting available symbols."""
|
|
# Mock HTTP response
|
|
mock_response_data = {
|
|
'retCode': 0,
|
|
'result': {
|
|
'list': [
|
|
{
|
|
'symbol': 'BTCUSDT',
|
|
'status': 'Trading'
|
|
},
|
|
{
|
|
'symbol': 'ETHUSDT',
|
|
'status': 'Trading'
|
|
},
|
|
{
|
|
'symbol': 'DISABLEDUSDT',
|
|
'status': 'Closed'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
with patch('aiohttp.ClientSession.get') as mock_get:
|
|
mock_response = AsyncMock()
|
|
mock_response.status = 200
|
|
mock_response.json = AsyncMock(return_value=mock_response_data)
|
|
mock_get.return_value.__aenter__.return_value = mock_response
|
|
|
|
symbols = await connector.get_symbols()
|
|
|
|
# Should only return trading symbols
|
|
assert 'BTCUSDT' in symbols
|
|
assert 'ETHUSDT' in symbols
|
|
assert 'DISABLEDUSDT' not in symbols
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_orderbook_snapshot(self, connector):
|
|
"""Test getting order book snapshot from REST API."""
|
|
# Mock HTTP response
|
|
mock_orderbook = {
|
|
'retCode': 0,
|
|
'result': {
|
|
'ts': 1609459200000,
|
|
'u': 12345,
|
|
'b': [['50000.00', '1.5']],
|
|
'a': [['50001.00', '1.2']]
|
|
}
|
|
}
|
|
|
|
with patch('aiohttp.ClientSession.get') as mock_get:
|
|
mock_response = AsyncMock()
|
|
mock_response.status = 200
|
|
mock_response.json = AsyncMock(return_value=mock_orderbook)
|
|
mock_get.return_value.__aenter__.return_value = mock_response
|
|
|
|
orderbook = await connector.get_orderbook_snapshot('BTCUSDT')
|
|
|
|
assert isinstance(orderbook, OrderBookSnapshot)
|
|
assert orderbook.symbol == 'BTCUSDT'
|
|
assert orderbook.exchange == 'bybit'
|
|
assert len(orderbook.bids) == 1
|
|
assert len(orderbook.asks) == 1
|
|
|
|
def test_statistics(self, connector):
|
|
"""Test getting connector statistics."""
|
|
# Add some test data
|
|
connector.subscribed_topics.add('orderbook.50.BTCUSDT')
|
|
|
|
stats = connector.get_bybit_stats()
|
|
|
|
assert stats['exchange'] == 'bybit'
|
|
assert 'orderbook.50.BTCUSDT' in stats['subscribed_topics']
|
|
assert stats['use_testnet'] is True
|
|
assert 'authenticated' in stats
|
|
|
|
|
|
async def test_bybit_integration():
|
|
"""Integration test for Bybit connector."""
|
|
connector = BybitConnector(use_testnet=True)
|
|
|
|
try:
|
|
# Test basic functionality
|
|
assert connector.exchange_name == "bybit"
|
|
|
|
# Test symbol normalization
|
|
assert connector.normalize_symbol('BTCUSDT') == 'BTCUSDT'
|
|
assert connector.normalize_symbol('btc-usdt') == 'BTCUSDT'
|
|
|
|
# Test message type detection
|
|
test_message = {'topic': 'orderbook.50.BTCUSDT', 'data': {}}
|
|
assert connector._get_message_type(test_message) == 'orderbook'
|
|
|
|
print("✓ Bybit connector integration test passed")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"✗ Bybit connector integration test failed: {e}")
|
|
return False
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run integration test
|
|
success = asyncio.run(test_bybit_integration())
|
|
if not success:
|
|
exit(1) |