trading/shared/broker/base.py
Viktor Barzin 82dc622544 feat(kevin): reconcile Alpaca bracket auto-closes + order status
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>
2026-06-04 22:31:24 +00:00

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.
"""
...