✅ Binance (completed previously)
✅ Coinbase Pro (completed in this task) ✅ Kraken (completed in this task)
This commit is contained in:
398
COBY/tests/test_kraken_connector.py
Normal file
398
COBY/tests/test_kraken_connector.py
Normal file
@ -0,0 +1,398 @@
|
||||
"""
|
||||
Unit tests for Kraken exchange connector.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ..connectors.kraken_connector import KrakenConnector
|
||||
from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel
|
||||
|
||||
|
||||
class TestKrakenConnector:
|
||||
"""Test suite for Kraken connector."""
|
||||
|
||||
@pytest.fixture
|
||||
def connector(self):
|
||||
"""Create connector instance for testing."""
|
||||
return KrakenConnector()
|
||||
|
||||
def test_initialization(self, connector):
|
||||
"""Test connector initializes correctly."""
|
||||
assert connector.exchange_name == "kraken"
|
||||
assert connector.WEBSOCKET_URL in connector.websocket_url
|
||||
assert 'book-25' in connector.message_handlers
|
||||
assert 'trade' in connector.message_handlers
|
||||
assert connector.system_status == 'unknown'
|
||||
|
||||
def test_symbol_normalization(self, connector):
|
||||
"""Test symbol normalization to Kraken format."""
|
||||
# Test standard conversions
|
||||
assert connector.normalize_symbol('BTCUSDT') == 'XBT/USD'
|
||||
assert connector.normalize_symbol('ETHUSDT') == 'ETH/USD'
|
||||
assert connector.normalize_symbol('ADAUSDT') == 'ADA/USD'
|
||||
|
||||
# Test generic conversion
|
||||
assert connector.normalize_symbol('LINKUSDT') == 'LINK/USD'
|
||||
|
||||
# Test already correct format
|
||||
assert connector.normalize_symbol('XBT/USD') == 'XBT/USD'
|
||||
|
||||
def test_symbol_denormalization(self, connector):
|
||||
"""Test converting Kraken format back to standard."""
|
||||
assert connector._denormalize_symbol('XBT/USD') == 'BTCUSDT'
|
||||
assert connector._denormalize_symbol('ETH/USD') == 'ETHUSDT'
|
||||
assert connector._denormalize_symbol('ADA/USD') == 'ADAUSDT'
|
||||
|
||||
# Test other quote currencies
|
||||
assert connector._denormalize_symbol('BTC/EUR') == 'BTCEUR'
|
||||
|
||||
def test_message_type_detection(self, connector):
|
||||
"""Test message type detection."""
|
||||
# Test order book message (array format)
|
||||
book_message = [123, {'b': [['50000', '1.5']]}, 'book-25', 'XBT/USD']
|
||||
assert connector._get_message_type(book_message) == 'book-25'
|
||||
|
||||
# Test trade message (array format)
|
||||
trade_message = [456, [['50000', '0.1', 1609459200, 'b', 'm', '']], 'trade', 'XBT/USD']
|
||||
assert connector._get_message_type(trade_message) == 'trade'
|
||||
|
||||
# Test status message (object format)
|
||||
status_message = {'event': 'systemStatus', 'status': 'online'}
|
||||
assert connector._get_message_type(status_message) == 'systemStatus'
|
||||
|
||||
# Test subscription message
|
||||
sub_message = {'event': 'subscriptionStatus', 'status': 'subscribed'}
|
||||
assert connector._get_message_type(sub_message) == 'subscriptionStatus'
|
||||
|
||||
# Test unknown message
|
||||
unknown_message = {'data': 'something'}
|
||||
assert connector._get_message_type(unknown_message) == 'unknown'
|
||||
|
||||
@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']
|
||||
|
||||
# Verify correct message was sent
|
||||
connector._send_message.assert_called()
|
||||
call_args = connector._send_message.call_args[0][0]
|
||||
assert call_args['event'] == 'subscribe'
|
||||
assert 'XBT/USD' in call_args['pair']
|
||||
assert call_args['subscription']['name'] == 'book'
|
||||
|
||||
# Test trade subscription
|
||||
await connector.subscribe_trades('ETHUSDT')
|
||||
|
||||
assert 'ETHUSDT' in connector.subscriptions
|
||||
assert 'trades' in connector.subscriptions['ETHUSDT']
|
||||
|
||||
# Test unsubscription
|
||||
await connector.unsubscribe_orderbook('BTCUSDT')
|
||||
|
||||
# Verify unsubscription message
|
||||
call_args = connector._send_message.call_args[0][0]
|
||||
assert call_args['event'] == 'unsubscribe'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orderbook_snapshot_parsing(self, connector):
|
||||
"""Test parsing order book snapshot data."""
|
||||
# Mock order book data from Kraken
|
||||
mock_data = {
|
||||
'bids': [
|
||||
['50000.00', '1.5', 1609459200],
|
||||
['49999.00', '2.0', 1609459201]
|
||||
],
|
||||
'asks': [
|
||||
['50001.00', '1.2', 1609459200],
|
||||
['50002.00', '0.8', 1609459201]
|
||||
]
|
||||
}
|
||||
|
||||
# Parse the data
|
||||
orderbook = connector._parse_orderbook_snapshot(mock_data, 'BTCUSDT')
|
||||
|
||||
# Verify parsing
|
||||
assert isinstance(orderbook, OrderBookSnapshot)
|
||||
assert orderbook.symbol == 'BTCUSDT'
|
||||
assert orderbook.exchange == 'kraken'
|
||||
|
||||
# Verify bids
|
||||
assert len(orderbook.bids) == 2
|
||||
assert orderbook.bids[0].price == 50000.00
|
||||
assert orderbook.bids[0].size == 1.5
|
||||
assert orderbook.bids[1].price == 49999.00
|
||||
assert orderbook.bids[1].size == 2.0
|
||||
|
||||
# Verify asks
|
||||
assert len(orderbook.asks) == 2
|
||||
assert orderbook.asks[0].price == 50001.00
|
||||
assert orderbook.asks[0].size == 1.2
|
||||
assert orderbook.asks[1].price == 50002.00
|
||||
assert orderbook.asks[1].size == 0.8
|
||||
|
||||
@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 Kraken order book update message
|
||||
update_message = [
|
||||
123, # channel ID
|
||||
{
|
||||
'b': [['50000.00', '1.5', '1609459200.123456']],
|
||||
'a': [['50001.00', '1.2', '1609459200.123456']]
|
||||
},
|
||||
'book-25',
|
||||
'XBT/USD'
|
||||
]
|
||||
|
||||
# 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 == 'kraken'
|
||||
|
||||
# Verify channel mapping was stored
|
||||
assert 123 in connector.channel_map
|
||||
assert connector.channel_map[123] == ('book-25', 'BTCUSDT')
|
||||
|
||||
# Verify bids and asks
|
||||
assert len(received_data.bids) == 1
|
||||
assert received_data.bids[0].price == 50000.00
|
||||
assert received_data.bids[0].size == 1.5
|
||||
|
||||
assert len(received_data.asks) == 1
|
||||
assert received_data.asks[0].price == 50001.00
|
||||
assert received_data.asks[0].size == 1.2
|
||||
|
||||
@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 Kraken trade message (array of trades)
|
||||
trade_message = [
|
||||
456, # channel ID
|
||||
[
|
||||
['50000.50', '0.1', 1609459200.123456, 'b', 'm', ''],
|
||||
['50001.00', '0.05', 1609459201.123456, 's', 'l', '']
|
||||
],
|
||||
'trade',
|
||||
'XBT/USD'
|
||||
]
|
||||
|
||||
# Handle the message
|
||||
await connector._handle_trade_update(trade_message)
|
||||
|
||||
# Verify callback was called
|
||||
assert callback_called
|
||||
assert len(received_trades) == 2
|
||||
|
||||
# Verify first trade (buy)
|
||||
trade1 = received_trades[0]
|
||||
assert isinstance(trade1, TradeEvent)
|
||||
assert trade1.symbol == 'BTCUSDT'
|
||||
assert trade1.exchange == 'kraken'
|
||||
assert trade1.price == 50000.50
|
||||
assert trade1.size == 0.1
|
||||
assert trade1.side == 'buy'
|
||||
|
||||
# Verify second trade (sell)
|
||||
trade2 = received_trades[1]
|
||||
assert trade2.price == 50001.00
|
||||
assert trade2.size == 0.05
|
||||
assert trade2.side == 'sell'
|
||||
|
||||
# Verify channel mapping was stored
|
||||
assert 456 in connector.channel_map
|
||||
assert connector.channel_map[456] == ('trade', 'BTCUSDT')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_status_handling(self, connector):
|
||||
"""Test handling system status messages."""
|
||||
# Mock system status message
|
||||
status_message = {
|
||||
'event': 'systemStatus',
|
||||
'status': 'online',
|
||||
'version': '1.0.0'
|
||||
}
|
||||
|
||||
# Handle the message
|
||||
await connector._handle_system_status(status_message)
|
||||
|
||||
# Verify status was updated
|
||||
assert connector.system_status == 'online'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscription_status_handling(self, connector):
|
||||
"""Test handling subscription status messages."""
|
||||
# Mock subscription status message
|
||||
sub_message = {
|
||||
'event': 'subscriptionStatus',
|
||||
'status': 'subscribed',
|
||||
'channelName': 'book-25',
|
||||
'channelID': 123,
|
||||
'pair': 'XBT/USD',
|
||||
'subscription': {'name': 'book', 'depth': 25}
|
||||
}
|
||||
|
||||
# Handle the message
|
||||
await connector._handle_subscription_status(sub_message)
|
||||
|
||||
# Verify channel mapping was stored
|
||||
assert 123 in connector.channel_map
|
||||
assert connector.channel_map[123] == ('book-25', 'BTCUSDT')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_symbols(self, connector):
|
||||
"""Test getting available symbols."""
|
||||
# Mock HTTP response
|
||||
mock_response_data = {
|
||||
'error': [],
|
||||
'result': {
|
||||
'XBTUSD': {
|
||||
'wsname': 'XBT/USD'
|
||||
},
|
||||
'ETHUSD': {
|
||||
'wsname': 'ETH/USD'
|
||||
},
|
||||
'XBTUSD.d': { # Dark pool - should be filtered out
|
||||
'wsname': 'XBT/USD.d'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 non-dark pool symbols
|
||||
assert 'BTCUSDT' in symbols
|
||||
assert 'ETHUSDT' in symbols
|
||||
# Dark pool should be filtered out
|
||||
assert len([s for s in symbols if '.d' in s]) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_orderbook_snapshot(self, connector):
|
||||
"""Test getting order book snapshot from REST API."""
|
||||
# Mock HTTP response
|
||||
mock_orderbook = {
|
||||
'error': [],
|
||||
'result': {
|
||||
'XBTUSD': {
|
||||
'bids': [['50000.00', '1.5', 1609459200]],
|
||||
'asks': [['50001.00', '1.2', 1609459200]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 == 'kraken'
|
||||
assert len(orderbook.bids) == 1
|
||||
assert len(orderbook.asks) == 1
|
||||
|
||||
def test_authentication_token(self, connector):
|
||||
"""Test authentication token generation."""
|
||||
# Set up credentials
|
||||
connector.api_key = 'test_key'
|
||||
connector.api_secret = 'dGVzdF9zZWNyZXQ=' # base64 encoded
|
||||
|
||||
# Generate token
|
||||
token = connector._get_auth_token()
|
||||
|
||||
# Should return a token (simplified implementation)
|
||||
assert isinstance(token, str)
|
||||
assert len(token) > 0
|
||||
|
||||
def test_statistics(self, connector):
|
||||
"""Test getting connector statistics."""
|
||||
# Add some test data
|
||||
connector.system_status = 'online'
|
||||
connector.channel_map[123] = ('book-25', 'BTCUSDT')
|
||||
|
||||
stats = connector.get_kraken_stats()
|
||||
|
||||
assert stats['exchange'] == 'kraken'
|
||||
assert stats['system_status'] == 'online'
|
||||
assert stats['channel_mappings'] == 1
|
||||
assert 'authenticated' in stats
|
||||
|
||||
|
||||
async def test_kraken_integration():
|
||||
"""Integration test for Kraken connector."""
|
||||
connector = KrakenConnector()
|
||||
|
||||
try:
|
||||
# Test basic functionality
|
||||
assert connector.exchange_name == "kraken"
|
||||
|
||||
# Test symbol normalization
|
||||
assert connector.normalize_symbol('BTCUSDT') == 'XBT/USD'
|
||||
assert connector._denormalize_symbol('XBT/USD') == 'BTCUSDT'
|
||||
|
||||
# Test message type detection
|
||||
test_message = [123, {}, 'book-25', 'XBT/USD']
|
||||
assert connector._get_message_type(test_message) == 'book-25'
|
||||
|
||||
# Test status message
|
||||
status_message = {'event': 'systemStatus', 'status': 'online'}
|
||||
assert connector._get_message_type(status_message) == 'systemStatus'
|
||||
|
||||
print("✓ Kraken connector integration test passed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Kraken connector integration test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run integration test
|
||||
success = asyncio.run(test_kraken_integration())
|
||||
if not success:
|
||||
exit(1)
|
Reference in New Issue
Block a user