""" 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_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_not_implemented(self): """Test that live broker raises NotImplementedError.""" with pytest.raises(NotImplementedError): create_broker(mode=TradingMode.LIVE) 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