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
+32
-2
@@ -1085,9 +1085,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||
# --- live exit management (breakeven / trail / scale / first-red) -------
|
||||
exit_cfg = ExitConfig.from_env()
|
||||
|
||||
# Exits run on their OWN thread (plan 018) with a DEDICATED research
|
||||
# connection — the shared rt["research_con"] is not thread-safe, and the
|
||||
# exit loop reads bars concurrently with the scheduler. Open it lazily so a
|
||||
# DB hiccup at boot doesn't stop the loop; None falls back to the shared
|
||||
# connection (still correct, just not concurrency-isolated).
|
||||
_exit_con: dict = {"c": None}
|
||||
|
||||
def _rth_bars(sym):
|
||||
from research import query as rq
|
||||
b = rq.query_minute_bars(rt["research_con"], sym, _now_session_date())
|
||||
con = _exit_con["c"] or rt["research_con"]
|
||||
b = rq.query_minute_bars(con, sym, _now_session_date())
|
||||
if b is not None and len(b) and "is_regular_hours" in b.columns:
|
||||
b = b[b["is_regular_hours"] == True].reset_index(drop=True) # noqa: E712
|
||||
return b
|
||||
@@ -1291,8 +1299,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
scheduler.add("command", step_commands,
|
||||
float(os.environ.get("OPERATOR_COMMAND_INTERVAL_SECONDS", "5")),
|
||||
enabled=(rt["execution"] is not None))
|
||||
# SINGLE-CALLER INVARIANT (plan 018): the scheduler exits step runs ONLY when
|
||||
# the dedicated exit thread is OFF — otherwise manage() would be called from
|
||||
# two threads (racing instance state + double-submit). Thread default ON.
|
||||
scheduler.add("exits", step_manage_exits, float(os.environ.get("TRADING_EXIT_MANAGE_INTERVAL_SECONDS", "12")),
|
||||
enabled=_flag("TRADING_EXECUTION_ENABLED", "1"))
|
||||
enabled=(_flag("TRADING_EXECUTION_ENABLED", "1")
|
||||
and not _flag("TRADING_EXIT_THREAD_ENABLED", "1")))
|
||||
scheduler.add("eod", step_eod_flatten, float(os.environ.get("TRADING_EOD_INTERVAL_SECONDS", "30")),
|
||||
enabled=_flag("TRADING_EXECUTION_ENABLED", "1"))
|
||||
scheduler.add("notify", step_notify, float(os.environ.get("TELEGRAM_NOTIFY_INTERVAL_SECONDS", "45")),
|
||||
@@ -1339,6 +1351,24 @@ def main(argv: list[str] | None = None) -> int:
|
||||
trigger_thread = threading.Thread(target=trigger_loop, name="trigger", daemon=True)
|
||||
trigger_thread.start()
|
||||
print(f"[boot] fast trigger thread started (every {trigger_interval:.0f}s)")
|
||||
# exits on their OWN thread (plan 018): sub-second reaction, independent of
|
||||
# the scheduler queue. Gated so the slow scheduler "exits" step is disabled
|
||||
# whenever this runs (single-caller invariant — see below).
|
||||
_exit_thread_on = (_flag("TRADING_EXIT_THREAD_ENABLED", "1")
|
||||
and exit_mgr is not None and client is not None)
|
||||
if _exit_thread_on:
|
||||
from runtime.exit_thread import run_exit_loop
|
||||
_exit_interval = float(os.environ.get("TRADING_EXIT_THREAD_INTERVAL_SECONDS", "1.0"))
|
||||
|
||||
def exit_loop():
|
||||
try: # dedicated connection so bars reads don't race the scheduler
|
||||
_exit_con["c"] = open_research_db("market")
|
||||
except Exception as exc: # noqa: BLE001 — fall back to shared con
|
||||
print(f"[exit-thread] dedicated DB unavailable, using shared: {exc}", flush=True)
|
||||
run_exit_loop(exit_mgr, stop_event, _exit_interval)
|
||||
|
||||
threading.Thread(target=exit_loop, name="exits", daemon=True).start()
|
||||
print(f"[boot] exit thread started (every {_exit_interval:.1f}s) — scheduler exits step OFF")
|
||||
# news enrichment on its OWN thread (never blocks the scheduler's trade steps)
|
||||
if ollama_cfg.enabled and ollama_cfg.enrichment_enabled:
|
||||
threading.Thread(target=enrich_loop, name="enrich", daemon=True).start()
|
||||
|
||||
Reference in new issue
Block a user