"""Tests for the broker API adapter interface and Alpaca implementation. Validates data structures, request building, response parsing, and fail-closed behavior. """ from services.adapters.broker_adapter import ( AccountInfo, AlpacaBrokerAdapter, BrokerDataAdapter, OrderEventType, OrderRequest, OrderResponse, OrderSide, OrderStatus, OrderType, PositionInfo, TradingMode, ) # --- Fake Alpaca responses --- ALPACA_ORDER_RESPONSE = { "id": "order-abc-123", "client_order_id": "client-001", "status": "accepted", "symbol": "AAPL", "side": "buy", "qty": "10", "filled_qty": "0", "filled_avg_price": None, "type": "market", "time_in_force": "day", "created_at": "2026-04-11T14:00:00Z", } ALPACA_FILLED_ORDER = { "id": "order-def-456", "status": "filled", "symbol": "AAPL", "side": "buy", "qty": "10", "filled_qty": "10", "filled_avg_price": "172.50", "type": "market", "time_in_force": "day", } ALPACA_REJECTED_ORDER = { "id": "order-ghi-789", "status": "rejected", "symbol": "AAPL", "side": "sell", "qty": "100", "filled_qty": "0", "filled_avg_price": None, } ALPACA_POSITION = { "symbol": "AAPL", "qty": "10", "avg_entry_price": "172.50", "current_price": "175.00", "unrealized_pl": "25.00", "market_value": "1750.00", "side": "long", } ALPACA_ACCOUNT = { "id": "acct-001", "buying_power": "50000.00", "cash": "25000.00", "portfolio_value": "75000.00", "currency": "USD", } # --- Enum tests --- class TestBrokerEnums: def test_order_side_values(self): assert OrderSide.BUY.value == "buy" assert OrderSide.SELL.value == "sell" def test_order_type_values(self): assert OrderType.MARKET.value == "market" assert OrderType.LIMIT.value == "limit" assert OrderType.STOP.value == "stop" assert OrderType.STOP_LIMIT.value == "stop_limit" def test_order_status_values(self): assert OrderStatus.PENDING.value == "pending" assert OrderStatus.FILLED.value == "filled" assert OrderStatus.REJECTED.value == "rejected" def test_trading_mode_values(self): assert TradingMode.PAPER.value == "paper" assert TradingMode.LIVE.value == "live" def test_order_event_type_values(self): assert OrderEventType.SUBMITTED.value == "submitted" assert OrderEventType.FILL.value == "fill" assert OrderEventType.CANCELLED.value == "cancelled" # --- OrderRequest tests --- class TestOrderRequest: def test_basic_market_order(self): req = OrderRequest( ticker="AAPL", side=OrderSide.BUY, quantity=10, ) assert req.ticker == "AAPL" assert req.side == OrderSide.BUY assert req.quantity == 10 assert req.order_type == OrderType.MARKET assert req.time_in_force == "day" assert req.idempotency_key # auto-generated def test_limit_order(self): req = OrderRequest( ticker="MSFT", side=OrderSide.SELL, quantity=5, order_type=OrderType.LIMIT, limit_price=400.0, ) assert req.order_type == OrderType.LIMIT assert req.limit_price == 400.0 def test_custom_idempotency_key(self): req = OrderRequest( ticker="AAPL", side=OrderSide.BUY, quantity=1, idempotency_key="my-key-123", ) assert req.idempotency_key == "my-key-123" def test_to_dict(self): req = OrderRequest( ticker="AAPL", side=OrderSide.BUY, quantity=10, order_type=OrderType.LIMIT, limit_price=170.0, idempotency_key="key-1", ) d = req.to_dict() assert d["ticker"] == "AAPL" assert d["side"] == "buy" assert d["quantity"] == 10 assert d["order_type"] == "limit" assert d["limit_price"] == 170.0 assert d["idempotency_key"] == "key-1" def test_to_dict_omits_none_prices(self): req = OrderRequest(ticker="AAPL", side=OrderSide.BUY, quantity=1) d = req.to_dict() assert "limit_price" not in d assert "stop_price" not in d # --- OrderResponse tests --- class TestOrderResponse: def test_ok_when_accepted(self): resp = OrderResponse( broker_order_id="abc", status=OrderStatus.ACCEPTED, ticker="AAPL", side=OrderSide.BUY, quantity=10, ) assert resp.ok is True def test_not_ok_when_rejected(self): resp = OrderResponse( broker_order_id="abc", status=OrderStatus.REJECTED, ticker="AAPL", side=OrderSide.BUY, quantity=10, error="insufficient funds", ) assert resp.ok is False def test_not_ok_when_error(self): resp = OrderResponse( broker_order_id="abc", status=OrderStatus.SUBMITTED, ticker="AAPL", side=OrderSide.BUY, quantity=10, error="network failure", ) assert resp.ok is False def test_to_dict(self): resp = OrderResponse( broker_order_id="order-1", status=OrderStatus.FILLED, ticker="AAPL", side=OrderSide.BUY, quantity=10, filled_quantity=10, filled_avg_price=172.5, ) d = resp.to_dict() assert d["broker_order_id"] == "order-1" assert d["status"] == "filled" assert d["filled_avg_price"] == 172.5 # --- PositionInfo tests --- class TestPositionInfo: def test_basic_position(self): pos = PositionInfo( ticker="AAPL", quantity=10, avg_entry_price=172.5, current_price=175.0, unrealized_pnl=25.0, market_value=1750.0, ) assert pos.ticker == "AAPL" assert pos.side == "long" def test_to_dict(self): pos = PositionInfo( ticker="AAPL", quantity=10, avg_entry_price=172.5, current_price=175.0, unrealized_pnl=25.0, market_value=1750.0, side="short", ) d = pos.to_dict() assert d["side"] == "short" assert d["unrealized_pnl"] == 25.0 # --- AccountInfo tests --- class TestAccountInfo: def test_basic_account(self): acct = AccountInfo( account_id="acct-1", buying_power=50000, cash=25000, portfolio_value=75000, ) assert acct.mode == TradingMode.PAPER assert acct.currency == "USD" def test_to_dict(self): acct = AccountInfo( account_id="acct-1", buying_power=50000, cash=25000, portfolio_value=75000, mode=TradingMode.LIVE, ) d = acct.to_dict() assert d["mode"] == "live" assert d["portfolio_value"] == 75000 # --- AlpacaBrokerAdapter tests --- class TestAlpacaSourceType: def test_source_type(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s") assert adapter.source_type() == "broker" def test_inherits_broker_data_adapter(self): assert issubclass(AlpacaBrokerAdapter, BrokerDataAdapter) def test_bucket_name(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s") assert adapter.bucket_name() == "stonks-raw-broker" def test_default_mode_is_paper(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s") assert adapter.mode == TradingMode.PAPER def test_paper_base_url(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s", mode=TradingMode.PAPER) assert "paper" in adapter.base_url def test_live_base_url(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s", mode=TradingMode.LIVE) assert "paper" not in adapter.base_url def test_custom_base_url(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s", base_url="http://localhost:8080/") assert adapter.base_url == "http://localhost:8080" class TestAlpacaHeaders: def test_headers_contain_api_keys(self): adapter = AlpacaBrokerAdapter(api_key="my-key", api_secret="my-secret") headers = adapter._headers() assert headers["APCA-API-KEY-ID"] == "my-key" assert headers["APCA-API-SECRET-KEY"] == "my-secret" assert headers["Content-Type"] == "application/json" class TestAlpacaBuildFetchUrl: def setup_method(self): self.adapter = AlpacaBrokerAdapter( api_key="k", api_secret="s", base_url="https://paper-api.alpaca.markets" ) def test_positions_url(self): url = self.adapter._build_fetch_url("AAPL", "positions") assert url == "https://paper-api.alpaca.markets/v2/positions/AAPL" def test_orders_url(self): url = self.adapter._build_fetch_url("AAPL", "orders") assert "v2/orders" in url assert "symbols=AAPL" in url def test_account_url(self): url = self.adapter._build_fetch_url("AAPL", "account") assert url == "https://paper-api.alpaca.markets/v2/account" def test_default_is_positions(self): url = self.adapter._build_fetch_url("AAPL", "unknown") assert "/v2/positions/AAPL" in url class TestAlpacaParseOrderResponse: def setup_method(self): self.adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s") def test_parse_accepted_order(self): resp = self.adapter._parse_order_response(ALPACA_ORDER_RESPONSE) assert resp.broker_order_id == "order-abc-123" assert resp.status == OrderStatus.ACCEPTED assert resp.ticker == "AAPL" assert resp.side == OrderSide.BUY assert resp.quantity == 10 assert resp.filled_quantity == 0 assert resp.filled_avg_price is None def test_parse_filled_order(self): resp = self.adapter._parse_order_response(ALPACA_FILLED_ORDER) assert resp.status == OrderStatus.FILLED assert resp.filled_quantity == 10 assert resp.filled_avg_price == 172.5 def test_parse_rejected_order(self): resp = self.adapter._parse_order_response(ALPACA_REJECTED_ORDER) assert resp.status == OrderStatus.REJECTED assert resp.ok is False def test_parse_unknown_status_defaults_to_pending(self): data = {**ALPACA_ORDER_RESPONSE, "status": "some_new_status"} resp = self.adapter._parse_order_response(data) assert resp.status == OrderStatus.PENDING def test_parse_sell_side(self): data = {**ALPACA_ORDER_RESPONSE, "side": "sell"} resp = self.adapter._parse_order_response(data) assert resp.side == OrderSide.SELL class TestAlpacaParsePosition: def setup_method(self): self.adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s") def test_parse_position(self): pos = self.adapter._parse_position(ALPACA_POSITION) assert pos.ticker == "AAPL" assert pos.quantity == 10 assert pos.avg_entry_price == 172.5 assert pos.current_price == 175.0 assert pos.unrealized_pnl == 25.0 assert pos.market_value == 1750.0 assert pos.side == "long" def test_parse_position_missing_fields(self): pos = self.adapter._parse_position({"symbol": "TSLA"}) assert pos.ticker == "TSLA" assert pos.quantity == 0 assert pos.avg_entry_price == 0 class TestAlpacaErrorResult: def test_error_result_fields(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s") result = adapter._error_result("AAPL", "rate limited", 150.0, http_status=429, raw=b"slow down") assert not result.ok assert result.error == "rate limited" assert result.http_status == 429 assert result.response_time_ms == 150.0 assert result.raw_payload == b"slow down" assert result.metadata["provider"] == "alpaca" assert result.metadata["mode"] == "paper" assert result.source_type == "broker" def test_error_result_defaults(self): adapter = AlpacaBrokerAdapter(api_key="k", api_secret="s") result = adapter._error_result("MSFT", "timeout", 200.0) assert result.http_status is None assert result.raw_payload == b"" assert result.ticker == "MSFT"