feat(018): sub-second exits on a dedicated thread + fast decision pipeline
Exits move off the single-threaded scheduler onto their own daemon thread at 1s (mirrors the fast trigger thread). Dedicated research connection (shared facade isn't thread-safe); scheduler exits step auto-disabled when the thread runs (single-caller invariant — no racing instance state, no double-submit); manage() already idempotent (stop ratchets >1c up only). 3 driver tests + 539 suite green. On fast small-caps this is the 'get out fast' the owner asked for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
375973fe21
commit
5896d291d8
5 files changed
+139
-2
No files matched your search
@@ -0,0 +1,43 @@
|
||||
"""Sub-second exit management on a dedicated daemon thread (plan 018).
|
||||
|
||||
Exits used to run as a step on the single-threaded main scheduler, so real
|
||||
reaction drifted past the interval whenever a slower step (ingest, discover,
|
||||
news) held the thread. On a $2-20 small-cap that round-trips in seconds, that
|
||||
gave back gains. This runs LiveExitManager.manage() on its OWN thread, at a
|
||||
~1s cadence, independent of the scheduler queue — the symmetric twin of the
|
||||
fast trigger thread that already handles entries.
|
||||
|
||||
Two invariants make this safe (audited 2026-07-19):
|
||||
* SINGLE CALLER — the scheduler's "exits" step is removed when the thread
|
||||
runs, so manage() is invoked from exactly one thread; its mutable instance
|
||||
state (_tracked / _naked_passes / _empty_snapshot_streak) never races.
|
||||
* IDEMPOTENT manage() — the stop ratchet only replaces when the desired stop
|
||||
is >1c ABOVE the resting one (exit_manager.py), so a 1s cadence re-submits
|
||||
nothing once a level is set; catastrophe/scale close the position, so the
|
||||
next pass sees it gone. The manager must also get its OWN research DB
|
||||
connection for bars (the shared facade connection is not thread-safe).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_exit_loop(exit_mgr, stop_event, interval: float = 1.0,
|
||||
on_error: Callable[[Exception], None] | None = None) -> None:
|
||||
"""Drive ``exit_mgr.manage()`` every ``interval`` seconds until
|
||||
``stop_event`` is set. Mirrors ``trigger_loop``: waits FIRST (so a
|
||||
pre-set stop returns immediately without a manage call), and a manage()
|
||||
exception is caught — a live exit loop must never die on a broker blip and
|
||||
leave positions unmanaged."""
|
||||
while not stop_event.wait(interval):
|
||||
try:
|
||||
exit_mgr.manage()
|
||||
except Exception as exc: # noqa: BLE001 — one bad pass must not kill exits
|
||||
if on_error is not None:
|
||||
on_error(exc)
|
||||
else:
|
||||
logger.warning("exit pass failed (continuing): %s", exc)
|
||||
Reference in new issue
Block a user