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:
1 parent
e8533446b2
commit
ddf93d199c
2 files changed
+43
-8
No files matched your search
+26
-2
@@ -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
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user