989 lines
31 KiB
Python
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
|