Bracket stop-loss/take-profit legs fill at Alpaca without passing through the executor, so those closes (and their P&L) were invisible locally. - broker: add get_order(nested) + list_orders to BaseBroker/AlpacaBroker (+ SimulatedBroker); BrokerOrder carries child legs - Trade gains broker_order_id (migration f6a7b8c9d0e1); executor stamps the entry order id - new api_gateway trade-reconcile loop: books a closing SELL + realized P&L when a bracket leg fills (idempotent on the leg order id), syncs PENDING->terminal status, logs drift; runs alongside portfolio_sync [ci skip] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
3.5 KiB
Python
135 lines
3.5 KiB
Python
"""Abstract base class for brokerage integrations.
|
|
|
|
All broker implementations must inherit from ``BaseBroker`` and provide
|
|
concrete implementations for order management, position tracking, and
|
|
account information retrieval. This abstraction layer allows the trading
|
|
bot to swap brokerages (Alpaca, Interactive Brokers, Tradier, ...) without
|
|
changing strategy or execution logic.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from shared.schemas.trading import (
|
|
AccountInfo,
|
|
BrokerOrder,
|
|
OrderRequest,
|
|
OrderResult,
|
|
PositionInfo,
|
|
)
|
|
|
|
|
|
class BaseBroker(ABC):
|
|
"""Interface that every brokerage adapter must implement."""
|
|
|
|
@abstractmethod
|
|
async def submit_order(self, order: OrderRequest) -> OrderResult:
|
|
"""Submit a new order to the brokerage.
|
|
|
|
Parameters
|
|
----------
|
|
order:
|
|
The order details including ticker, side, quantity, and order type.
|
|
|
|
Returns
|
|
-------
|
|
OrderResult
|
|
Result containing the order ID, status, and fill information.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def cancel_order(self, order_id: str) -> bool:
|
|
"""Cancel an open order.
|
|
|
|
Parameters
|
|
----------
|
|
order_id:
|
|
The brokerage-assigned order identifier.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if the cancellation was accepted, ``False`` otherwise.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_positions(self) -> list[PositionInfo]:
|
|
"""Return all currently open positions.
|
|
|
|
Returns
|
|
-------
|
|
list[PositionInfo]
|
|
One entry per open position with quantity, average entry, current
|
|
price, and unrealized P&L.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_account(self) -> AccountInfo:
|
|
"""Return account-level summary information.
|
|
|
|
Returns
|
|
-------
|
|
AccountInfo
|
|
Equity, cash, buying power, and total portfolio value.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_order_status(self, order_id: str) -> OrderResult:
|
|
"""Fetch the current status of an existing order.
|
|
|
|
Parameters
|
|
----------
|
|
order_id:
|
|
The brokerage-assigned order identifier.
|
|
|
|
Returns
|
|
-------
|
|
OrderResult
|
|
Current state of the order including fill price if applicable.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_order(
|
|
self, order_id: str, *, nested: bool = True
|
|
) -> BrokerOrder | None:
|
|
"""Fetch an order including its bracket child legs.
|
|
|
|
Parameters
|
|
----------
|
|
order_id:
|
|
The brokerage-assigned order identifier.
|
|
nested:
|
|
When ``True`` (the default), child legs (stop-loss / take-profit)
|
|
are populated on the returned :class:`BrokerOrder` so callers can
|
|
tell which leg filled and at what price.
|
|
|
|
Returns
|
|
-------
|
|
BrokerOrder | None
|
|
The order with its legs, or ``None`` if the order does not exist.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def list_orders(
|
|
self, *, status: str = "all", limit: int = 100
|
|
) -> list[OrderResult]:
|
|
"""List orders, optionally filtered by status.
|
|
|
|
Parameters
|
|
----------
|
|
status:
|
|
One of ``"open"``, ``"closed"``, or ``"all"`` (the default).
|
|
limit:
|
|
Maximum number of orders to return.
|
|
|
|
Returns
|
|
-------
|
|
list[OrderResult]
|
|
One entry per matching order.
|
|
"""
|
|
...
|