brighter-trading/tests/test_broker_keys.py

272 lines
9.9 KiB
Python

"""
Tests for broker_keys module.
Tests the broker key parsing, building, and backward compatibility.
"""
import pytest
from brokers.broker_keys import (
parse_broker_key,
build_broker_key,
normalize_broker_key,
get_available_broker_keys,
get_exchange_connection_key,
is_margin_supported,
ParsedBrokerKey,
BROKER_KEY_ALIASES,
)
class TestNormalizeBrokerKey:
"""Test backward compatibility aliases."""
def test_paper_alias(self):
assert normalize_broker_key('paper') == 'paper_spot'
def test_kucoin_production_alias(self):
assert normalize_broker_key('kucoin_production') == 'kucoin_spot_production'
def test_kucoin_testnet_alias(self):
assert normalize_broker_key('kucoin_testnet') == 'kucoin_spot_testnet'
def test_binance_production_alias(self):
assert normalize_broker_key('binance_production') == 'binance_spot_production'
def test_new_format_unchanged(self):
assert normalize_broker_key('paper_spot') == 'paper_spot'
assert normalize_broker_key('kucoin_spot_production') == 'kucoin_spot_production'
assert normalize_broker_key('paper_margin_isolated') == 'paper_margin_isolated'
class TestParseBrokerKey:
"""Test broker key parsing."""
def test_parse_paper_spot(self):
parsed = parse_broker_key('paper_spot')
assert parsed.exchange == 'paper'
assert parsed.product == 'spot'
assert parsed.margin_mode is None
assert parsed.mode is None
assert parsed.is_paper
assert not parsed.is_live
assert not parsed.is_margin
def test_parse_paper_margin_isolated(self):
parsed = parse_broker_key('paper_margin_isolated')
assert parsed.exchange == 'paper'
assert parsed.product == 'margin'
assert parsed.margin_mode == 'isolated'
assert parsed.mode is None
assert parsed.is_paper
assert parsed.is_margin
def test_parse_kucoin_spot_production(self):
parsed = parse_broker_key('kucoin_spot_production')
assert parsed.exchange == 'kucoin'
assert parsed.product == 'spot'
assert parsed.margin_mode is None
assert parsed.mode == 'production'
assert parsed.is_live
assert not parsed.is_paper
assert not parsed.is_margin
assert parsed.is_production
def test_parse_kucoin_spot_testnet(self):
parsed = parse_broker_key('kucoin_spot_testnet')
assert parsed.exchange == 'kucoin'
assert parsed.product == 'spot'
assert parsed.mode == 'testnet'
assert parsed.is_testnet
def test_parse_kucoin_margin_isolated_production(self):
parsed = parse_broker_key('kucoin_margin_isolated_production')
assert parsed.exchange == 'kucoin'
assert parsed.product == 'margin'
assert parsed.margin_mode == 'isolated'
assert parsed.mode == 'production'
assert parsed.is_live
assert parsed.is_margin
assert parsed.is_production
def test_parse_old_format_paper(self):
"""Old 'paper' format should be normalized and parsed correctly."""
parsed = parse_broker_key('paper')
assert parsed.exchange == 'paper'
assert parsed.product == 'spot'
def test_parse_old_format_kucoin_production(self):
"""Old 'kucoin_production' format should be normalized and parsed."""
parsed = parse_broker_key('kucoin_production')
assert parsed.exchange == 'kucoin'
assert parsed.product == 'spot'
assert parsed.mode == 'production'
def test_parse_invalid_format(self):
with pytest.raises(ValueError, match="Invalid broker key"):
parse_broker_key('invalid')
def test_parse_invalid_product(self):
with pytest.raises(ValueError, match="Invalid product"):
parse_broker_key('paper_futures')
def test_parse_margin_missing_mode(self):
with pytest.raises(ValueError, match="missing margin_mode"):
parse_broker_key('paper_margin')
def test_parse_invalid_margin_mode(self):
with pytest.raises(ValueError, match="Invalid margin_mode"):
parse_broker_key('paper_margin_cross') # cross not in v1
class TestBuildBrokerKey:
"""Test broker key building."""
def test_build_paper_spot(self):
key = build_broker_key('paper', 'spot')
assert key == 'paper_spot'
def test_build_paper_margin_isolated(self):
key = build_broker_key('paper', 'margin', 'isolated')
assert key == 'paper_margin_isolated'
def test_build_kucoin_spot_production(self):
key = build_broker_key('kucoin', 'spot', mode='production')
assert key == 'kucoin_spot_production'
def test_build_kucoin_spot_testnet(self):
key = build_broker_key('kucoin', 'spot', mode='testnet')
assert key == 'kucoin_spot_testnet'
def test_build_kucoin_margin_isolated_production(self):
key = build_broker_key('kucoin', 'margin', 'isolated', 'production')
assert key == 'kucoin_margin_isolated_production'
def test_build_invalid_product(self):
with pytest.raises(ValueError, match="Invalid product"):
build_broker_key('paper', 'futures')
def test_build_margin_missing_mode(self):
with pytest.raises(ValueError, match="margin_mode"):
build_broker_key('paper', 'margin') # Missing margin_mode
def test_build_live_missing_mode(self):
with pytest.raises(ValueError, match="mode"):
build_broker_key('kucoin', 'spot') # Missing mode for live
class TestRoundTrip:
"""Test that build -> parse -> build is consistent."""
def test_paper_spot_roundtrip(self):
key = build_broker_key('paper', 'spot')
parsed = parse_broker_key(key)
rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode)
assert key == rebuilt
def test_paper_margin_roundtrip(self):
key = build_broker_key('paper', 'margin', 'isolated')
parsed = parse_broker_key(key)
rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode)
assert key == rebuilt
def test_kucoin_spot_roundtrip(self):
key = build_broker_key('kucoin', 'spot', mode='production')
parsed = parse_broker_key(key)
rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode)
assert key == rebuilt
def test_kucoin_margin_roundtrip(self):
key = build_broker_key('kucoin', 'margin', 'isolated', 'testnet')
parsed = parse_broker_key(key)
rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode)
assert key == rebuilt
class TestGetAvailableBrokerKeys:
"""Test available broker key generation."""
def test_paper_keys(self):
keys = get_available_broker_keys('paper')
assert len(keys) == 2
spot = next(k for k in keys if k['product'] == 'spot')
assert spot['key'] == 'paper_spot'
assert spot['label'] == 'Spot'
margin = next(k for k in keys if k['product'] == 'margin')
assert margin['key'] == 'paper_margin_isolated'
assert margin['margin_mode'] == 'isolated'
def test_kucoin_testnet_keys(self):
keys = get_available_broker_keys('kucoin', testnet=True)
assert len(keys) == 2
spot = next(k for k in keys if k['product'] == 'spot')
assert spot['key'] == 'kucoin_spot_testnet'
margin = next(k for k in keys if k['product'] == 'margin')
assert margin['key'] == 'kucoin_margin_isolated_testnet'
def test_kucoin_production_keys(self):
keys = get_available_broker_keys('kucoin', testnet=False)
spot = next(k for k in keys if k['product'] == 'spot')
assert spot['key'] == 'kucoin_spot_production'
margin = next(k for k in keys if k['product'] == 'margin')
assert margin['key'] == 'kucoin_margin_isolated_production'
def test_unsupported_exchange_no_margin(self):
# Exchange not in MARGIN_SUPPORTED_EXCHANGES
keys = get_available_broker_keys('coinbase', testnet=True)
assert len(keys) == 1
assert keys[0]['product'] == 'spot'
def test_exclude_margin(self):
keys = get_available_broker_keys('kucoin', testnet=True, include_margin=False)
assert len(keys) == 1
assert keys[0]['product'] == 'spot'
class TestGetExchangeConnectionKey:
"""Test exchange connection key derivation."""
def test_paper_connection_key(self):
parsed = parse_broker_key('paper_spot')
assert get_exchange_connection_key(parsed) == 'paper'
def test_paper_margin_connection_key(self):
# Paper margin uses same connection as paper spot
parsed = parse_broker_key('paper_margin_isolated')
assert get_exchange_connection_key(parsed) == 'paper'
def test_kucoin_spot_connection_key(self):
parsed = parse_broker_key('kucoin_spot_production')
assert get_exchange_connection_key(parsed) == 'kucoin_production'
def test_kucoin_margin_connection_key(self):
# Margin uses same connection as spot (same CCXT session)
parsed = parse_broker_key('kucoin_margin_isolated_production')
assert get_exchange_connection_key(parsed) == 'kucoin_production'
def test_testnet_connection_key(self):
parsed = parse_broker_key('kucoin_spot_testnet')
assert get_exchange_connection_key(parsed) == 'kucoin_testnet'
class TestIsMarginSupported:
"""Test margin support checking."""
def test_kucoin_supports_margin(self):
assert is_margin_supported('kucoin') is True
def test_paper_supports_margin(self):
assert is_margin_supported('paper') is True
def test_coinbase_no_margin(self):
assert is_margin_supported('coinbase') is False
def test_binance_no_margin_v1(self):
# Binance margin not in v1 scope
assert is_margin_supported('binance') is False