"""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)