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
Deploying Momentum
Three supported shapes, one image/codebase. Pick by where it runs:
| Mode | Status | Use when | Docs |
|---|---|---|---|
| Docker Compose | ✅ canonical (production since 2026-07-09) | Any always-on Linux host; TrueNAS SCALE | docker/README.md, docker-compose.yml (root), docker/truenas-compose.yml |
| Local (systemd) | ✅ maintained | Development machine, WSL, bare metal without Docker | systemd/README.md + unit templates in deploy/systemd/ |
| Helm / Kubernetes | 🚧 planned refresh | Future k8s hosting | helm/ — the chart predates the 2026-07 architecture; starting point, not deployable |
Docker Compose (canonical)
The root docker-compose.yml runs the full stack self-contained: postgres 18,
trading loop, dashboard (:8765), admin panel (:8767), telegram bot, transcripts
pipeline, nightly scheduler, on-demand jobs, optional Grafana. The image is
published to docker.io/fez5587/momentum-app and ghcr.io/fez5587/momentum-app
by CI on every master push.
For TrueNAS SCALE use docker/truenas-compose.yml (pull-only, dataset host
paths, inline env). Read the gotchas in docker/README.md before editing the
YAML — anchor expansion, per-service env, and path case-sensitivity have each
caused an outage.
Local (systemd user services)
The original deployment: unit templates in deploy/systemd/ running a git
checkout + venv, config from .env. Still the best loop for development —
tests, backtests, and research scripts run here. Keep the units disabled
while the compose stack owns production: two loops against one Alpaca account
create duplicate orders.
Helm (future)
deploy/helm/ holds the original chart (single app deployment + CronJobs). It
predates the service split, the session gate, and the env-flag inventory; the
refresh is tracked in GitHub issues. Until then, compose is the way.
Config in all modes
.env.example(committed, sanitized) is the full variable inventory and says which layer each belongs in.- Behavior flags that MUST be set explicitly in any deployment — silent
defaults bite:
TRADING_AUTO_APPROVE=1(else entries queue for manual approval),ALPACA_DATA_FEED=sip,OLLAMA_ENABLED=true(catalyst engine),FMP_API_KEY(float cache).TRADING_SESSION_ONLYdefaults ON in deployed configs (bars after-hours entries). - Runtime overrides: the admin panel writes
data/overrides.env, which every service loads over its baked env at boot (runtime/env_overrides.py).