money-path: daily self-restart + SIGTERM + positive-evidence exit gate
Plan 003 steps 1-3. - Loop exit(0) after a SUCCESSFUL EOD flatten (TRADING_EXIT_AFTER_EOD_FLATTEN, default on); compose restarts fresh. Restart-churn guard: _rebuild_daily_state detects today's eod_flatten event and seeds _session_closed/_eod_done so a post-flatten restart neither re-flattens nor re-exits. SIGTERM = clean drain. - Exit manager: _FETCH_FAILED sentinel distinguishes a failed get_orders from a genuinely-empty snapshot. A failed fetch skips naked-stop enforcement (catastrophe still exits, re-fetching); an empty-but-successful snapshot needs TWO consecutive reads before 'naked' is trusted (Alpaca silently returns [] for symbols=). Costs ~one 12s pass on the all-naked case only; isolated naked positions unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af8137dfd7
commit
d16eb85ee9
7 files changed
+322
-9
No files matched your search
+42
-4
@@ -24,6 +24,10 @@ from strategy.exits import ExitConfig, TRAIL_NONE, catastrophe_triggered, manage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Sentinel for "the orders fetch FAILED" — distinct from None / an empty list so
|
||||
# a transient broker/API error is never mistaken for "every position is naked".
|
||||
_FETCH_FAILED = object()
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Tracked:
|
||||
@@ -53,6 +57,7 @@ class LiveExitManager:
|
||||
self._tracked: dict[str, _Tracked] = {}
|
||||
self._naked_passes: dict[str, int] = {} # consecutive passes a held position had no live stop
|
||||
self._open_anchored: set[str] = set() # breakeven carries re-anchored to today's opening range
|
||||
self._empty_snapshot_streak = 0 # consecutive empty-but-successful order snapshots (Alpaca can silently return [])
|
||||
|
||||
# -- broker helpers ----------------------------------------------------
|
||||
|
||||
@@ -65,19 +70,26 @@ class LiveExitManager:
|
||||
# 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.
|
||||
# POSITIVE-EVIDENCE CONTRACT: a FAILED call returns the _FETCH_FAILED
|
||||
# sentinel (NOT None), so it's distinguishable from a genuinely-empty
|
||||
# but successful response — the caller must never read a fetch failure
|
||||
# as "every position is naked" and market-flatten a protected book.
|
||||
try:
|
||||
return self.client.get_orders(status="all", limit=500, nested=True,
|
||||
symbols=list(symbols) if symbols else None)
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
return _FETCH_FAILED
|
||||
|
||||
def _stop_leg(self, symbol: str, orders):
|
||||
"""(order_id, stop_price) of the live sell-stop protecting symbol.
|
||||
|
||||
Uses pre-fetched ``orders`` (status="all" — the protective leg is a
|
||||
'held' child of the FILLED entry, which status="open" wouldn't surface).
|
||||
A failed fetch (``_FETCH_FAILED``) or ``None`` yields (None, None) — the
|
||||
pass loop guards naked enforcement on the sentinel separately, so this
|
||||
only ever means "no stop found in what we could read".
|
||||
"""
|
||||
if orders is None:
|
||||
if orders is None or orders is _FETCH_FAILED:
|
||||
return (None, None)
|
||||
for o in orders:
|
||||
for c in [o, *(o.get("legs") or [])]:
|
||||
@@ -184,6 +196,23 @@ class LiveExitManager:
|
||||
orders = self._all_orders(held.keys())
|
||||
mkt_open = self._market_open() # once per pass — gates flatten-vs-rest + trailing
|
||||
|
||||
# POSITIVE-EVIDENCE GATE — never let an ambiguous read command a naked flatten:
|
||||
# - fetch FAILED (sentinel): skip naked enforcement this pass entirely; the
|
||||
# catastrophe stop still runs but flattens with orders=None so the helper
|
||||
# re-fetches instead of consuming the sentinel.
|
||||
# - fetch SUCCEEDED but empty while positions are held (Alpaca's documented
|
||||
# silently-empty symbols= response): require TWO consecutive empty reads
|
||||
# before trusting "naked" — one blip can't flatten a protected book.
|
||||
fetch_failed = orders is _FETCH_FAILED
|
||||
orders_for_flatten = None if fetch_failed else orders
|
||||
if fetch_failed:
|
||||
logger.warning("orders fetch failed — skipping naked-stop enforcement this pass")
|
||||
else:
|
||||
self._empty_snapshot_streak = (self._empty_snapshot_streak + 1) if not orders else 0
|
||||
empty_untrusted = (not fetch_failed) and (not orders) and self._empty_snapshot_streak < 2
|
||||
# naked enforcement is suppressed when we can't positively confirm nakedness
|
||||
naked_ok = not fetch_failed and not empty_untrusted
|
||||
|
||||
actions: list[str] = []
|
||||
for sym, p in held.items():
|
||||
entry = float(p.get("avg_entry_price") or 0.0)
|
||||
@@ -202,7 +231,11 @@ class LiveExitManager:
|
||||
loss_pct = (entry - cur) / entry * 100
|
||||
try:
|
||||
if mkt_open:
|
||||
self._flatten(sym, orders)
|
||||
# orders_for_flatten is None on a failed fetch so the helper
|
||||
# re-fetches rather than acting on the sentinel; a true
|
||||
# catastrophe still exits even when stop status is unknowable
|
||||
# (a deep-drawdown position is worse than a redundant cancel).
|
||||
self._flatten(sym, orders_for_flatten)
|
||||
self._emit(sym, f"CATASTROPHE exit {sym} @~{cur:.4f} "
|
||||
f"({loss_pct:.1f}% below entry)", "exit_catastrophe",
|
||||
{"price": cur, "entry": entry, "stop": cur_stop})
|
||||
@@ -231,7 +264,7 @@ class LiveExitManager:
|
||||
# Flatten after a short grace (consecutive passes) so a just-filled
|
||||
# bracket's legs can attach first.
|
||||
grace = self.cfg.enforce_stop_grace_passes
|
||||
if grace and entry > 0 and leg_id is None:
|
||||
if grace and entry > 0 and leg_id is None and naked_ok:
|
||||
n = self._naked_passes.get(sym, 0) + 1
|
||||
self._naked_passes[sym] = n
|
||||
if n >= grace:
|
||||
@@ -259,6 +292,11 @@ class LiveExitManager:
|
||||
logger.warning("naked-stop enforcement failed %s: %s", sym, exc)
|
||||
self._naked_passes.pop(sym, None)
|
||||
continue # don't run trail logic on a naked position
|
||||
# Couldn't positively confirm the stop this pass (failed/untrusted-empty
|
||||
# fetch) and no leg was found: skip the symbol — don't reset the naked
|
||||
# counter on unverified evidence, and don't trail against a None stop.
|
||||
if leg_id is None and not naked_ok:
|
||||
continue
|
||||
self._naked_passes.pop(sym, None) # has a live stop
|
||||
|
||||
# TRAILING runs only while the session is OPEN: after-hours there are no new
|
||||
|
||||
Reference in new issue
Block a user