brighter-trading/tests/test_brokers.py

289 lines
8.8 KiB
Python

"""
Tests for the broker abstraction layer.
"""
import pytest
from brokers import (
BaseBroker, BacktestBroker, PaperBroker,
OrderSide, OrderType, OrderStatus, OrderResult, Position,
create_broker, TradingMode
)
class TestPaperBroker:
"""Tests for PaperBroker."""
def test_create_paper_broker(self):
"""Test creating a paper broker."""
broker = PaperBroker(initial_balance=10000)
assert broker.get_balance() == 10000
assert broker.get_available_balance() == 10000
def test_paper_broker_market_buy(self):
"""Test market buy order."""
broker = PaperBroker(initial_balance=10000, commission=0.001)
broker.update_price('BTC/USDT', 50000)
result = broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.MARKET,
size=0.1
)
assert result.success
assert result.status == OrderStatus.FILLED
position = broker.get_position('BTC/USDT')
assert position is not None
assert position.size == 0.1
# Check balance deducted (price * size + commission)
expected_cost = 50000 * 0.1 * (1 + 0.001) # with slippage
assert broker.get_available_balance() < 10000
def test_paper_broker_market_sell(self):
"""Test market sell order."""
broker = PaperBroker(initial_balance=10000, commission=0.001)
broker.update_price('BTC/USDT', 50000)
# First buy
broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.MARKET,
size=0.1
)
# Update price and sell
broker.update_price('BTC/USDT', 55000)
result = broker.place_order(
symbol='BTC/USDT',
side=OrderSide.SELL,
order_type=OrderType.MARKET,
size=0.1
)
assert result.success
assert result.status == OrderStatus.FILLED
# Position should be closed
position = broker.get_position('BTC/USDT')
assert position is None
def test_paper_broker_insufficient_funds(self):
"""Test order rejection due to insufficient funds."""
broker = PaperBroker(initial_balance=1000)
broker.update_price('BTC/USDT', 50000)
result = broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.MARKET,
size=0.1 # Would cost ~5000
)
assert not result.success
assert "Insufficient funds" in result.message
def test_paper_broker_limit_order(self):
"""Test limit order placement and fill."""
broker = PaperBroker(initial_balance=10000, commission=0.001)
broker.update_price('BTC/USDT', 50000)
result = broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.LIMIT,
size=0.1,
price=49000
)
assert result.success
assert result.status == OrderStatus.OPEN
# Order should be pending
open_orders = broker.get_open_orders()
assert len(open_orders) == 1
# Update price below limit - should fill
broker.update_price('BTC/USDT', 48000)
events = broker.update()
assert len(events) == 1
assert events[0]['type'] == 'fill'
# Now position should exist
position = broker.get_position('BTC/USDT')
assert position is not None
assert position.size == 0.1
def test_paper_broker_cancel_order(self):
"""Test order cancellation."""
broker = PaperBroker(initial_balance=10000, commission=0, slippage=0)
broker.update_price('BTC/USDT', 50000)
initial_balance = broker.get_available_balance()
result = broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.LIMIT,
size=0.1,
price=49000
)
assert result.success
assert broker.get_available_balance() < initial_balance # Funds locked
# Cancel the order
cancelled = broker.cancel_order(result.order_id)
assert cancelled
# Funds should be released
assert broker.get_available_balance() == initial_balance
def test_paper_broker_pnl_tracking(self):
"""Test P&L tracking."""
broker = PaperBroker(initial_balance=10000, commission=0, slippage=0)
broker.update_price('BTC/USDT', 50000)
# Buy at 50000
broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.MARKET,
size=0.1
)
# Price goes up
broker.update_price('BTC/USDT', 52000)
broker.update()
position = broker.get_position('BTC/USDT')
assert position is not None
# Unrealized P&L: (52000 - 50000) * 0.1 = 200
assert position.unrealized_pnl == 200
def test_paper_broker_equity_calculation(self):
"""Test that equity = cash + position value (not just unrealized PnL)."""
broker = PaperBroker(initial_balance=10000, commission=0, slippage=0)
broker.update_price('BTC/USDT', 100)
# Initial equity should equal initial balance
assert broker.get_balance() == 10000
# Buy 1 unit at $100 = spend $100
broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.MARKET,
size=1.0
)
# Cash is now 9900, position value is 100
# Total equity should still be 10000 (9900 + 100)
assert broker.get_available_balance() == 9900 # Cash
assert broker.get_balance() == 10000 # Total equity
# Price doubles
broker.update_price('BTC/USDT', 200)
broker.update()
# Cash still 9900, position value now 200
# Total equity should be 10100 (9900 + 200)
assert broker.get_available_balance() == 9900
assert broker.get_balance() == 10100
def test_paper_broker_reset(self):
"""Test broker reset."""
broker = PaperBroker(initial_balance=10000)
broker.update_price('BTC/USDT', 50000)
broker.place_order(
symbol='BTC/USDT',
side=OrderSide.BUY,
order_type=OrderType.MARKET,
size=0.1
)
broker.reset()
assert broker.get_balance() == 10000
assert broker.get_all_positions() == []
assert broker.get_open_orders() == []
class TestBrokerFactory:
"""Tests for the broker factory."""
def test_create_paper_broker(self):
"""Test creating paper broker via factory."""
broker = create_broker(
mode=TradingMode.PAPER,
initial_balance=5000
)
assert isinstance(broker, PaperBroker)
assert broker.get_balance() == 5000
def test_create_backtest_broker(self):
"""Test creating backtest broker via factory."""
broker = create_broker(
mode=TradingMode.BACKTEST,
initial_balance=10000
)
assert isinstance(broker, BacktestBroker)
def test_create_live_broker_falls_back_to_paper(self):
"""Test that live broker falls back to paper broker with warning."""
broker = create_broker(mode=TradingMode.LIVE, initial_balance=5000)
# Should return a PaperBroker (fallback)
assert isinstance(broker, PaperBroker)
assert broker.get_balance() == 5000
def test_invalid_mode(self):
"""Test that invalid mode raises ValueError."""
with pytest.raises(ValueError):
create_broker(mode='invalid_mode')
class TestOrderResult:
"""Tests for OrderResult dataclass."""
def test_order_result_success(self):
"""Test successful order result."""
result = OrderResult(
success=True,
order_id='12345',
status=OrderStatus.FILLED,
filled_qty=0.1,
filled_price=50000
)
assert result.success
assert result.order_id == '12345'
assert result.status == OrderStatus.FILLED
def test_order_result_failure(self):
"""Test failed order result."""
result = OrderResult(
success=False,
message="Insufficient funds"
)
assert not result.success
assert "Insufficient" in result.message
class TestPosition:
"""Tests for Position dataclass."""
def test_position_creation(self):
"""Test position creation."""
position = Position(
symbol='BTC/USDT',
size=0.1,
entry_price=50000,
current_price=51000,
unrealized_pnl=100
)
assert position.symbol == 'BTC/USDT'
assert position.size == 0.1
assert position.unrealized_pnl == 100