272 lines
9.9 KiB
Python
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
|