Journal: capture bracket-leg exits + paginate the order history

Today's-trades reconstruction showed normally-exited positions as falsely
"open" and understated realized P&L. Two causes:

1. sync_orders recorded only TOP-LEVEL orders. A position closed by its bracket
   STOP or TAKE-PROFIT leg has that exit fill as a child LEG, so every stop/TP
   exit was invisible — QTEX/BBD/UWMC/etc. looked open when they were flat, and
   their realized P&L was dropped. Now flatten parent + filled legs (legs inherit
   the parent symbol).

2. get_orders only fetched the most-recent page; Alpaca's `limit` truncates by
   recency (legs count toward the cap), so a busy day could drop the morning's
   fills entirely. Unscoped get_orders now pages backwards with `until` (dedup by
   id) to capture the full day; the symbol-scoped path stays single-page.

Verified live: journal open set now matches the broker exactly
(ATPC/GRAB/LNKS/NOWL/WPRT) and every reconstructed round-trip ties to a real
broker fill. NB: the realized total is FIFO-lot based, so while positions are
partially open it won't equal the broker's average-cost day P&L — the broker
figure remains the authoritative headline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-18 14:13:35 -04:00
1 parent e8533446b2
commit ddf93d199c
2 files changed
+43 -8

No files matched your search

+26 -2
View File
@@ -176,10 +176,34 @@ class AlpacaPaperClient:
# the query to the handful of held names (no truncation risk); that's what
# the exit manager / safety checks must do.
params: dict = {"status": status, "limit": min(limit, 500),
"nested": str(nested).lower()}
"nested": str(nested).lower(), "direction": "desc"}
if symbols:
# scoped to a few names — well under the cap, one page is complete
params["symbols"] = ",".join(symbols)
return self._trading("GET", "/orders", params=params)
return self._trading("GET", "/orders", params=params)
# unscoped: page backwards with `until` so the whole history is captured,
# not just the most-recent page (the journal needs every fill, incl. early
# ones, or realized P&L silently drops the morning's round-trips). Dedup by
# id across page boundaries; cap pages as a runaway guard.
out: list[dict] = []
seen: set = set()
until: str | None = None
for _ in range(20):
page_params = dict(params)
if until:
page_params["until"] = until
page = self._trading("GET", "/orders", params=page_params) or []
fresh = [o for o in page if o.get("id") not in seen]
for o in fresh:
seen.add(o.get("id"))
out.extend(fresh)
if len(page) < params["limit"]:
break # last page
subs = [o.get("submitted_at") for o in page if o.get("submitted_at")]
if not subs:
break
until = min(subs) # next page: orders older than this one
return out
def submit_order(
self,
+17 -6
View File
@@ -113,16 +113,19 @@ class AlpacaPaperSync:
def sync_orders(self, status: str = "all", limit: int = 500) -> list[dict] | None:
try:
raw = self.client.get_orders(status=status, limit=limit)
raw = self.client.get_orders(status=status, limit=limit, nested=True)
except Exception as exc:
logger.warning("alpaca orders sync failed: %s", exc)
self._emit_health(f"orders: {exc}")
return None
orders = [
{
def _flat(o: dict, parent: dict | None = None) -> dict:
return {
"broker_order_id": o.get("id"),
"client_order_id": o.get("client_order_id"),
"symbol": o.get("symbol"),
# bracket child legs (stop/take-profit) don't repeat the symbol —
# inherit it from the parent so an exit fill isn't dropped.
"symbol": o.get("symbol") or (parent or {}).get("symbol"),
"side": o.get("side"),
"quantity": float(o.get("qty") or 0),
"filled_quantity": float(o.get("filled_qty") or 0),
@@ -132,8 +135,16 @@ class AlpacaPaperSync:
"filled_avg_price": o.get("filled_avg_price"),
"submitted_at": o.get("submitted_at"),
}
for o in raw
]
# Flatten parent orders AND their child legs. A position exited by its
# bracket STOP or TAKE-PROFIT leg has that exit fill as a LEG, not a
# top-level order — recording only parents made every stop/TP exit
# invisible (positions showed falsely "open", realized P&L understated).
orders = []
for o in raw:
orders.append(_flat(o))
for leg in (o.get("legs") or []):
orders.append(_flat(leg, parent=o))
self.store.emit(
AccountOrdersUpdatedEvent(
timestamp=datetime.now(),