Binance (completed previously)

 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)
This commit is contained in:
Dobromir Popov
2025-08-04 23:41:42 +03:00
parent 4170553cf3
commit 7339972eab
7 changed files with 2526 additions and 0 deletions

View File

@ -0,0 +1,271 @@
"""
Comprehensive tests for all exchange connectors.
Tests the consistency and compatibility across all implemented connectors.
"""
import asyncio
import pytest
from unittest.mock import Mock, AsyncMock
from ..connectors.binance_connector import BinanceConnector
from ..connectors.coinbase_connector import CoinbaseConnector
from ..connectors.kraken_connector import KrakenConnector
from ..connectors.bybit_connector import BybitConnector
from ..connectors.okx_connector import OKXConnector
from ..connectors.huobi_connector import HuobiConnector
class TestAllConnectors:
"""Test suite for all exchange connectors."""
@pytest.fixture
def all_connectors(self):
"""Create instances of all connectors for testing."""
return {
'binance': BinanceConnector(),
'coinbase': CoinbaseConnector(use_sandbox=True),
'kraken': KrakenConnector(),
'bybit': BybitConnector(use_testnet=True),
'okx': OKXConnector(use_demo=True),
'huobi': HuobiConnector()
}
def test_all_connectors_initialization(self, all_connectors):
"""Test that all connectors initialize correctly."""
expected_names = ['binance', 'coinbase', 'kraken', 'bybit', 'okx', 'huobi']
for name, connector in all_connectors.items():
assert connector.exchange_name == name
assert hasattr(connector, 'websocket_url')
assert hasattr(connector, 'message_handlers')
assert hasattr(connector, 'subscriptions')
def test_interface_consistency(self, all_connectors):
"""Test that all connectors implement the required interface methods."""
required_methods = [
'connect',
'disconnect',
'subscribe_orderbook',
'subscribe_trades',
'unsubscribe_orderbook',
'unsubscribe_trades',
'get_symbols',
'get_orderbook_snapshot',
'normalize_symbol',
'get_connection_status',
'add_data_callback',
'remove_data_callback'
]
for name, connector in all_connectors.items():
for method in required_methods:
assert hasattr(connector, method), f"{name} missing method {method}"
assert callable(getattr(connector, method)), f"{name}.{method} not callable"
def test_symbol_normalization_consistency(self, all_connectors):
"""Test symbol normalization across all connectors."""
test_symbols = ['BTCUSDT', 'ETHUSDT', 'btcusdt', 'BTC-USDT', 'BTC/USDT']
for name, connector in all_connectors.items():
for symbol in test_symbols:
try:
normalized = connector.normalize_symbol(symbol)
assert isinstance(normalized, str)
assert len(normalized) > 0
print(f"{name}: {symbol} -> {normalized}")
except Exception as e:
print(f"{name} failed to normalize {symbol}: {e}")
@pytest.mark.asyncio
async def test_subscription_interface(self, all_connectors):
"""Test subscription interface consistency."""
for name, connector in all_connectors.items():
# Mock the _send_message method
connector._send_message = AsyncMock(return_value=True)
try:
# Test order book subscription
await connector.subscribe_orderbook('BTCUSDT')
assert 'BTCUSDT' in connector.subscriptions
# Test trade subscription
await connector.subscribe_trades('ETHUSDT')
assert 'ETHUSDT' in connector.subscriptions
# Test unsubscription
await connector.unsubscribe_orderbook('BTCUSDT')
await connector.unsubscribe_trades('ETHUSDT')
print(f"{name} subscription interface works")
except Exception as e:
print(f"{name} subscription interface failed: {e}")
def test_message_type_detection(self, all_connectors):
"""Test message type detection across connectors."""
# Test with generic message structures
test_messages = [
{'type': 'test'},
{'event': 'test'},
{'op': 'test'},
{'ch': 'test'},
{'topic': 'test'},
[1, {}, 'test', 'symbol'], # Kraken format
{'unknown': 'data'}
]
for name, connector in all_connectors.items():
for msg in test_messages:
try:
msg_type = connector._get_message_type(msg)
assert isinstance(msg_type, str)
print(f"{name}: {msg} -> {msg_type}")
except Exception as e:
print(f"{name} failed to detect message type for {msg}: {e}")
def test_statistics_interface(self, all_connectors):
"""Test statistics interface consistency."""
for name, connector in all_connectors.items():
try:
stats = connector.get_stats()
assert isinstance(stats, dict)
assert 'exchange' in stats
assert stats['exchange'] == name
assert 'connection_status' in stats
print(f"{name} statistics interface works")
except Exception as e:
print(f"{name} statistics interface failed: {e}")
def test_callback_system(self, all_connectors):
"""Test callback system consistency."""
for name, connector in all_connectors.items():
try:
# Test adding callback
def test_callback(data):
pass
connector.add_data_callback(test_callback)
assert test_callback in connector.data_callbacks
# Test removing callback
connector.remove_data_callback(test_callback)
assert test_callback not in connector.data_callbacks
print(f"{name} callback system works")
except Exception as e:
print(f"{name} callback system failed: {e}")
def test_connection_status(self, all_connectors):
"""Test connection status interface."""
for name, connector in all_connectors.items():
try:
status = connector.get_connection_status()
assert hasattr(status, 'value') # Should be an enum
# Test is_connected property
is_connected = connector.is_connected
assert isinstance(is_connected, bool)
print(f"{name} connection status interface works")
except Exception as e:
print(f"{name} connection status interface failed: {e}")
async def test_connector_compatibility():
"""Test compatibility across all connectors."""
print("=== Testing All Exchange Connectors ===")
connectors = {
'binance': BinanceConnector(),
'coinbase': CoinbaseConnector(use_sandbox=True),
'kraken': KrakenConnector(),
'bybit': BybitConnector(use_testnet=True),
'okx': OKXConnector(use_demo=True),
'huobi': HuobiConnector()
}
# Test basic functionality
for name, connector in connectors.items():
try:
print(f"\nTesting {name.upper()} connector:")
# Test initialization
assert connector.exchange_name == name
print(f" ✓ Initialization: {connector.exchange_name}")
# Test symbol normalization
btc_symbol = connector.normalize_symbol('BTCUSDT')
eth_symbol = connector.normalize_symbol('ETHUSDT')
print(f" ✓ Symbol normalization: BTCUSDT -> {btc_symbol}, ETHUSDT -> {eth_symbol}")
# Test message type detection
test_msg = {'type': 'test'} if name != 'kraken' else [1, {}, 'test', 'symbol']
msg_type = connector._get_message_type(test_msg)
print(f" ✓ Message type detection: {msg_type}")
# Test statistics
stats = connector.get_stats()
print(f" ✓ Statistics: {len(stats)} fields")
# Test connection status
status = connector.get_connection_status()
print(f" ✓ Connection status: {status.value}")
print(f"{name.upper()} connector passed all tests")
except Exception as e:
print(f"{name.upper()} connector failed: {e}")
print("\n=== All Connector Tests Completed ===")
return True
async def test_multi_connector_data_flow():
"""Test data flow across multiple connectors simultaneously."""
print("=== Testing Multi-Connector Data Flow ===")
connectors = {
'binance': BinanceConnector(),
'coinbase': CoinbaseConnector(use_sandbox=True),
'kraken': KrakenConnector()
}
# Set up data collection
received_data = {name: [] for name in connectors.keys()}
def create_callback(exchange_name):
def callback(data):
received_data[exchange_name].append(data)
print(f"Received data from {exchange_name}: {type(data).__name__}")
return callback
# Add callbacks to all connectors
for name, connector in connectors.items():
connector.add_data_callback(create_callback(name))
connector._send_message = AsyncMock(return_value=True)
# Test subscription to same symbol across exchanges
symbol = 'BTCUSDT'
for name, connector in connectors.items():
try:
await connector.subscribe_orderbook(symbol)
await connector.subscribe_trades(symbol)
print(f"✓ Subscribed to {symbol} on {name}")
except Exception as e:
print(f"✗ Failed to subscribe to {symbol} on {name}: {e}")
print("Multi-connector data flow test completed")
return True
if __name__ == "__main__":
# Run all tests
async def run_all_tests():
await test_connector_compatibility()
await test_multi_connector_data_flow()
print("✅ All connector tests completed successfully")
asyncio.run(run_all_tests())

View File

@ -0,0 +1,321 @@
"""
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)