feat: reset endpoint now liquidates Alpaca positions and cancels orders

- Added cancel_all_orders() and close_all_positions() to AlpacaBrokerAdapter
- Reset endpoint creates a temporary adapter to call Alpaca DELETE /v2/orders
  and DELETE /v2/positions before clearing DB and engine state
- Also clears positions table and processed_recommendation_ids on reset
- Broker reset is best-effort — DB/engine reset proceeds even if Alpaca fails
This commit is contained in:
Celes Renata
2026-04-17 04:03:31 +00:00
parent 5fc78bd9b4
commit 5fb59b379c
2 changed files with 85 additions and 1 deletions
+52
View File
@@ -276,6 +276,14 @@ class BrokerDataAdapter(BaseAdapter, ABC):
"""Get account summary (balance, buying power, etc.)."""
...
async def cancel_all_orders(self) -> int:
"""Cancel all open orders. Returns the number of orders cancelled."""
return 0
async def close_all_positions(self) -> int:
"""Liquidate all open positions. Returns the number of positions closed."""
return 0
# --- Concrete Alpaca implementation ---
@@ -551,6 +559,50 @@ class AlpacaBrokerAdapter(BrokerDataAdapter):
mode=self._mode,
)
async def cancel_all_orders(self) -> int:
"""Cancel all open orders on Alpaca.
Uses DELETE /v2/orders which cancels all open orders in bulk.
Returns the number of orders that were cancelled.
"""
async with httpx.AsyncClient(timeout=30) as client:
try:
resp = await client.delete(
f"{self.base_url}/v2/orders",
headers=self._headers(),
)
resp.raise_for_status()
# Alpaca returns a list of cancelled order objects (HTTP 207)
data = resp.json()
cancelled = len(data) if isinstance(data, list) else 0
logger.info("Cancelled %d open orders on Alpaca", cancelled)
return cancelled
except Exception as e:
logger.error("Cancel all orders failed: %s", e)
return 0
async def close_all_positions(self) -> int:
"""Liquidate all open positions on Alpaca.
Uses DELETE /v2/positions which closes all positions at market.
Returns the number of positions that were closed.
"""
async with httpx.AsyncClient(timeout=30) as client:
try:
resp = await client.delete(
f"{self.base_url}/v2/positions",
headers=self._headers(),
)
resp.raise_for_status()
# Alpaca returns a list of closed position order objects (HTTP 207)
data = resp.json()
closed = len(data) if isinstance(data, list) else 0
logger.info("Closed %d positions on Alpaca", closed)
return closed
except Exception as e:
logger.error("Close all positions failed: %s", e)
return 0
def _parse_order_response(self, data: dict[str, Any]) -> OrderResponse:
"""Parse an Alpaca order response into an OrderResponse."""
status_map: dict[str, OrderStatus] = {