feat(019): discovery on its own thread + tuner heartbeat (honest stale alert)
Telemetry: discovery took 6-30s ON the single scheduler, freezing ingest/arm so the trigger read stale bars — the real 'late edge detection'. Now discovery runs on its own daemon thread with a dedicated DB connection; the watchlist it mutates is a lock-guarded Watchlist (list-like drop-in, no torn reads); the scheduler discover step auto-disables (single-writer). Loop is now 3 fast threads (entries/exits/discovery) + slow scheduler for tolerant work. Also: nightly_tune writes a heartbeat every run, so the boot alert distinguishes 'tuner not running' (real) from 'ran, no profitable ORB config, kept params' (expected — the strategy has no edge). Kills the misleading Telegram alarm. 543 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a619a99f05
commit
b3a47d775d
7 files changed
+214
-21
No files matched your search
@@ -0,0 +1,68 @@
|
||||
"""Thread-safe watchlist (plan 019).
|
||||
|
||||
Discovery runs on its own thread (so its 6-30s pass never blocks ingest/arm)
|
||||
and mutates the live watchlist while the scheduler's ingest/arm/watch steps
|
||||
iterate it. A bare list mutated across threads yields torn reads / "list
|
||||
changed during iteration". This is a small lock-guarded, list-like drop-in:
|
||||
every read (len, ``in``, iteration, indexing/slicing) takes a consistent
|
||||
snapshot under the lock; mutations (append/extend/trim) hold the lock. Order
|
||||
is preserved and adds dedupe (a name already watched isn't re-added)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
class Watchlist:
|
||||
def __init__(self, initial: Iterable[str] | None = None):
|
||||
self._lock = threading.Lock()
|
||||
self._items: list[str] = []
|
||||
if initial:
|
||||
self.extend(initial)
|
||||
|
||||
# -- reads (each snapshots under the lock) ----------------------------
|
||||
def snapshot(self) -> tuple[str, ...]:
|
||||
with self._lock:
|
||||
return tuple(self._items)
|
||||
|
||||
def __len__(self) -> int:
|
||||
with self._lock:
|
||||
return len(self._items)
|
||||
|
||||
def __contains__(self, item) -> bool:
|
||||
with self._lock:
|
||||
return item in self._items
|
||||
|
||||
def __iter__(self):
|
||||
# iterate a snapshot copy so a concurrent mutation can't tear the loop
|
||||
return iter(self.snapshot())
|
||||
|
||||
def __getitem__(self, idx):
|
||||
with self._lock:
|
||||
return self._items[idx] # supports symbols[:N] slicing (arm)
|
||||
|
||||
# -- mutations (locked, dedupe-preserving-order) ----------------------
|
||||
def append(self, name: str) -> None:
|
||||
with self._lock:
|
||||
if name not in self._items:
|
||||
self._items.append(name)
|
||||
|
||||
def extend(self, names: Iterable[str]) -> None:
|
||||
with self._lock:
|
||||
seen = set(self._items)
|
||||
for n in names:
|
||||
if n not in seen:
|
||||
self._items.append(n)
|
||||
seen.add(n)
|
||||
|
||||
def trim(self, max_len: int) -> None:
|
||||
"""Bound to the most-recent ``max_len`` names (drops oldest)."""
|
||||
with self._lock:
|
||||
if len(self._items) > max_len:
|
||||
del self._items[: len(self._items) - max_len]
|
||||
|
||||
def replace(self, names: Iterable[str]) -> None:
|
||||
"""Atomically swap the whole list (boot-time universe resolution)."""
|
||||
with self._lock:
|
||||
self._items = list(dict.fromkeys(names))
|
||||
Reference in new issue
Block a user