Exit manager: scope get_orders to held symbols (stop-leg truncation)
Alpaca's orders `limit` keeps only the most-RECENT orders and counts child legs toward the cap. On a busy day the order list runs to hundreds (every backed-out entry is an order), so the exit manager's limit=100 snapshot silently dropped the stop legs of older OPEN positions. A stop-leg lookup then read "no stop" on a fully-protected position -> broken trailing/profit-lock for that name (and false-naked alarms in monitoring). Fix: get_orders gains a `symbols` filter; the exit manager scopes its snapshot to the handful of held names (no truncation possible). Verified live: an unscoped limit=100 query reported PLUG stop=None while the scoped query correctly surfaced PLUG's stop @2.65. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
1cd82de6a8
commit
11ab6c7b56
2 files changed
+24
-9
No files matched your search
+15
-5
@@ -162,14 +162,24 @@ class AlpacaPaperClient:
|
||||
return self._cached("positions", lambda: self._trading("GET", "/positions"))
|
||||
|
||||
def get_orders(
|
||||
self, status: str = "all", limit: int = 100, nested: bool = True
|
||||
self, status: str = "all", limit: int = 500, nested: bool = True,
|
||||
symbols: list[str] | None = None,
|
||||
) -> list[dict]:
|
||||
# nested=true includes bracket child legs (stop/target) so working risk
|
||||
# isn't under-reported in the orders snapshot.
|
||||
return self._trading(
|
||||
"GET", "/orders",
|
||||
params={"status": status, "limit": limit, "nested": str(nested).lower()},
|
||||
)
|
||||
#
|
||||
# IMPORTANT: Alpaca's `limit` truncates to the most-RECENT orders (and
|
||||
# counts child legs toward the cap). On a busy day the order list runs to
|
||||
# hundreds (every backed-out entry is an order), so a small limit silently
|
||||
# drops the stop legs of older OPEN positions — a stop-leg lookup then
|
||||
# reads "no stop" on a fully-protected position. Pass `symbols` to scope
|
||||
# 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()}
|
||||
if symbols:
|
||||
params["symbols"] = ",".join(symbols)
|
||||
return self._trading("GET", "/orders", params=params)
|
||||
|
||||
def submit_order(
|
||||
self,
|
||||
|
||||
@@ -57,9 +57,13 @@ class LiveExitManager:
|
||||
_ACTIVE = {"held", "new", "accepted", "pending_new",
|
||||
"accepted_for_bidding", "partially_filled"}
|
||||
|
||||
def _all_orders(self):
|
||||
def _all_orders(self, symbols=None):
|
||||
# scope to the held symbols — Alpaca's `limit` keeps only the most-recent
|
||||
# orders, so on a busy day an unscoped query drops older positions' stop
|
||||
# legs and we'd misread a protected position as naked.
|
||||
try:
|
||||
return self.client.get_orders(status="all", limit=100, nested=True)
|
||||
return self.client.get_orders(status="all", limit=500, nested=True,
|
||||
symbols=list(symbols) if symbols else None)
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
|
||||
@@ -126,8 +130,9 @@ class LiveExitManager:
|
||||
if not held:
|
||||
return []
|
||||
# fetch the orders snapshot ONCE per pass (status="all" is the heavy call;
|
||||
# reuse it for every position's stop-leg + entry-time lookups)
|
||||
orders = self._all_orders()
|
||||
# reuse it for every position's stop-leg + entry-time lookups), scoped to
|
||||
# the held symbols so a busy day's order volume can't truncate the legs.
|
||||
orders = self._all_orders(held.keys())
|
||||
|
||||
actions: list[str] = []
|
||||
for sym, p in held.items():
|
||||
|
||||
Reference in new issue
Block a user