TradingAgents/tests/unit/execution/test_alpaca_broker.py

989 lines
31 KiB
Python

"""Tests for Alpaca Broker module.
Issue #24: [EXEC-23] Alpaca broker - US stocks, ETFs, crypto
These tests use mocks to test the broker without requiring actual
Alpaca API credentials.
"""
import pytest
from datetime import datetime, timezone
from decimal import Decimal
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch
import sys
from tradingagents.execution import (
# Enums
AssetClass,
OrderSide,
OrderType,
TimeInForce,
OrderStatus,
PositionSide,
# Data Classes
OrderRequest,
Order,
Position,
AccountInfo,
Quote,
AssetInfo,
# Exceptions
BrokerError,
AuthenticationError,
ConnectionError,
OrderError,
InsufficientFundsError,
InvalidOrderError,
PositionError,
RateLimitError,
)
# =============================================================================
# Mock Alpaca SDK
# =============================================================================
class MockAlpacaOrderSide:
"""Mock Alpaca OrderSide enum."""
BUY = "buy"
SELL = "sell"
class MockAlpacaOrderType:
"""Mock Alpaca OrderType enum."""
MARKET = "market"
LIMIT = "limit"
STOP = "stop"
STOP_LIMIT = "stop_limit"
TRAILING_STOP = "trailing_stop"
class MockAlpacaOrderStatus:
"""Mock Alpaca OrderStatus enum."""
NEW = "new"
ACCEPTED = "accepted"
PENDING_NEW = "pending_new"
PARTIALLY_FILLED = "partially_filled"
FILLED = "filled"
CANCELED = "canceled"
EXPIRED = "expired"
REPLACED = "replaced"
REJECTED = "rejected"
DONE_FOR_DAY = "done_for_day"
PENDING_CANCEL = "pending_cancel"
PENDING_REPLACE = "pending_replace"
ACCEPTED_FOR_BIDDING = "accepted_for_bidding"
STOPPED = "stopped"
SUSPENDED = "suspended"
CALCULATED = "calculated"
class MockAlpacaTimeInForce:
"""Mock Alpaca TimeInForce enum."""
DAY = "day"
GTC = "gtc"
IOC = "ioc"
FOK = "fok"
OPG = "opg"
CLS = "cls"
class MockQueryOrderStatus:
"""Mock Alpaca QueryOrderStatus."""
OPEN = "open"
CLOSED = "closed"
ALL = "all"
class MockAlpacaPositionSide:
"""Mock Alpaca PositionSide."""
LONG = "long"
SHORT = "short"
class MockAlpacaAccount:
"""Mock Alpaca account response."""
def __init__(
self,
account_number: str = "TEST123456",
status: str = "ACTIVE",
cash: str = "100000.00",
portfolio_value: str = "150000.00",
buying_power: str = "200000.00",
equity: str = "150000.00",
initial_margin: str = "5000.00",
regt_buying_power: str = "195000.00",
daytrade_count: int = 0,
pattern_day_trader: bool = False,
account_type: str = "margin",
):
self.account_number = account_number
self.status = status
self.cash = cash
self.portfolio_value = portfolio_value
self.buying_power = buying_power
self.equity = equity
self.initial_margin = initial_margin
self.regt_buying_power = regt_buying_power
self.daytrade_count = daytrade_count
self.pattern_day_trader = pattern_day_trader
self.account_type = account_type
class MockAlpacaClock:
"""Mock Alpaca clock response."""
def __init__(self, is_open: bool = True):
self.is_open = is_open
self.timestamp = datetime.now(timezone.utc)
self.next_open = datetime.now(timezone.utc)
self.next_close = datetime.now(timezone.utc)
class MockAlpacaOrder:
"""Mock Alpaca order response."""
def __init__(
self,
id: str = "order-123",
client_order_id: Optional[str] = None,
symbol: str = "AAPL",
side: str = "buy",
qty: str = "100",
order_type: str = "market",
status: str = "new",
limit_price: Optional[str] = None,
stop_price: Optional[str] = None,
time_in_force: str = "day",
filled_qty: str = "0",
filled_avg_price: Optional[str] = None,
):
self.id = id
self.client_order_id = client_order_id
self.symbol = symbol
self.side = MagicMock()
self.side.__eq__ = lambda s, other: side == other.value if hasattr(other, 'value') else side == other
# For comparison, mock the value attribute
if side == "buy":
self.side = type('MockSide', (), {'value': 'buy', '__eq__': lambda s, o: o == MockAlpacaOrderSide.BUY or getattr(o, 'value', o) == 'buy'})()
else:
self.side = type('MockSide', (), {'value': 'sell', '__eq__': lambda s, o: o == MockAlpacaOrderSide.SELL or getattr(o, 'value', o) == 'sell'})()
self.qty = qty
self.order_type = type('MockOrderType', (), {'value': order_type})()
self.status = type('MockStatus', (), {'value': status})()
self.limit_price = limit_price
self.stop_price = stop_price
self.time_in_force = type('MockTIF', (), {'value': time_in_force})()
self.filled_qty = filled_qty
self.filled_avg_price = filled_avg_price
self.created_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(timezone.utc)
self.submitted_at = datetime.now(timezone.utc)
self.filled_at = None
self.expired_at = None
self.canceled_at = None
class MockAlpacaPosition:
"""Mock Alpaca position response."""
def __init__(
self,
symbol: str = "AAPL",
qty: str = "100",
avg_entry_price: str = "150.00",
current_price: str = "160.00",
market_value: str = "16000.00",
cost_basis: str = "15000.00",
unrealized_pl: str = "1000.00",
unrealized_plpc: str = "0.0667",
asset_class: str = "us_equity",
):
self.symbol = symbol
self.qty = qty
self.avg_entry_price = avg_entry_price
self.current_price = current_price
self.market_value = market_value
self.cost_basis = cost_basis
self.unrealized_pl = unrealized_pl
self.unrealized_plpc = unrealized_plpc
self.asset_class = asset_class
class MockAlpacaAsset:
"""Mock Alpaca asset response."""
def __init__(
self,
symbol: str = "AAPL",
name: str = "Apple Inc.",
asset_class: str = "us_equity",
exchange: str = "NASDAQ",
tradable: bool = True,
shortable: bool = True,
marginable: bool = True,
fractionable: bool = True,
easy_to_borrow: bool = True,
):
self.symbol = symbol
self.name = name
self.asset_class = asset_class
self.exchange = exchange
self.tradable = tradable
self.shortable = shortable
self.marginable = marginable
self.fractionable = fractionable
self.easy_to_borrow = easy_to_borrow
class MockAlpacaQuote:
"""Mock Alpaca quote response."""
def __init__(
self,
symbol: str = "AAPL",
bid_price: float = 159.95,
ask_price: float = 160.05,
bid_size: int = 100,
ask_size: int = 100,
):
self.symbol = symbol
self.bid_price = bid_price
self.ask_price = ask_price
self.bid_size = bid_size
self.ask_size = ask_size
self.timestamp = datetime.now(timezone.utc)
class MockTradingClient:
"""Mock Alpaca TradingClient."""
def __init__(
self,
api_key: str,
secret_key: str,
paper: bool = True,
):
self.api_key = api_key
self.secret_key = secret_key
self.paper = paper
self._orders: Dict[str, MockAlpacaOrder] = {}
self._order_counter = 0
def get_account(self) -> MockAlpacaAccount:
return MockAlpacaAccount()
def get_clock(self) -> MockAlpacaClock:
return MockAlpacaClock()
def submit_order(self, request: Any) -> MockAlpacaOrder:
self._order_counter += 1
order_id = f"order-{self._order_counter}"
order = MockAlpacaOrder(
id=order_id,
symbol=request.symbol,
qty=str(request.qty),
side="buy" if str(request.side).lower() == "buy" else "sell",
order_type=getattr(request, 'order_type', 'market') or 'market',
limit_price=str(getattr(request, 'limit_price', None)) if getattr(request, 'limit_price', None) else None,
stop_price=str(getattr(request, 'stop_price', None)) if getattr(request, 'stop_price', None) else None,
client_order_id=getattr(request, 'client_order_id', None),
)
self._orders[order_id] = order
return order
def cancel_order_by_id(self, order_id: str) -> None:
if order_id in self._orders:
self._orders[order_id].status = type('MockStatus', (), {'value': 'canceled'})()
def get_order_by_id(self, order_id: str) -> MockAlpacaOrder:
if order_id in self._orders:
return self._orders[order_id]
raise Exception(f"Order {order_id} not found")
def replace_order_by_id(self, order_id: str, order_data: Any) -> MockAlpacaOrder:
self._order_counter += 1
new_order_id = f"order-{self._order_counter}"
old_order = self._orders.get(order_id)
if not old_order:
raise Exception(f"Order {order_id} not found")
new_order = MockAlpacaOrder(
id=new_order_id,
symbol=old_order.symbol,
qty=str(order_data.qty) if order_data.qty else old_order.qty,
side=old_order.side.value,
limit_price=str(order_data.limit_price) if order_data.limit_price else old_order.limit_price,
stop_price=str(order_data.stop_price) if order_data.stop_price else old_order.stop_price,
)
self._orders[new_order_id] = new_order
return new_order
def get_orders(self, request: Any) -> List[MockAlpacaOrder]:
return list(self._orders.values())
def get_all_positions(self) -> List[MockAlpacaPosition]:
return [MockAlpacaPosition()]
def get_open_position(self, symbol: str) -> MockAlpacaPosition:
return MockAlpacaPosition(symbol=symbol)
def get_asset(self, symbol: str) -> MockAlpacaAsset:
return MockAlpacaAsset(symbol=symbol)
class MockStockHistoricalDataClient:
"""Mock Alpaca StockHistoricalDataClient."""
def __init__(self, api_key: str, secret_key: str):
self.api_key = api_key
self.secret_key = secret_key
def get_stock_latest_quote(self, request: Any) -> Dict[str, MockAlpacaQuote]:
symbols = request.symbol_or_symbols
return {symbol: MockAlpacaQuote(symbol=symbol) for symbol in symbols}
class MockCryptoHistoricalDataClient:
"""Mock Alpaca CryptoHistoricalDataClient."""
def __init__(self, api_key: str, secret_key: str):
self.api_key = api_key
self.secret_key = secret_key
def get_crypto_latest_quote(self, request: Any) -> Dict[str, MockAlpacaQuote]:
symbols = request.symbol_or_symbols
return {symbol: MockAlpacaQuote(symbol=symbol) for symbol in symbols}
# =============================================================================
# Fixtures
# =============================================================================
@pytest.fixture
def mock_alpaca_module():
"""Create mock alpaca module."""
# Create mock module structure
mock_trading = MagicMock()
mock_trading.client.TradingClient = MockTradingClient
mock_trading.requests.MarketOrderRequest = MagicMock
mock_trading.requests.LimitOrderRequest = MagicMock
mock_trading.requests.StopOrderRequest = MagicMock
mock_trading.requests.StopLimitOrderRequest = MagicMock
mock_trading.requests.TrailingStopOrderRequest = MagicMock
mock_trading.requests.ReplaceOrderRequest = MagicMock
mock_trading.requests.GetOrdersRequest = MagicMock
mock_trading.enums.OrderSide = MockAlpacaOrderSide
mock_trading.enums.OrderType = MockAlpacaOrderType
mock_trading.enums.OrderStatus = MockAlpacaOrderStatus
mock_trading.enums.TimeInForce = MockAlpacaTimeInForce
mock_trading.enums.QueryOrderStatus = MockQueryOrderStatus
mock_trading.enums.PositionSide = MockAlpacaPositionSide
mock_data = MagicMock()
mock_data.historical.StockHistoricalDataClient = MockStockHistoricalDataClient
mock_data.historical.CryptoHistoricalDataClient = MockCryptoHistoricalDataClient
mock_data.requests.StockLatestQuoteRequest = MagicMock
mock_data.requests.CryptoLatestQuoteRequest = MagicMock
mock_data.live.StockDataStream = MagicMock
mock_data.live.CryptoDataStream = MagicMock
return mock_trading, mock_data
# =============================================================================
# AlpacaBroker Tests - Initialization
# =============================================================================
class TestAlpacaBrokerInit:
"""Tests for AlpacaBroker initialization."""
def test_init_default(self):
"""Test default initialization."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker()
assert broker.name == "Alpaca"
assert broker.is_paper_trading is True
assert AssetClass.EQUITY in broker.supported_asset_classes
assert AssetClass.ETF in broker.supported_asset_classes
assert AssetClass.CRYPTO in broker.supported_asset_classes
def test_init_with_credentials(self):
"""Test initialization with credentials."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(
api_key="test-api-key",
api_secret="test-api-secret",
paper_trading=True,
)
assert broker.api_key == "test****-key" # Masked
def test_init_live_trading(self):
"""Test initialization for live trading."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(
api_key="test-api-key",
api_secret="test-api-secret",
paper_trading=False,
)
assert broker.is_paper_trading is False
assert "paper" not in broker.base_url
def test_init_paper_trading(self):
"""Test initialization for paper trading."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(
api_key="test-api-key",
api_secret="test-api-secret",
paper_trading=True,
)
assert broker.is_paper_trading is True
assert "paper" in broker.base_url
# =============================================================================
# AlpacaBroker Tests - Connection (with mocks)
# =============================================================================
class TestAlpacaBrokerConnection:
"""Tests for connection management."""
@pytest.mark.asyncio
async def test_connect_without_credentials(self):
"""Test connect fails without credentials."""
from tradingagents.execution.alpaca_broker import AlpacaBroker, ALPACA_AVAILABLE
if not ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = AlpacaBroker(api_key="", api_secret="")
with pytest.raises(AuthenticationError, match="credentials not provided"):
await broker.connect()
@pytest.mark.asyncio
async def test_connect_without_sdk(self):
"""Test connect fails gracefully without SDK."""
from tradingagents.execution import alpaca_broker
# Save original value
original_available = alpaca_broker.ALPACA_AVAILABLE
try:
# Mock SDK not available
alpaca_broker.ALPACA_AVAILABLE = False
broker = alpaca_broker.AlpacaBroker(
api_key="test-key",
api_secret="test-secret",
)
with pytest.raises(BrokerError, match="alpaca-py is not installed"):
await broker.connect()
finally:
# Restore original value
alpaca_broker.ALPACA_AVAILABLE = original_available
@pytest.mark.asyncio
async def test_disconnect(self):
"""Test disconnect."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker()
broker._connected = True
await broker.disconnect()
assert broker.is_connected is False
# =============================================================================
# AlpacaBroker Tests - Order Mapping
# =============================================================================
class TestAlpacaBrokerOrderMapping:
"""Tests for order type/side/status mapping."""
def test_map_order_side_buy(self):
"""Test mapping buy order side."""
from tradingagents.execution import alpaca_broker
if not alpaca_broker.ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = alpaca_broker.AlpacaBroker()
result = broker._map_order_side(OrderSide.BUY)
# Check it maps correctly (exact check depends on SDK)
assert result is not None
def test_map_time_in_force(self):
"""Test mapping time in force."""
from tradingagents.execution import alpaca_broker
if not alpaca_broker.ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = alpaca_broker.AlpacaBroker()
for tif in TimeInForce:
result = broker._map_time_in_force(tif)
assert result is not None
# =============================================================================
# AlpacaBroker Tests - Order Requests (structure tests)
# =============================================================================
class TestAlpacaBrokerOrderRequests:
"""Tests for order request building."""
def test_market_order_request(self):
"""Test market order request."""
request = OrderRequest.market("AAPL", OrderSide.BUY, 100)
assert request.symbol == "AAPL"
assert request.side == OrderSide.BUY
assert request.quantity == Decimal("100")
assert request.order_type == OrderType.MARKET
def test_limit_order_request(self):
"""Test limit order request."""
request = OrderRequest.limit(
symbol="AAPL",
side=OrderSide.BUY,
quantity=100,
limit_price=Decimal("150.00"),
)
assert request.symbol == "AAPL"
assert request.order_type == OrderType.LIMIT
assert request.limit_price == Decimal("150.00")
def test_stop_order_request(self):
"""Test stop order request."""
request = OrderRequest.stop(
symbol="AAPL",
side=OrderSide.SELL,
quantity=100,
stop_price=Decimal("145.00"),
)
assert request.order_type == OrderType.STOP
assert request.stop_price == Decimal("145.00")
def test_stop_limit_order_request(self):
"""Test stop-limit order request."""
request = OrderRequest.stop_limit(
symbol="AAPL",
side=OrderSide.SELL,
quantity=100,
stop_price=Decimal("145.00"),
limit_price=Decimal("144.50"),
)
assert request.order_type == OrderType.STOP_LIMIT
assert request.stop_price == Decimal("145.00")
assert request.limit_price == Decimal("144.50")
def test_trailing_stop_order_request(self):
"""Test trailing stop order request."""
request = OrderRequest.trailing_stop(
symbol="AAPL",
side=OrderSide.SELL,
quantity=100,
trail_percent=Decimal("2.0"),
)
assert request.order_type == OrderType.TRAILING_STOP
assert request.trail_percent == Decimal("2.0")
# =============================================================================
# AlpacaBroker Tests - With Mocked SDK
# =============================================================================
class TestAlpacaBrokerWithMockedSDK:
"""Tests using mocked Alpaca SDK."""
async def _create_connected_broker(self):
"""Create a broker with mocked SDK and connect it."""
from tradingagents.execution import alpaca_broker
# Set up broker
broker = alpaca_broker.AlpacaBroker(
api_key="test-key",
api_secret="test-secret",
paper_trading=True,
)
# Mock the SDK connection
broker._trading_client = MockTradingClient(
api_key="test-key",
secret_key="test-secret",
paper=True,
)
broker._stock_data_client = MockStockHistoricalDataClient(
api_key="test-key",
secret_key="test-secret",
)
broker._crypto_data_client = MockCryptoHistoricalDataClient(
api_key="test-key",
secret_key="test-secret",
)
broker._connected = True
return broker
@pytest.mark.asyncio
async def test_get_account(self):
"""Test getting account info."""
broker = await self._create_connected_broker()
account = await broker.get_account()
assert isinstance(account, AccountInfo)
assert account.account_id == "TEST123456"
assert account.status == "ACTIVE"
assert account.cash == Decimal("100000.00")
@pytest.mark.asyncio
async def test_is_market_open(self):
"""Test checking market status."""
broker = await self._create_connected_broker()
is_open = await broker.is_market_open()
assert is_open is True
@pytest.mark.asyncio
async def test_submit_market_order(self):
"""Test submitting market order."""
from tradingagents.execution import alpaca_broker
if not alpaca_broker.ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = await self._create_connected_broker()
request = OrderRequest.market("AAPL", OrderSide.BUY, 100)
order = await broker.submit_order(request)
assert order is not None
assert order.symbol == "AAPL"
@pytest.mark.asyncio
async def test_get_positions(self):
"""Test getting positions."""
broker = await self._create_connected_broker()
positions = await broker.get_positions()
assert len(positions) >= 1
assert positions[0].symbol == "AAPL"
@pytest.mark.asyncio
async def test_get_position(self):
"""Test getting specific position."""
broker = await self._create_connected_broker()
position = await broker.get_position("AAPL")
assert position is not None
assert position.symbol == "AAPL"
@pytest.mark.asyncio
async def test_get_quote_stock(self):
"""Test getting stock quote."""
from tradingagents.execution import alpaca_broker
if not alpaca_broker.ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = await self._create_connected_broker()
quote = await broker.get_quote("AAPL")
assert quote.symbol == "AAPL"
assert quote.bid_price is not None
assert quote.ask_price is not None
@pytest.mark.asyncio
async def test_get_quote_crypto(self):
"""Test getting crypto quote."""
from tradingagents.execution import alpaca_broker
if not alpaca_broker.ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = await self._create_connected_broker()
quote = await broker.get_quote("BTCUSD")
assert quote.symbol == "BTCUSD"
@pytest.mark.asyncio
async def test_get_asset(self):
"""Test getting asset info."""
broker = await self._create_connected_broker()
asset = await broker.get_asset("AAPL")
assert asset.symbol == "AAPL"
assert asset.name == "Apple Inc."
assert asset.tradable is True
# =============================================================================
# AlpacaBroker Tests - Error Handling
# =============================================================================
class TestAlpacaBrokerErrorHandling:
"""Tests for error handling."""
@pytest.mark.asyncio
async def test_requires_connection(self):
"""Test operations fail without connection."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(api_key="test", api_secret="test")
with pytest.raises(ConnectionError, match="Not connected"):
await broker.get_account()
@pytest.mark.asyncio
async def test_invalid_limit_order_without_price(self):
"""Test limit order without price fails."""
from tradingagents.execution import alpaca_broker
if not alpaca_broker.ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = alpaca_broker.AlpacaBroker(
api_key="test-key",
api_secret="test-secret",
)
broker._trading_client = MockTradingClient(
api_key="test", secret_key="test"
)
broker._connected = True
# Create limit order request without limit price
request = OrderRequest(
symbol="AAPL",
side=OrderSide.BUY,
quantity=Decimal("100"),
order_type=OrderType.LIMIT,
time_in_force=TimeInForce.DAY,
# Missing limit_price
)
with pytest.raises(InvalidOrderError, match="Limit price required"):
await broker.submit_order(request)
@pytest.mark.asyncio
async def test_invalid_stop_order_without_price(self):
"""Test stop order without price fails."""
from tradingagents.execution import alpaca_broker
if not alpaca_broker.ALPACA_AVAILABLE:
pytest.skip("alpaca-py not installed")
broker = alpaca_broker.AlpacaBroker(
api_key="test-key",
api_secret="test-secret",
)
broker._trading_client = MockTradingClient(
api_key="test", secret_key="test"
)
broker._connected = True
# Create stop order without stop price
request = OrderRequest(
symbol="AAPL",
side=OrderSide.SELL,
quantity=Decimal("100"),
order_type=OrderType.STOP,
time_in_force=TimeInForce.DAY,
# Missing stop_price
)
with pytest.raises(InvalidOrderError, match="Stop price required"):
await broker.submit_order(request)
# =============================================================================
# AlpacaBroker Tests - Asset Class Support
# =============================================================================
class TestAlpacaBrokerAssetClasses:
"""Tests for asset class support."""
def test_supports_equity(self):
"""Test broker supports equity."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker()
assert broker.supports_asset_class(AssetClass.EQUITY) is True
def test_supports_etf(self):
"""Test broker supports ETF."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker()
assert broker.supports_asset_class(AssetClass.ETF) is True
def test_supports_crypto(self):
"""Test broker supports crypto."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker()
assert broker.supports_asset_class(AssetClass.CRYPTO) is True
def test_does_not_support_futures(self):
"""Test broker does not support futures."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker()
assert broker.supports_asset_class(AssetClass.FUTURE) is False
def test_does_not_support_options(self):
"""Test broker does not support options."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker()
assert broker.supports_asset_class(AssetClass.OPTION) is False
# =============================================================================
# AlpacaBroker Tests - API Key Masking
# =============================================================================
class TestAlpacaBrokerSecurity:
"""Tests for security features."""
def test_api_key_masked(self):
"""Test API key is masked in property."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(
api_key="PKEXAMPLEAPIKEY12345",
api_secret="supersecretkey",
)
# Key should be masked
assert "****" in broker.api_key
assert "EXAMPLE" not in broker.api_key
def test_short_api_key_fully_masked(self):
"""Test short API key is fully masked."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(
api_key="short",
api_secret="test",
)
# Short keys should be fully masked
assert broker.api_key == "****"
# =============================================================================
# AlpacaBroker Tests - URL Configuration
# =============================================================================
class TestAlpacaBrokerURLs:
"""Tests for URL configuration."""
def test_paper_url(self):
"""Test paper trading URL."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(paper_trading=True)
assert "paper" in broker.base_url
def test_live_url(self):
"""Test live trading URL."""
from tradingagents.execution.alpaca_broker import AlpacaBroker
broker = AlpacaBroker(paper_trading=False)
assert "paper" not in broker.base_url
assert "api.alpaca.markets" in broker.base_url
# =============================================================================
# Integration with BrokerRouter
# =============================================================================
class TestAlpacaBrokerRouterIntegration:
"""Tests for AlpacaBroker integration with BrokerRouter."""
def test_register_with_router(self):
"""Test registering AlpacaBroker with router."""
from tradingagents.execution import AlpacaBroker, BrokerRouter
router = BrokerRouter()
broker = AlpacaBroker()
router.register(broker)
assert "Alpaca" in router.registered_brokers
assert AssetClass.EQUITY in router.supported_asset_classes
assert AssetClass.CRYPTO in router.supported_asset_classes
def test_route_to_alpaca(self):
"""Test routing routes to Alpaca for supported assets."""
from tradingagents.execution import AlpacaBroker, BrokerRouter
router = BrokerRouter()
broker = AlpacaBroker()
router.register(broker)
routed_broker, decision = router.route("AAPL")
assert routed_broker.name == "Alpaca"
assert decision.asset_class == AssetClass.EQUITY
def test_route_crypto_to_alpaca(self):
"""Test crypto routing goes to Alpaca."""
from tradingagents.execution import AlpacaBroker, BrokerRouter
router = BrokerRouter()
broker = AlpacaBroker()
router.register(broker)
routed_broker, decision = router.route("BTCUSD")
assert routed_broker.name == "Alpaca"
assert decision.asset_class == AssetClass.CRYPTO