Momentum Research Log — 2026-06-19
A working log of the session: data-integrity fixes, a parameter sweep, a concentration/attribution check, a reframe of how the strategy will be used, the $5M liquidity rule, and a live shortlist webpage. Honest throughout — some of this is a result that says "not a validated edge", not a win.
0. Context: resumed after a dropped connection
Picked the thread back up by inspecting running processes and recently-modified files rather than guessing. The cross-sectional momentum backtest on the new 2,164-name US universe had completed; nothing was corrupted by the disconnect.
1. Data-integrity fix — the in-progress week
The original run's headline numbers leaned on a partial weekly bar dated 2026-06-21 (a Sunday two days in the future). Underlying daily data only ran through Wed 2026-06-17; the backtest treated that stub as a closed week.
- Fix: a guard in
momentum_engine.pydrops any weekly bar whose week-ending date hasn't passed (load_weekly). Last real bar is now 06-14. - Effect of removing the fake week (it had been flattering OOS):
| Period | PF before | PF after | Avg trade before | after |
|---|---|---|---|---|
| OOS 2026 | 0.98 | 0.89 | −0.16% | −0.95% |
| Train | 0.97 | 0.98 | — | — |
Honest read of the default config: profit factor < 1.0 everywhere → no edge after costs. The old +16% OOS headline was a couple of lucky months on top of a losing per-trade average.
2. Operational — DB lock & the cache
big_bots.db kept throwing "database is locked". Cause: exchange_universe.service
(the OHLCV scheduler) holds a write lock; SQLite rollback-journal mode blocks
even read-only readers during writes. No corruption — just contention.
- We stopped only
exchange_universewhile working (not the signal bots, which write tosignals.dband produce live 72h outcome tracking), then restarted it. - Durable fix: backtests now read a local
weekly_cache.pklsnapshot (close + volume, from 2023-06-01 for lookback warm-up). No live-DB contention, and sweeps run fast.momentum_publish.pyrefreshes the cache with lock-retry.
3. Parameter sweep (momentum_sweep.py) — tuned on TRAIN only
225 configs (lookback × skip × top_n × regime gate), ranked by train profit factor; OOS looked at only afterward. Best-on-train shapes were classic "12-1" momentum (≈52w lookback, skip ≈1 month):
| Lookback | Skip | Pos | Gate | Train PF | Train ret | OOS PF | OOS ret |
|---|---|---|---|---|---|---|---|
| 52w | 4 | 5 | off | 3.98 | +636% | 1.78 | +22% |
| 39w | 4 | 5 | off | 3.55 | +566% | 1.29 | +27% |
| 52w | 1 | 8 | breadth>0.6 | 1.92 | +60% | 2.74 | +26% |
138/225 configs beat PF 1.0 on train. But do not trust the absolute numbers
— see §4. Full grid: momentum_sweep_results.csv.
4. Attribution (momentum_attribution.py) — the edge is 2–3 names ⚠️
For the low-drawdown config (52w/skip1/8/breadth>0.6) we attributed the equity curve to individual tickers:
- Train: top 1 name = 35% of net gain; top 3 = 93% (MNPR, RGTI, RGC).
- OOS 2026: top 1 = 38%; top 3 = 91% (RGC, HYMC, SCZM).
The drivers are speculative micro-cap moonshots (quantum, drones, lottery biotech). Conclusion: the result is a handful of explosive winners, not a broad effect. On a survivor-only universe these names are over-represented by construction. Validation cannot correct for this because train and OOS draw from the same survivor universe.
5. Reframe — discretionary use (Jacques' direction)
Jacques: "It is all we have so lets carry on. I am not making an automated tool,
I will still vet every trade myself." So:
- Stop treating survivorship as a disqualifier; note it once, move on.
- The strategy is an idea generator / weekly shortlist, not an automated edge.
- Concentration is useful context for manual vetting (which 1–2 names to size).
- (Saved to memory: discretionary-manual-vetting.)
6. Liquidity rule — $5M/day floor 🔒
Jacques flagged thin volume on some names. Added average daily dollar volume
(ADDV) = mean(close×volume) over 13 weeks / 5. Hard rule: never display any
name with ADDV < $5M/day (saved to memory: liquidity-floor-5m). At that floor
only a couple of names (e.g. BVC at $0.2M) get cut; most top names are liquid.
7. Live webpage 🌐
momentum_publish.py → https://customsmartbots.com/research/momentum_live.html
(pinned on the research index).
- Top 30 momentum names above the $5M floor, regime banner, 13w trend in
green/red, ADDV, last-updated stamp; styled to match existing reports.
- Refresh: cron 02:30 SAST (UTC+2), Tue–Sat runs the publisher the
morning after each US close (cache refresh → recompute → rewrite); Tue covers
Mon's close … Sat covers Fri's. Browser also auto-refreshes hourly.
(Was 23:30 Mon–Fri; moved to 02:30 for a safe post-close margin year-round.)
- Reads the cache, so it never fights the live DB writer.
- Note: page is public (same as existing reports). Can be gated/local-only
on request.
Current state (as of this session)
- Regime: RISK-OFF (~56% breadth) — gated strategy would hold cash.
- Default backtest config: no validated edge after costs.
- Strong-config backtest returns: real but driven by 2–3 survivor-selected names; treated as a discretionary screen, not a system.
8. Restyle — "Groovy Baby" theme (2026-06-20)
Jacques asked to match the live shortlist page to the Austin Powers theme on
the main Big Bots homepage (/var/www/customsmartbots/index.html).
- Reworked the generator (momentum_publish.py) only — the cron regenerates the
page nightly, so styling lives in the source, not the HTML. CSS + a DECOR
block (hypno spiral, rainbow waves, daisies/targets) + bubble-letter HERO
ported from the homepage; same plum/cream palette and Bagel Fat One / Nunito /
Fredoka / Pacifico / Space Mono fonts.
- The data table stays fully legible: background decor runs at low opacity behind
a cream "pop-card", positive/negative cells use a darker green/red that reads on
cream. Regime banner is now a groovy tilted pill (acid=risk-on, orange=risk-off).
- Re-rendered from weekly_cache.pkl (no live-DB touch); page 6.8K→15K, data
unchanged (top names BW, CELC, LWLG… , regime RISK-OFF). Honours prefers-
reduced-motion. Live: customsmartbots.com/research/momentum_live.html
9. Clickable TradingView charts on the live page (2026-06-20)
Jacques wanted to jump straight from a name on the shortlist to its chart. Added a TradingView deep-link on every ticker.
- Exchange resolution. TradingView chart URLs need an exchange prefix
(
NASDAQ:BWvs a bareBW). Builttv_exchange.pyto query TradingView's own symbol-search and return the chart-ready code (NASDAQ / NYSE / AMEX), preferring the primary US listing. Results are cached intv_exchange_map.jsonso the nightly publisher never re-hits the network for a name it already knows; network errors fall back to a bare symbol and never poison the map. - One-off backfill.
build_tv_map.pypre-resolved the whole universe: 2,164 / 2,164 tickers mapped, 0 left bare (log:build_tv_map.log). - Wired into the page.
momentum_publish.pynow wraps each name in a link totradingview.com/chart/?symbol=<EX>:<TICKER>&interval=D(daily candles) and persists any newly-resolved exchanges back to the map on each run. - Verified. Page rewritten 07:32; 30 working chart links (e.g.
NYSE:BW,NASDAQ:CELC). Data unchanged — regime still RISK-OFF, top names BW, CELC, LWLG… Re-rendered fromweekly_cache.pkl, no live-DB touch.
10. Golden-cross dashboard restyle — "Groovy Baby" (2026-06-20)
While we were theming the momentum page, Jacques asked for the daily golden/death-cross dashboard to get the same Austin Powers treatment so the whole research site looks of a piece.
What the dashboard is (pre-existing, unchanged this session): a daily scan of
big_bots.db (ohlcv_snapshots) for SMA50 crossing SMA200 across all
exchanges over a rolling 30-day window — golden cross (50 rises above 200) and
death cross (50 falls below 200) reported separately. Each hit gets a 0–~10
strength score (relative volume + SMA50 slope + the gap between the two
averages). Logic lives in golden_cross.py (built late May, untouched here).
What we changed (purely cosmetic, golden_cross_report.py): ported the same
theme used on the momentum page — plum/cream palette, bubble-letter title, the
Bagel Fat One / Nunito / Fredoka / Pacifico / Space Mono fonts, and the hypno
spiral + wave + decor background at low opacity behind a legible card. No
change to the scan, the scoring, or the data — verified the Jun-20 diff against
the Jun-4 backup (golden_cross_report.bak_*.py) is CSS/markup only. The previous
version is kept as that backup.
How it runs (also pre-existing, confirmed healthy):
- golden_cross_report.timer → fires daily 04:00 SAST (Persistent=true, so
a missed run catches up). Regenerates a self-contained page (data baked in, no
backend) at /root/golden_cross_web/index.html and writes a dated JSON
snapshot to …/archive/YYYY-MM-DD.json.
- golden_cross_web.service serves the folder on :8800.
- Archives are landing daily (latest 2026-06-24.json, today 04:06) — the timer
is working.
Honest note: this was a presentation change only — it doesn't add any signal or research finding. Logged here so the trail matches what's on disk (the section above stopped at 07:32; the restyle landed 09:31 the same morning).
Files
momentum_engine.py (backtest + in-progress-week guard), momentum_sweep.py,
momentum_attribution.py, momentum_now.py (CLI screen + shared compute()),
momentum_publish.py (webpage), weekly_cache.pkl (close+volume cache),
momentum_sweep_results.csv, tv_exchange.py + tv_exchange_map.json +
build_tv_map.py (TradingView chart links). Universe: us_universe_clean.csv
(2,164 names). Golden-cross dashboard: golden_cross.py (scan logic),
golden_cross_report.py (page generator, restyled), golden_cross_web/
(output + dated JSON archive), served by golden_cross_web.service on :8800,
generated daily by golden_cross_report.timer at 04:00 SAST.