fix: halt-aware flatten — let a working market sell rest instead of churning
A market sell that sits unfilled means the symbol is halted (or session closed); cancelling and resubmitting it every exit-manager pass fills no faster and spams the broker — YXT 2026-08-05: 87 market sells across a 10-minute LULD halt, 86 cancelled, plus one exit_naked_stop journal event every ~4s. On a halt-down the resting sell is also the position's only protection. cancel_protective_and_close now returns False (nothing submitted) when a market sell for the symbol is already working, and only cancels non-market protective sells before closing. Exit-manager paths (catastrophe / naked-stop / first-red / trail) journal the exit only when a close was actually submitted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3871bda616
commit
390d6660ea
3 files changed
+114
-24
No files matched your search
+25
-21
@@ -101,13 +101,15 @@ class LiveExitManager:
|
|||||||
return (c.get("id"), float(sp) if sp else None)
|
return (c.get("id"), float(sp) if sp else None)
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
def _flatten(self, symbol: str, orders) -> None:
|
def _flatten(self, symbol: str, orders) -> bool:
|
||||||
"""Market-close a position, cancelling its resting protective legs first.
|
"""Market-close a position, cancelling its resting protective legs first.
|
||||||
|
|
||||||
Delegates to the shared helper so the held-qty 403 fix lives in exactly
|
Delegates to the shared helper so the held-qty 403 fix lives in exactly
|
||||||
one place (see runtime/flatten.py). ``orders`` is the pass's pre-fetched
|
one place (see runtime/flatten.py). ``orders`` is the pass's pre-fetched
|
||||||
nested snapshot, reused to avoid a redundant fetch."""
|
nested snapshot, reused to avoid a redundant fetch. Returns False when a
|
||||||
cancel_protective_and_close(self.client, symbol, orders=orders)
|
market sell is already resting (halted symbol) — nothing new submitted,
|
||||||
|
so callers should not re-journal the exit."""
|
||||||
|
return cancel_protective_and_close(self.client, symbol, orders=orders)
|
||||||
|
|
||||||
def _market_open(self) -> bool:
|
def _market_open(self) -> bool:
|
||||||
"""Broker clock — is the regular session open? A market FLATTEN can't fill
|
"""Broker clock — is the regular session open? A market FLATTEN can't fill
|
||||||
@@ -235,11 +237,11 @@ class LiveExitManager:
|
|||||||
# re-fetches rather than acting on the sentinel; a true
|
# re-fetches rather than acting on the sentinel; a true
|
||||||
# catastrophe still exits even when stop status is unknowable
|
# catastrophe still exits even when stop status is unknowable
|
||||||
# (a deep-drawdown position is worse than a redundant cancel).
|
# (a deep-drawdown position is worse than a redundant cancel).
|
||||||
self._flatten(sym, orders_for_flatten)
|
if self._flatten(sym, orders_for_flatten):
|
||||||
self._emit(sym, f"CATASTROPHE exit {sym} @~{cur:.4f} "
|
self._emit(sym, f"CATASTROPHE exit {sym} @~{cur:.4f} "
|
||||||
f"({loss_pct:.1f}% below entry)", "exit_catastrophe",
|
f"({loss_pct:.1f}% below entry)", "exit_catastrophe",
|
||||||
{"price": cur, "entry": entry, "stop": cur_stop})
|
{"price": cur, "entry": entry, "stop": cur_stop})
|
||||||
actions.append(f"{sym} CATASTROPHE-exit @{cur:.2f} (-{loss_pct:.0f}%)")
|
actions.append(f"{sym} CATASTROPHE-exit @{cur:.2f} (-{loss_pct:.0f}%)")
|
||||||
elif cur_stop is None:
|
elif cur_stop is None:
|
||||||
# closed + naked: a market flatten can't fill — rest a protective
|
# closed + naked: a market flatten can't fill — rest a protective
|
||||||
# stop below market so it fires at the open instead of churning.
|
# stop below market so it fires at the open instead of churning.
|
||||||
@@ -271,11 +273,13 @@ class LiveExitManager:
|
|||||||
try:
|
try:
|
||||||
if mkt_open:
|
if mkt_open:
|
||||||
# session open: a market flatten fills — the safe exit.
|
# session open: a market flatten fills — the safe exit.
|
||||||
self._flatten(sym, orders)
|
# False = a sell is already resting (halted symbol);
|
||||||
self._emit(sym, f"naked-stop enforced exit {sym} "
|
# don't re-journal every pass (87-order YXT churn).
|
||||||
f"(no live stop, {n} passes)", "exit_naked_stop",
|
if self._flatten(sym, orders):
|
||||||
{"passes": n, "price": cur, "entry": entry})
|
self._emit(sym, f"naked-stop enforced exit {sym} "
|
||||||
actions.append(f"{sym} naked-stop exit (no live stop)")
|
f"(no live stop, {n} passes)", "exit_naked_stop",
|
||||||
|
{"passes": n, "price": cur, "entry": entry})
|
||||||
|
actions.append(f"{sym} naked-stop exit (no live stop)")
|
||||||
else:
|
else:
|
||||||
# closed: a market sell can't fill (it just churns every
|
# closed: a market sell can't fill (it just churns every
|
||||||
# pass) — REST a protective stop so the position survives
|
# pass) — REST a protective stop so the position survives
|
||||||
@@ -361,10 +365,10 @@ class LiveExitManager:
|
|||||||
# (1) first-red: exit the whole position now
|
# (1) first-red: exit the whole position now
|
||||||
if d.exit_now:
|
if d.exit_now:
|
||||||
try:
|
try:
|
||||||
self._flatten(sym, orders)
|
if self._flatten(sym, orders):
|
||||||
self._emit(sym, f"first-red exit {sym} @~mkt", "exit_first_red",
|
self._emit(sym, f"first-red exit {sym} @~mkt", "exit_first_red",
|
||||||
{"reason": d.reason})
|
{"reason": d.reason})
|
||||||
actions.append(f"{sym} first-red exit")
|
actions.append(f"{sym} first-red exit")
|
||||||
self._tracked.pop(sym, None)
|
self._tracked.pop(sym, None)
|
||||||
continue
|
continue
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
@@ -393,10 +397,10 @@ class LiveExitManager:
|
|||||||
# prior swing low) — a sell-stop above market would be
|
# prior swing low) — a sell-stop above market would be
|
||||||
# rejected, so exit at market now (this is the trail firing).
|
# rejected, so exit at market now (this is the trail firing).
|
||||||
try:
|
try:
|
||||||
self._flatten(sym, orders)
|
if self._flatten(sym, orders):
|
||||||
self._emit(sym, f"trail breached, exit {sym} @~{last_px:.2f}",
|
self._emit(sym, f"trail breached, exit {sym} @~{last_px:.2f}",
|
||||||
"exit_trail", {"trail": d.desired_stop, "price": last_px})
|
"exit_trail", {"trail": d.desired_stop, "price": last_px})
|
||||||
actions.append(f"{sym} trail-exit @{last_px:.2f}")
|
actions.append(f"{sym} trail-exit @{last_px:.2f}")
|
||||||
self._tracked.pop(sym, None)
|
self._tracked.pop(sym, None)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
logger.warning("trail exit failed %s: %s", sym, exc)
|
logger.warning("trail exit failed %s: %s", sym, exc)
|
||||||
|
|||||||
+25
-3
@@ -95,9 +95,12 @@ RESERVING_STATES = frozenset({
|
|||||||
|
|
||||||
|
|
||||||
def cancel_protective_and_close(client, symbol: str, orders=None,
|
def cancel_protective_and_close(client, symbol: str, orders=None,
|
||||||
retries: int = 4) -> None:
|
retries: int = 4) -> bool:
|
||||||
"""Cancel ``symbol``'s resting protective sell orders, then market-close it.
|
"""Cancel ``symbol``'s resting protective sell orders, then market-close it.
|
||||||
|
|
||||||
|
Returns True when a close was submitted this call, False when a market sell
|
||||||
|
was already working (liquidation in flight — e.g. resting through a halt).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: broker client exposing ``cancel_order``, ``close_position`` and
|
client: broker client exposing ``cancel_order``, ``close_position`` and
|
||||||
(optionally) ``get_orders(status, nested, symbols)``.
|
(optionally) ``get_orders(status, nested, symbols)``.
|
||||||
@@ -112,16 +115,35 @@ def cancel_protective_and_close(client, symbol: str, orders=None,
|
|||||||
the last close exception if every attempt fails (callers collect, not
|
the last close exception if every attempt fails (callers collect, not
|
||||||
crash — one bad symbol must not block flattening the rest of the book).
|
crash — one bad symbol must not block flattening the rest of the book).
|
||||||
"""
|
"""
|
||||||
# 1) free the reserved shares: cancel the symbol's working sell legs
|
|
||||||
if orders is None and hasattr(client, "get_orders"):
|
if orders is None and hasattr(client, "get_orders"):
|
||||||
try:
|
try:
|
||||||
orders = client.get_orders(status="open", nested=True, symbols=[symbol])
|
orders = client.get_orders(status="open", nested=True, symbols=[symbol])
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
orders = None
|
orders = None
|
||||||
|
|
||||||
|
# 0) a working MARKET sell means liquidation is already in flight — let it
|
||||||
|
# rest. A market sell only sits unfilled while the symbol is HALTED (or the
|
||||||
|
# session closed); cancelling and resubmitting it every pass fills no faster
|
||||||
|
# and churns the broker (YXT 2026-08-05: 87 market sells over a 10-minute
|
||||||
|
# LULD halt, 86 cancelled). On a halt-down, the resting sell is also the
|
||||||
|
# position's only protection — the one order that fills at the reopen.
|
||||||
for parent in (orders or []):
|
for parent in (orders or []):
|
||||||
for o in [parent, *(parent.get("legs") or [])]:
|
for o in [parent, *(parent.get("legs") or [])]:
|
||||||
sym = o.get("symbol") or parent.get("symbol")
|
sym = o.get("symbol") or parent.get("symbol")
|
||||||
if (sym == symbol and o.get("side") == "sell"
|
if (sym == symbol and o.get("side") == "sell"
|
||||||
|
and o.get("type") == "market"
|
||||||
|
and o.get("status") in RESERVING_STATES):
|
||||||
|
logger.info("flatten %s: market sell %s already working — "
|
||||||
|
"resting through the halt", symbol, o.get("id"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 1) free the reserved shares: cancel the symbol's working PROTECTIVE sell
|
||||||
|
# legs (stop/limit — never a market sell, that's the liquidation itself)
|
||||||
|
for parent in (orders or []):
|
||||||
|
for o in [parent, *(parent.get("legs") or [])]:
|
||||||
|
sym = o.get("symbol") or parent.get("symbol")
|
||||||
|
if (sym == symbol and o.get("side") == "sell"
|
||||||
|
and o.get("type") != "market"
|
||||||
and o.get("status") in RESERVING_STATES and o.get("id")):
|
and o.get("status") in RESERVING_STATES and o.get("id")):
|
||||||
try:
|
try:
|
||||||
client.cancel_order(o["id"])
|
client.cancel_order(o["id"])
|
||||||
@@ -133,7 +155,7 @@ def cancel_protective_and_close(client, symbol: str, orders=None,
|
|||||||
for attempt in range(retries):
|
for attempt in range(retries):
|
||||||
try:
|
try:
|
||||||
client.close_position(symbol)
|
client.close_position(symbol)
|
||||||
return
|
return True
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
last_exc = exc
|
last_exc = exc
|
||||||
time.sleep(0.3 * (attempt + 1))
|
time.sleep(0.3 * (attempt + 1))
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""cancel_protective_and_close: halt-aware liquidation (the YXT 2026-08-05
|
||||||
|
churn — 87 market sells through a 10-minute LULD halt, 86 cancelled)."""
|
||||||
|
|
||||||
|
from runtime.flatten import cancel_protective_and_close
|
||||||
|
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.cancelled: list[str] = []
|
||||||
|
self.closed: list[str] = []
|
||||||
|
|
||||||
|
def cancel_order(self, order_id):
|
||||||
|
self.cancelled.append(order_id)
|
||||||
|
|
||||||
|
def close_position(self, symbol, qty=None):
|
||||||
|
self.closed.append(symbol)
|
||||||
|
|
||||||
|
|
||||||
|
def _order(**kw):
|
||||||
|
base = {"symbol": "YXT", "side": "sell", "type": "market",
|
||||||
|
"status": "accepted", "id": "o1", "legs": []}
|
||||||
|
base.update(kw)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def test_resting_market_sell_is_left_alone():
|
||||||
|
client = FakeClient()
|
||||||
|
submitted = cancel_protective_and_close(client, "YXT", orders=[_order()])
|
||||||
|
assert submitted is False
|
||||||
|
assert client.cancelled == []
|
||||||
|
assert client.closed == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_protective_stop_is_cancelled_then_closed():
|
||||||
|
client = FakeClient()
|
||||||
|
submitted = cancel_protective_and_close(
|
||||||
|
client, "YXT", orders=[_order(type="stop", id="stop1")])
|
||||||
|
assert submitted is True
|
||||||
|
assert client.cancelled == ["stop1"]
|
||||||
|
assert client.closed == ["YXT"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_orders_just_closes():
|
||||||
|
client = FakeClient()
|
||||||
|
assert cancel_protective_and_close(client, "YXT", orders=[]) is True
|
||||||
|
assert client.closed == ["YXT"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dead_market_sell_does_not_block_close():
|
||||||
|
# a filled/cancelled market sell no longer reserves shares — must not be
|
||||||
|
# mistaken for an in-flight liquidation
|
||||||
|
client = FakeClient()
|
||||||
|
submitted = cancel_protective_and_close(
|
||||||
|
client, "YXT", orders=[_order(status="canceled")])
|
||||||
|
assert submitted is True
|
||||||
|
assert client.closed == ["YXT"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_other_symbols_market_sell_ignored():
|
||||||
|
client = FakeClient()
|
||||||
|
submitted = cancel_protective_and_close(
|
||||||
|
client, "YXT", orders=[_order(symbol="SWIM")])
|
||||||
|
assert submitted is True
|
||||||
|
assert client.closed == ["YXT"]
|
||||||
Reference in new issue
Block a user