Fix naked-stop regression: speed cache made the guard strip live brackets
The get_positions TTL cache (added for the speed fix) silently broke the naked-stop guard: _broker_held_symbols read 2.5s-stale positions, so a just-filled entry wasn't seen as held, the guard thought it was unfilled, and cancelled its bracket on a price dip -> NAKED position (APWC bled -12.8% unprotected today). Fixes: - client.get_positions(fresh=True) bypasses the cache; the guard uses it. - entry_grace_seconds (default 5): never cancel an entry within N seconds of arming, so a marketable fill can confirm before the bracket could be stripped. Regression test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
27b0748b71
commit
1cd82de6a8
5 files changed
+43
-4
No files matched your search
+11
-1
@@ -96,6 +96,10 @@ class ExecutionSettings:
|
||||
# cap the marketable limit so a breakout FILLS on a runner instead of
|
||||
# resting forever at the trigger, while still bounding slippage.
|
||||
trigger_slippage_pct: float = 0.004
|
||||
# don't cancel an entry within this many seconds of arming — a marketable
|
||||
# fill needs a moment to confirm at the broker; cancelling first strips its
|
||||
# bracket (the naked-stop failure mode).
|
||||
entry_grace_seconds: float = 5.0
|
||||
# --- account-aware sizing (matters most on a small REAL account) -------
|
||||
# one position's dollar value <= this fraction of equity, so a single trade
|
||||
# can't exceed buying power (critical at $300: 3 positions must fit, so ~1/3).
|
||||
@@ -137,6 +141,7 @@ class ExecutionSettings:
|
||||
trigger_slippage_pct=float(
|
||||
values.get("TRADING_TRIGGER_SLIP_PCT", "0.004")
|
||||
),
|
||||
entry_grace_seconds=float(values.get("TRADING_ENTRY_GRACE_SECONDS", "5")),
|
||||
max_position_pct=float(values.get("TRADING_MAX_POSITION_PCT", "0.33")),
|
||||
liquidity_max_volume_pct=float(
|
||||
values.get("TRADING_LIQUIDITY_MAX_VOLUME_PCT", "0.0")
|
||||
@@ -251,7 +256,7 @@ class TradingExecutionService:
|
||||
if client is None or not hasattr(client, "get_positions"):
|
||||
return None
|
||||
try:
|
||||
positions = client.get_positions() or []
|
||||
positions = client.get_positions(fresh=True) or []
|
||||
return {str(p.get("symbol")) for p in positions if p.get("symbol")}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("broker positions unreadable in entry guard: %s", exc)
|
||||
@@ -855,6 +860,11 @@ class TradingExecutionService:
|
||||
if symbol in broker_held:
|
||||
self._armed.pop(order_id, None)
|
||||
continue
|
||||
# grace window: a just-armed entry may still be filling — don't strip
|
||||
# its bracket before the fill can confirm (belt to the fresh-positions
|
||||
# check above; together they close the naked-stop race)
|
||||
if (now - armed["armed_at"]).total_seconds() < self.settings.entry_grace_seconds:
|
||||
continue
|
||||
|
||||
armed["checks"] += 1
|
||||
reason = None
|
||||
|
||||
Reference in new issue
Block a user