perf: stop the watcher scoring names that can never trade
criteria_evaluated + signal_blocked held 3.1 GB of a 7.6 GB events table.
On 2026-09-10 alone the watcher wrote 191,458 criteria receipts across 57
symbols -- ~3,359 per symbol per day -- and 154,260 of them (80.6%) were
for names whose static open gap was outside the tradeable band. NOK, NIO,
PATH, PLUG, SOFI, AAL and F all sit inside the $2-20 price filter, so
WATCHER_PRICE_MIN/MAX admits them; only the gap floor excludes them, and
that was checked per-tick instead of once up front.
Apply the SHARED population gate (strategy/population.population_reject --
the same function, same config the entry path already enforces) before the
evaluation and its receipts. This is behaviour-preserving for trading: those
names were being scored, emitted, and then rejected downstream anyway --
429 "outside tradeable population" risk events across 221 symbols, led by
exactly the names that top the receipt counts. The decision just moves
upstream of the write.
Details that matter:
- The verdict is recomputed each tick, never cached. A >150% gapper reads
"squeeze unverified" on thin volume and is admitted once rvol confirms;
a cached rejection would bench it for the session.
- _transition only emits on CHANGE, so a rejected symbol costs one
state event per session and is then silent.
- Fail-open throughout: an unreadable open, a missing previous close or
absent volume returns None, so a data gap can never bench a symbol.
- Gap is the STATIC OPEN gap (session open vs previous close), fixed for
the day, not the live price -- the verdict must not wander intraday.
Off by default (population=None) so every existing caller and test keeps
its current behaviour; the live loop opts in, and WATCHER_POPULATION_GATE=0
restores the old path.
Note for diagnostics: momentum_cli's criteria views now show only names
inside the population. That is the intended trade -- the excluded rows
described symbols the bot could never have taken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
This commit is contained in:
1 parent
9034839661
commit
97a3a1b29d
4 files changed
+236
-3
No files matched your search
@@ -65,6 +65,7 @@ from runtime.market_calendar import (
|
||||
)
|
||||
from runtime.triggers import ArmedTriggerBook
|
||||
from runtime.watcher import Watcher, WatcherConfig
|
||||
from strategy.population import PopulationConfig
|
||||
from strategy.exits import ExitConfig
|
||||
from storage.event_schema import EventMode, ModuleTickEvent
|
||||
from storage.event_store import EventStore
|
||||
@@ -412,6 +413,11 @@ def build_runtime(args: argparse.Namespace) -> dict:
|
||||
ready_score_pct=float(os.environ.get(
|
||||
"WATCHER_READY_SCORE", str(_learned.get("ready_score_pct", 60.0)))),
|
||||
min_quality=float(os.environ.get("WATCHER_MIN_QUALITY", "0.30")),
|
||||
# Same population gate the entry path enforces, applied before the
|
||||
# watcher scores anything — so it stops writing criteria receipts for
|
||||
# names that can never trade. Opt out with WATCHER_POPULATION_GATE=0.
|
||||
population=(PopulationConfig.from_env()
|
||||
if _flag("WATCHER_POPULATION_GATE", "1") else None),
|
||||
),
|
||||
catalyst_score_provider=(
|
||||
catalyst_score_provider if ollama_cfg.catalyst_score_enabled else None
|
||||
|
||||
Reference in new issue
Block a user