100 Commits
Author SHA1 Message Date
momentum devandClaude Fable 5.1 731e5a5818 chore: close the news-scorer deviation in the live test freeze as adopted baseline
The reactive-coverage cap (3e523e1) has run unchanged since it went live at
10:10 ET on 2026-10-02, so it is part of the strategy under test rather than an
open deviation; only TNON that morning traded without it. Dated closed with a
resolution note so the daily report stops listing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXxioW7HA18AY1gqnzA2HN
2026-10-10 10:04:04 -05:00
momentum devandClaude Fable 5.1 b5b38ab798 fix: scorecard counts round trips the broker filled but the journal never closed (TNON)
On 2026-10-02 the live bot bought 7 TNON at 4.62 and the Tradier sell-stop filled at
4.30 forty-nine seconds later, before a position record existed, so no position_closed
event was written and the 80-trade scorecard silently dropped a -1R loser. The daily
equity-vs-P&L gap ($2.31 that day, cents everywhere else) is what exposed it.

load_trades now merges the journal with a cross-check of the broker's filled orders
(account_orders_updated snapshots, one row per broker order id). For any session day and
symbol with a filled buy and sell but no journaled close, reconstruct_missing synthesises
the trade from the fills: fill VWAPs for entry/exit, the sell-stop's stop price for R.
Such trades are labelled "[broker fill; journal missed]" and the daily report alerts on a
same-day miss. The paper side of the exit A/B skips the check: that store's order
snapshots run to 2.2 GB over the window and its Alpaca journal is reconciled by
alpaca_paper/sync.

Corrected test state: 16/80, mean R -0.08, realized +$0.91 (was 15/80, -0.02, +$3.15).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXxioW7HA18AY1gqnzA2HN
2026-10-08 12:58:53 -05:00
momentum devandClaude Opus 5.5 b3f023f6e2 feat: nightly off-box backup of the live DB, and phone alerts from the daily report
The live (Tradier) stack has no backup service. Only the paper stack backs up its
own DB, so the 80-trade test's whole record lived only on the TrueNAS pool.
- deploy/backup_live_db.py, run by Task Scheduler "Momentum\LiveDbBackup" at 20:40 ET
  nightly: pg_dump -Fc (PostgreSQL 18.6 client) of the live DB to
  C:\Users\Admin\momentum-backups. It is verified with pg_restore --list (the events
  and config_overrides data must be present), keeps the newest 7, and writes
  status.json. A broken dump is deleted rather than kept.
- research/daily_report.py alerts when the last backup failed or is more than 36 h
  old. When there are alerts it sends them to Telegram, using the stack's own sender
  and the credentials in .env. --no-telegram suppresses that for dry runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-02 21:46:05 -05:00
momentum devandClaude Opus 5.5 ac92280b9a fix: daily report no longer flags a stack as silent when run mid-session
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-02 11:11:27 -05:00
momentum devandClaude Opus 5.5 1101e0e3fe chore: record the news-scorer reactive-coverage cap in the live test deviations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-02 09:17:35 -05:00
momentum devandClaude Opus 5.5 3e523e132a fix: reactive coverage (halt notices, "why is X up") no longer scores as a catalyst
qwen3:14b rated "halted on circuit breaker to the upside, stock now up 172%" at
conviction 0.8 / sentiment +1.0, a bigger rank boost than most real news. So
names that had already run were ranked up.
- Prompt: halt notices, movers roundups and articles that only report a price move
  are not a new catalyst (conviction <= 0.1). If one names a new announcement,
  classify it with conviction <= 0.3.
- A deterministic cap that does not depend on the model: halt type or halt wording
  caps conviction at 0.1; after-the-fact wording ("why is X stock up", "X shares
  surge", "stocks moving") caps it at 0.3. Dilution flags are never touched.
On the 4,046-headline news history the pattern catches 100% of halt notices and 82%
of roundups, 0% of analyst items and 0.8% of earnings items. Against qwen3:14b on a
fixed 20-headline set: reactive mean boost 0.318 -> 0.055 (max 0.80 -> 0.21); real
catalysts 0.353 -> 0.362; offerings 3/3 still flagged dilutive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 21:53:42 -05:00
momentum devandClaude Opus 5.5 082d01e8bd chore: close the news-scoring outage in the live test record (qwen3:14b from 2026-10-01 22:37 ET)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 21:40:30 -05:00
momentum devandClaude Opus 5.5 6189885aed chore: record the qwen3:14b-on-mason restore plan for the live news scorer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 21:06:56 -05:00
momentum devandClaude Opus 5.5 4361f80e1b fix: drop unused import that failed the CI ruff gate (blocked image builds)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 21:00:44 -05:00
momentum devandClaude Opus 5.5 eed4ec0b62 fix: catalyst classifier works with qwen3 (OLLAMA_THINK) and no longer hides an empty answer
qwen3 thinks by default. With format=json, Ollama then returns "{}", which the parser
silently read as a confident "other" (sentiment 0, conviction 0), so every headline
scored zero with no error. Two changes:
- OLLAMA_THINK (true/false). It is sent as Ollama's "think" flag only when set, so other
  models are untouched. qwen3:14b needs OLLAMA_THINK=false.
- A JSON object with none of the expected keys is now a failure (None, retried
  next pass), not "other".
Checked through the live code path against qwen3:14b on mason (think=false): 1.4 s per
headline. 5/5 offerings (including an ATM sales agreement and a PIPE) flagged
dilutive. 'Stocks moving' roundups scored no catalyst. Earnings, guidance, analyst,
FDA and contract items were labelled correctly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 20:59:32 -05:00
momentum devandClaude Opus 5.5 b38edd4ca9 feat: daily after-close report and the judge paper-trial criteria
research/daily_report.py, run by Task Scheduler "Momentum\DailyReport" at 16:25 ET
on weekdays, writes data/reports/daily_<date>.md with ALERTS on top:
- the live opening-range bot's frozen 80-trade test: verdict, mean R with CI,
  P&L, equity headroom over the floor, today's trades, open deviations, settings
  drift;
- the judge v2 paper trial: judge.review for every counted day, cumulative
  paper-vs-sim execution parity, today's flagged trades, trader errors and
  restarts;
- the old-vs-new exit A/B (alerts at 50 pairs);
- stack health: down at the open, silent gaps of 10+ min, the press-release
  recorder.
Read-only everywhere (PG_SKIP_SCHEMA=1, SELECTs only). Replayed on 2026-09-30 it
flags that day's real problems: the judge error loop, the paper stack down until
13:51, and a 25-min gap.

judge/PAPER_TRIAL.md, written before any counted trade: trades count from
2026-10-02. At 60 trades the paper trial must show execution parity with the
simulator (paper minus sim >= -0.15R) before a 150-trade performance read. Real
money is only considered after that, and only with the owner's go-ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 14:37:18 -05:00
momentum devandClaude Opus 5.5 3e3388c7fd chore: note the TrueNAS Ollama outage as a known deviation in the live test
The host ran out of memory (843 MiB free; Ollama held 7.8 GB at a 131k context)
and the owner turned ollama2 off on 2026-09-30 evening. Strategy settings are
unchanged, but the LLM catalyst score is inert while it is off, and the
dilution veto falls back to its regex floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-09-30 19:06:02 -05:00
momentum devandClaude Opus 5.5 1cc15f5ea7 chore: restart the live test on 2026-09-29 with the squeeze exception off
TRADING_GAP_SQUEEZE_ENABLED=0 from the next session. The >=150% gap names lost
in both halves of the Jun-Sep history (-0.21R / -0.30R) and in our real fills,
while the 10-35% band made money. 2026-09-28's four trades ran with the
exception on, so they are recorded as a previous period and not counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
2026-09-28 16:33:16 -05:00
momentum devandClaude Opus 5.5 efde7c721d fix: one invalid symbol can no longer stop all live bar ingestion
At 09:34 ET on 2026-09-28 Alpaca's movers screener returned 'TSX:STC', a
Toronto listing. The discovery filter only dropped symbols containing a dot,
so it joined the universe. The live ingest fetches bars for every symbol in
one batched request, and Alpaca rejects the WHOLE batch with 400 "invalid
symbol". From 09:35 every ingest pass stored 0 rows and the loop ran on
stale bars ("INGEST STALE: freshest bar 458s old").

- Discovery keeps only plain 1-5 letter US tickers from both screeners.
- The ingest never sends a non-US or invalid symbol. If Alpaca still rejects
  one ("invalid symbol: X"), it drops X and retries the rest (up to 3 times),
  so one bad name costs one symbol, not the session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
2026-09-28 08:48:51 -05:00
momentum devandClaude Opus 5.5 990ee89b19 chore: restart the live test on 2026-09-28 with the deployed fixes
The 2026-09-24 period ran on settings the 5372585 deploy changed: live volume
was inflated 80-160x, squeezes were verified on a mean volume baseline, and
the first ingest began at ~09:15. It also had no premarket scan and no
fail-fast exit (now 60s/0.3R). Trades from before the restart would mix two
systems, so the 80-trade count starts again on the first session under the
new build. The four earlier trades are kept in the freeze file for the record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
2026-09-27 19:46:50 -05:00
momentum devandClaude Opus 5.5 dff21bae29 Merge: whole-market premarket squeeze scanner (snapshot scan)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:13:11 -05:00
momentum devandClaude Opus 5.5 1cead3f29b feat: whole-market premarket squeeze scan via Alpaca snapshots
Discovery finds premarket squeezes far too late. Across 53 trades by a
professional small-cap momentum trader, our scanner had found only 4 of his
names before his entry, and 27 of his 28 entries were premarket (07:00-09:30
ET). KITT on 2026-09-25 ran $3.50 -> $8.39 at 09:00-09:01 ET (prior close
$0.49); the loop first "Discovered KITT via manual" at 09:35. The existing
sources (Alpaca movers / most-actives screeners) only reflect the regular
session, so a premarket squeeze is invisible until the open.

research/ingestion/snapshot_scan.py sweeps every active, tradable, listed US
equity (~11.9k names; OTC, warrants/units/rights and DISCOVER_EXCLUDE dropped;
asset list cached per ET day) through GET /v2/stocks/snapshots in 2,000-symbol
batches. The endpoint is bound by URL length rather than symbol count (8,000
worked; ~12.7k returned 414). A name qualifies when:
  - the LATEST TRADE price is inside WATCHER_PRICE_MIN/MAX (never the prior
    close, which is routinely < $1 on these names),
  - % change vs the prior close >= DISCOVER_SNAPSHOT_MIN_PCT (20),
  - it is trading now: last trade <= 180s old, latest minute bar <= 300s old
    with >= 5,000 shares, and >= 100,000 shares since 04:00 ET (summed from
    minute bars for the short list, one extra request).
Qualifiers are ranked by % change, capped per sweep, and announced once per
day. dailyBar OHLC ignores extended-hours prints (KITT's dailyBar high was
$3.24 against a premarket high of $8.39), so in premarket it can still be
yesterday's bar. The prior close is prevDailyBar.c only when dailyBar is dated
today, otherwise dailyBar.c.

run_live_paper.py runs it on its own daemon thread (04:00-16:00 ET weekdays,
every 45s), using the plan-019 pattern: its own DB connection, the thread-safe
Watchlist and the generic interval driver. Each new name emits
symbol_discovered with source="snapshot_scan" and price/prev_close/pct/minute
and cumulative volume in symbol_data, so first-sighting latency can be
measured. It gets a daily-history backfill and joins the watchlist, so the
normal new-symbol ingest path gives it its first full-session pull. This is
discovery only; no trading gate changes.

Gated by DISCOVER_SNAPSHOT_SCAN_ENABLED (default 1); knobs in .env.example.
Live read-only sweep at 13:10 ET: 11,871 names in 8 requests / 6.3s cold
(7 requests / 3.5s warm); top hits KITT +376%, MSGY +262%, INLF +60%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:11:45 -05:00
momentum devandClaude Opus 5.5 81e505b845 fix: verify squeezes on a median volume baseline and ingest from the premarket
KITT on 2026-09-25 (+557%, 9.3M shares, the day's biggest mover) was benched
as "gap 557% squeeze unverified (rvol 1.54)". Two separate faults:

1. The rvol baseline was the 20-day MEAN, and two 11-14M-share days on
   09-01/09-02 put it at 1.66M against a 333k median. Squeeze verification
   (gap >= 150% needs rvol >= 5) now takes the stronger of the mean-baseline
   reading and the median-baseline one. Both are cumulative volume over a
   full day's baseline, not prorated, so they share the units the rvol >= 5
   boundary was calibrated in. A reverse-split fake still reads ~0.01 on
   both. This applies in the watcher pre-filter, the trigger book's squeeze
   exception and the fast-path population gate. The ranking and the tod-rvol
   arm signal are unchanged.

2. A name's first ingest reached back only to ~09:15 (09:30 + 15 min, or
   the 20-minute refresh floor). KITT was found at 09:35, so its stored day
   began at 09:13 and missed the 09:00 squeeze and ~6M premarket shares.
   The first pass now covers the session from 04:00 ET.

With both fixed, KITT's volume at 09:35 is ~20x its median day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:08:13 -05:00
momentum devandClaude Opus 5.5 7f1ebe9cab fix: read the data feed through the Tradier wrapper — live volume was 80-160x
With BROKER=tradier the loop's `client` is a TradierClient that delegates
market data to the Alpaca client in `data_client`. Two places read the feed
as getattr(client.settings, "feed", "iex"). TradierSettings has no feed, so
both read "iex" even though the container runs ALPACA_DATA_FEED=sip (verified
in the running loop and in config_overrides):

  - step_vratio kept computing IEX->SIP ratios as if on the free feed
  - ingest_live_minute_bars multiplied the real SIP volume by those ratios

Checked against the paper stack's SIP bars for the same minutes: prices are
identical and unscaled volume matches 1.00x, but scaled rows run x83 (GLND),
x117-126 (GRML) and x163 (SKYQ). The 2026-09-25 GRML signal logged a
cum_volume of 1.39 billion shares and an rvol of 64.6. For any name with a
ratio, every volume gate — rvol, the 300k day-volume floor, min dollar
volume, the 2.5%-of-volume liquidity cap, and the gap x rvol ranking — has
been reading inflated numbers since the stack went live on 2026-07-31.

data_feed() reads through data_client when present. The regression test
wraps a SIP client in a TradierClient and fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:14:11 -05:00
momentum devandClaude Opus 5.5 e2e9687c45 feat: live-test scorecard and frozen strategy settings
Pre-registers the real-money test that starts 2026-09-24, the first session
without commission drag. PASS needs at least 80 round trips with a positive
mean R and positive P&L. FAIL is any session the equity floor halts. Changing
any strategy setting in live_test_freeze.json restarts the count, and the
scorecard reports that drift.

The settings were frozen, not tuned, because a study of 239 real fills
re-simulated under the current exit found nothing worth changing:
  - Capping how far above the trigger an entry may fill lost R at every cap
    from 0.25% to 10%, with either skip or resting-limit handling. Fills more
    than 5% above the trigger were the best bucket (+0.66R).
  - No break-time factor (spread, price, VWAP distance, stop width, L1
    bid/ask size, premarket range) kept its sign across both date halves and
    both accounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:53:39 -05:00
momentum devandClaude Fable 5 40e51c8dd5 feat: morning pick journal — prospective human/LLM forecast experiment
Tests whether a PROSPECTIVE forecaster (owner or local LLM) carries signal
the machine levers never found. Picks land premarket via dashboard panel or
research/llm_picker.py (Ollama); research/pick_scoring.py judges them under
a pre-registered bar (n>=40, >=30 sessions, mean >=+0.5%/pick net, median>0,
both halves positive). Picks after 9:30 ET stored but flagged invalid.
Shadow-only: human_picks is never read by the live loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:52:39 -05:00
momentum devandClaude Fable 5 485cec0372 feat: wide_stop_veto_v1 — skip entries with stops >12% below entry (shadow)
Year-replay stop-width buckets: <8% +0.072R, 8-12% +0.120R, 12-16%
-0.054R, >16% -0.092R — the wide cohort holds every recent dollar
disaster (BIYA 16.7%, FCUV day-2 17.5%), where the exit ladder's blind
spot (BE/trail only arm on the way up) rides the full catastrophe
distance. New stopwidth_gate intervention (fail-open without levels) +
registered spec, same forward bar as the pm-high gate; the two cohorts
overlap so the adjudicator attributes which does the work. In-sample
counts for nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:06:37 -05:00
momentum devandClaude Fable 5 c7249b22e2 feat: orb_pmhigh_gate_v1 — premarket-high reclaim gate, forward OOS test
H3 (enter the pm-high break at 9:30 instead of the ORB) FAILED its
pre-registered bar on the year replay: paired delta -0.069R (n=689) —
entering earlier on the same names does not pay; the 5-minute wait stays.

The decomposition found the real signal: ORB fires where RTH price also
reclaimed the premarket high ran +0.316R (n=689) vs -0.298R (n=477) where
it never did — 100th percentile of a 2000-draw random-gate null at the
same keep-rate (null p99 +0.127), sign-consistent 14/14 months. First
lever of 15 to survive both checks in-sample.

Because the split was found in this dataset, it counts for nothing until
fresh fires confirm: new 'pmhigh_gate' intervention kind (fail-open on a
missing premarket_structure row, RTH-only reclaim check at/before the
entry bar) + registered spec with expR_delta >= 0.05 at n >= 200 forward
bar. Cohort self-registers on the next nightly experiments run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:20:25 -05:00
momentum devandClaude Fable 5 3fe0cb0891 feat: H3 replay — premarket-high break at 9:30 vs ORB, paired comparison
The pre-registered premarket hypothesis H3 (the source trader's actual
entry) made executable: both legs run per symbol-day over the year of
labeled gapper sessions, sharing the deployed exit engine (config parity)
and +1-tick adverse fills so the paired delta isolates ENTRY choice.

Comparison metric locked in the module docstring before the first run:
paired mean R delta (H3-ORB) where both fire, pass at >= +0.05R with
n >= 300 pairs, plus a stop-unit guard (median stop-distance % and
captured-move % printed for both legs — R parity from a tighter stop is
a fail, the vwap-reclaim lesson).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:13:57 -05:00
momentum devandClaude Fable 5 37757d3d47 fix: armed trigger names now get watcher scoring (blank reasoning panels)
Four of ten armed triggers had NO criteria events ever — the trigger book
arms from the live gapper scan while the watcher scores only the
discovery watchlist, so scanner-only names (TCX/ZTG class) showed an
empty reasoning panel with nothing behind it.

- arm pass injects every complete armed candidate into the watchlist
  provider's extra_symbols (its standing manual-add hook)
- provider returns injected symbols FIRST: the watcher truncates at
  max_symbols, so extras appended after a full research list were
  silently dropped — 'always include manually injected symbols' was
  untrue exactly when the list was full

60 watcher/watchlist/trigger tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:41:11 -05:00
momentum devandClaude Fable 5 054405dbdf feat: pyramid_1r_v1 experiment — add-into-strength, pre-registered
New 'pyramid' intervention kind in the replay engine: at +add_at_r on
initial risk, add add_frac x size (+adverse fill), whole-position stop to
blended breakeven; scratch there if a later bar trades through, else both
legs exit at the baseline exit. variant_r is per unit DEPLOYED
(P&L / ((1+add_frac) x risk)) so leverage alone cannot fake a pass.

Spec pyramid_1r_v1: add 0.5x at +1R, bar expR_delta >= +0.05 at n >= 300,
futility kill at n >= 500 below -0.02. Hypothesis notes the known failure
mode (median winners scratched at blended BE on first pullback). Shadow
only — no live-path changes; cohort self-registers on the next nightly
experiments run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 11:33:59 -05:00
momentum devandClaude Fable 5 733a182f10 feat: premarket-structure labeler (Phase 1 of premarket axis, observe-only)
Distills each gapper's premarket tape (first-hit price/time, pm high/low,
run %, open-vs-pm-high, VWAP distance, jackknife count) into a
premarket_structure table for entry-conversion analysis. Motivated by
FCUV 2026-07-31: our DB captured the full $1.98->$10.49 premarket ramp
while the ORB entered at $11.89 into the reversal.

Pre-registered hypotheses H1-H3 locked in the module docstring before any
data was examined; report command prints descriptive buckets only, with
n>=30/bucket floor and Phase-2 shadow bar required for promotion. Gates
nothing, trades nothing.

Wired into the nightly scheduler after premarket_eval. One session scan
per build (per-symbol queries were ~100x slower); partial session index
created post-close only (mid-market CONCURRENTLY build deadlocked against
live ingest).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 11:10:16 -05:00
momentum devandClaude Fable 5 8c62804808 feat: broker-aware factory for reconcile/journal/health tools
eod_reconcile, backfill_journal and the admin health block were hardwired
to AlpacaPaperClient — on a BROKER=tradier stack they would silently
reconcile/report against the wrong broker. make_trading_client() honors
BROKER env with no silent cross-broker fallback (missing Tradier keys =>
skip with reason, never fall back to Alpaca). Admin UI shows the active
broker label; JSON key stays 'alpaca' for compatibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:13:01 -05:00
momentum devandClaude Fable 5 6c6d0cd799 feat: weekly execution tallies — midday-entry net + market_exit post-run
Read-only nightly-scheduler job (Friday-gated): buckets realized P&L by entry
time (open/late-morning/midday) and, for each market_exit close, computes the
post-exit run-up capturable under a breakeven stop (0 if price undercuts entry
first; intrabar ties count as stopped). Emits a weekly_tally event + Telegram
summary. Evidence accumulator for the dead no_new_entries_after cutoff and the
weakness-exit winner-clipping question — no live trading path touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:25:14 -05:00
momentum devandClaude Fable 5 30c7232137 fix: market_context must not import runtime (research-isolation guard)
The DB-config wiring was unnecessary there anyway — market_context reads
zero trading knobs; the guard test (plan 009) rightly rejected the import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:36:29 -05:00
momentum devandClaude Fable 5 82273d9d4e feat: image version identity + DB-config parity for nightly scripts
Version: CI now tags images YYYY.MM.DD-<shortsha> alongside :latest/:sha
and passes GIT_SHA/BUILD_TAG build-args; the running app reports them via
dashboard /api/health and admin /api/status (TrueNAS custom apps only show
'custom/1.0.0', so the app self-reports what it actually runs).

Config parity: every scheduler-run script (nightly_tune, eod_replay,
premarket_eval, regime, market_context, experiments cli) now loads
config_overrides at startup like the live services do — a bare env would
replay under DEFAULT exits (the documented 4.5x-baseline landmine).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:28:06 -05:00
momentum devandClaude Fable 5 e9a1f577a9 feat: research.slippage — paper-vs-real fill variance from recorded NBBO
Prices each closed round-trip against the nearest quote_snapshots NBBO:
per-trade spread cost, book-walk flag (order size > displayed ask), and a
cost-adjusted P&L total. Measured this week: 0.3-0.5% median spreads on
gate-passing names, ~12% profit drag — far below the static 2-4% microcap
proxy. Coverage gaps (quote recorder only follows tracked names) reported
per-trade, not silently dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:04:47 -05:00
momentum devandClaude Fable 5 ed176b3712 fix: backtest starvation — schedule the labeler nightly, make inserts idempotent
Dashboard backtest sources sessions from setup_events v1, which only the
labeler writes — and the labeler was never in the nightly scheduler, so
setups (and therefore backtests) froze at the last manual run (07-17).
Root blocker was _insert crashing on duplicate setup_ids on any re-run;
now ON CONFLICT DO NOTHING, so incremental nightly builds are safe.
Scheduler gains 'labeler build' + 'build-tod1' after eod_replay in both
compose files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 06:51:56 -05:00
momentum devandClaude Fable 5 9bfd458956 feat: tod1 shadow track — zero-gap intraday volume-explosion detector
The 07-21 misses (CPHI +417%, DFNS +94%, GREE/WGRX +40%) were all static-gap
<10% names the validated gapper population excludes by design, but they share
a signature no refuted detector (liq1/run1) measured: rvol_tod (cum volume vs
median-20d prorated by elapsed session) at 50-1000x on a flat open. New
labeler setup tod1 fires on the rising edge of rvol_tod>=50 AND price>=
prev_close*1.20, restricted to static gap <10%, stop = 5-bar swing low,
scored via the shared _forward_labels. Shadow-only; pre-registered promotion
bar locked before forward data (median fwdmax60 >=+10%, median dd60 >-6%,
+1R>50%, top stratum rvol_tod>=200 clears alone).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:59:51 -05:00
momentum devandClaude Fable 5 5597250ff1 fix: marketable entries price from live last + time-of-day rvol arms fast path
Day-22 forensic (BIYA session): 6 entry submissions, 0 fills — IREX missed
+1.7R, IRE +1.65R by one cent. Two root causes fixed:

1. Slow-path limit rested AT the trigger while price ran 2% past it, and
   retries reused the same stale price. Marketable model now prices from
   max(trigger, live last) via price_provider; the anti-chase extension
   gate gets the live ref too (was an entry_ref==trigger no-op on this
   path). TRADING_ENTRY_FILL_MODEL flipped to marketable.

2. Fast trigger path armed nothing all day: legacy rvol (cum-so-far /
   full-day MEAN) is ~0.1-0.3 minutes after the open for a normal heavy
   name and its mean is poisoned by reverse-split/prior-squeeze days
   (BIYA legacy 1.27 vs real activity 481x median). New rvol_tod =
   cum / (MEDIAN(20d) * elapsed-session fraction) drives arm eligibility
   and the scanner keep-filter; legacy rvol untouched for validated
   boundaries (gap-squeeze rvol>=5).

Also: stagnation time-stop (TRADING_EXIT_STAGNATION_MIN/_R, default OFF)
in simulate_exit + manage_live with sweep_exits.py variants for
validation before any live enable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 15:40:15 -05:00
momentum devandClaude Fable 5 46d3460d5c experiments: depth_overhang_v1 — L2 order-book overhang gate (first signal to clear the bar+null)
Plan 017 Phase 0->3. Databento depth backfilled for 602 fires across 21
sessions -> depth_features table. Reweighted to true 23% population win rate,
gating on ask-overhang-above-break (keep>=0.034) lifts win rate 23->28% and
clears the random-gate null (+0.057 vs +0.05 on the conservative +1/-1
bracket). iv_depth_overhang_gate = keep-or-zero, skip-on-no-coverage (the
dilution-v1 lesson). This commit registers the REAL-exit harness run
(simulate_exit asymmetric trail) for the true live delta. Counter-intuitive
mechanism: more resting supply above the break = MORE likely to win
(absorption/fuel), invisible in bar data — the owner's 'size stacked above'
instinct, proven.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:32:06 -05:00
momentum devandClaude Fable 5 6e6bca1518 fix(attribution): stop KeyError-ing when DATABASE_URL is unset
research/attribution.py's _connect() read os.environ["DATABASE_URL"]
directly, bypassing the storage facade fallback chain (DATABASE_URL,
PG* vars, LAN host retries) every other module goes through — and
KeyError-ing (uncaught by name, though still 500'd by the endpoint's
broad except) whenever DATABASE_URL was unset.

Delegate to storage.db.get_connection().raw instead, so DB resolution
matches EventStore/research.multi_schema, and a genuinely unreachable
DB fails with a catchable psycopg2.OperationalError rather than a
KeyError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:49:55 -05:00
momentum devandClaude Fable 5 849bd476f9 adjudicator: random-gate null for keep-or-zero PASSes + register orderflow H6 (quote-size book imbalance)
Null (the H2 lesson): zeroing share f of a negative baseline mechanically
yields expR_delta = f*|mean baseline|; a PASS on expR_delta_min now also
requires expR_delta_vs_null >= the same margin whenever zeroed_share >= 5%.
Exposure-shrinking can no longer masquerade as selection; non-gate
experiments unaffected. Honest disclosure: regime_meta_v1's FIRE-weighted
pass would not clear this bar either — its standing rests on the session-
level walk-forward OOS evidence, which no random gate reproduces.

H6: CKS order-flow-imbalance from quote_snapshots bid/ask SIZES —
size-weighted book imbalance over the 10-min pre-trigger window, zero
ask-tilted books; skip-on-no-coverage; judged under the null from day one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:50:05 -05:00
momentum devandClaude Fable 5 07ddbd30b9 experiments: register orderflow H3 (spread-expansion gate) + H4 (volume-spike gate)
H4 (3-book): keep-or-zero on trigger-bar volume concentration vs prior 5-bar
sum (>1.0 = completed one-shot burst). H3 (5-book, direction disputed):
fragility-camp primary — zero fires whose fill-time relative spread exceeds
2x the name's own 10-min median from quote_snapshots (which carry bid/ask
SIZES since 07-02 — the note's data-upgrade #1 was already satisfied).
Coverage design learns the dilution_veto_v1 lesson: no-quote fires are
SKIPPED (variant None, excluded from n), so thin coverage cannot false-FAIL.
H5 deliberately deferred until H1's verdict (its incremental value is defined
net of H1's catches). 6 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:32:10 -05:00
momentum devandClaude Fable 5 46f80eccf2 experiments: register orderflow H1 (trigger-erasure exit) + H2 (flow-persistence gate)
From docs/research/orderflow_notes.md — the order-flow shelf distillation.
H1 (5-book convergence): exit on first post-fill 1-min close back below the
trigger within 5 bars — the transitory-impact/recross signature — else the
standing exit stack. H2 (4-book): keep-or-zero on pre-trigger volume-weighted
bar-sign persistence (<0.30 over 15 bars = choppy one-shot burst, zeroed);
strictly-before-entry window, fail-open on thin history. Both pre-registered
at the house bar (+0.05R, n>=300, standard fidelity); nightly adjudication
decides. 7 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:24:22 -05:00
momentum devandClaude Fable 5 a9aa37c885 backtest: replay the LIVE track only (v1) — shadow research tracks poisoned /backtest
replay_window reconstructed fires from ALL setup versions, so liq1/vr1/run1/pm1
(refuted shadow tracks, never traded live, 8:1 volume vs v1, tight-stop
artifacts) dominated /backtest: 17k trades / 45 sessions at -1.8R/trade —
garbage scale vs the harness-validated -0.08R/fire. reconstruct_fires_from_setups
gains an optional setup_version filter (default None: experiments unchanged);
replay_window pins it to v1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:22:03 -05:00
momentum devandClaude Fable 5 f6999c7f6a dilution(016): unbreak nightly EDGAR refresh + pre-register filing-veto experiment
premarket_eval's shadow enrichment call site queried session_date = current_date
(wrong on UTC-server Postgres at night, so it never matched any symbols) and
shared one try-block for float_cache + dilution filings (a float_cache failure
silently skipped the dilution refresh too). Fix: compute the ET session date
in Python, split into two independent try-blocks, lift into a testable
_refresh_shadow_enrichment(con, session_date). float_cache now applies its own
DDL unconditionally on use instead of only when an API key is present.

Adds iv_dilution_gate (additive, mirrors iv_regime_gate's keep-or-zero shape
keyed on symbol instead of session_date, fail-open on unfetched symbols) plus
its once-per-run preload, and registers the pre-registered dilution_veto_v1
experiment via the existing experiments/*.json self-registering cohort. Live
veto stays OFF; this only wires the shadow measurement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:54:49 -05:00
momentum devandClaude Fable 5 8ac222a4ca research-tooling: /backtest replay dashboard — theoretical trades under current settings
Adds a read-only /backtest page showing how the bot WOULD have traded past
sessions under the CURRENTLY configured live settings: theoretical ORB fires
from setup_events, replayed through the shared exit engine (simulate_exit +
ExitConfig.from_env(), same adverse-fill accounting the exit-experiment
harness uses), screened through the shared population gate and (optionally)
the live regime gate. No new simulation math — replay.replay_window() is pure
assembly of existing pieces (reconstruct_fires_from_setups, _fire_in_population,
_load_regime_favorable, simulate_exit); api/main.py adds the /backtest and
/api/backtest routes plus a dedicated-lock 10-min cache so a slow replay never
blocks the live dashboard's SSE stream. Also extends /api/chart with an
optional date= anchor so the trade drill-in chart modal can reach sessions
older than the existing 72h-from-now window, and adds a one-line BACKTEST nav
link to the live dashboard header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 08:42:43 -05:00
momentum devandClaude Fable 5 43a3ace91d Regime gate: make the live gate actually decide (it shipped inert) + fix breadth_rvol units
The 07-15 live gate never made a real decision: live_session_favorable read
today's breadth_rvol from today's OWN setup_events rows, which only the
NIGHTLY labeler writes — always None intraday, fail-open every session (the
07-15 "validation block" was computed pre-guard on a zero-filled feature).
Worse, breadth_rvol was unit-corrupted: _session_context pooled all setup
versions and pm1 stores RAW cumulative premarket volume in relative_volume
(median ~261k) vs the ratio tracks (~0.8) — ~240/267 historical sessions'
breadth was actually median premarket volume, and plan 009's v1 RVOL fallback
was about to flip forward sessions to ratio units.

Fixes (TDD, 21 regime tests):
- breadth_rvol is now pm1-ONLY: median premarket volume — the dominant
  historical source, unit-consistent, and strictly pre-open knowable.
- _live_breadth_rvol recomputes today's pm1 fires IN MEMORY from premarket
  minute_bars (pure compute_premarket_setups, no insert); previous_close
  falls back to the latest prior daily close when today's daily_bars row is
  absent intraday (else the gap basis silently flips to the premarket open).
- _regime_reject: verdicts before 9:50 ET are PROVISIONAL (re-scored ~5 min;
  ingest lag can truncate the premarket frame) and the first verdict at/after
  9:50 is FINAL even when None — caps the scorer at a handful of runs/day
  instead of ~78 on quiet days (hot-path stall guard). Throttle arithmetic
  moved inside the fail-open try (naive/aware mixing must not escape).
- Live fit excludes breadth_rvol-NULL training rows (zero-fill taught a
  "zero premarket volume" class the serve path can never produce).

Prod re-validation on the cleaned feature (regime_features rebuilt, scores
rewritten): regime_meta_v1 fire-weighted expR_delta +0.0570 (pre-registered
bar +0.05, n=2058, PASS — the contaminated +0.0965 was inflated), keeps 37%;
recent-80-session delta +0.149R/session (always −0.141 → gated +0.008).
First genuine live verdict 2026-07-16: UNFAVORABLE (pm breadth 612,819).
Known accepted skew: live scaled-IEX volumes vs nightly SIP tape — the
meta_live-vs-EOD comparison is the standing forward measure.

Also backfilled the missed 07-15 nightly job supervised (SIP refetch, 50 v1
+ 6 pm1 setups, SPY/QQQ ingest, regime row) — plan 009's accrual clock is
unbroken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:00:07 -05:00
momentum devandClaude Opus 4.8 0ac09d3574 Regime gate LIVE (plan 011): gate fresh entries on the session-level metalabel
Wires the regime signal into the money path — fresh entries are BLOCKED on
sessions the walk-forward model scores unfavorable. Session-level: evaluated
once per day, cached. Enabled in both compose templates (paper).

Correctness (blocker 1 resolved): the LIVE feature set drops tape_regime (the
only non-strictly-open feature — AND measured 0.000 importance) + breadth_gap
(also 0.000). The 5 remaining features are all non-look-ahead at entry time:
breadth_rvol = median(relative_volume=vol_upto/avg_vol_20d) over the session's
setups so far (the SAME up-to-trigger calc the live bot runs, NOT day-end),
spy_* lagged to prior close, trailing from PRIOR sessions. The live single-
session fit reproduces the batch meta_v1 verdict exactly on the last 6 sessions.

Safety: fail-open everywhere — disabled, unfit model (<60 prior sessions),
missing breadth_rvol (pre-setup), or ANY exception returns None and never
blocks. The gate can only SUBTRACT trades on bad-regime days.

- research/regime.py: live_session_favorable() (refit on priors <today, predict
  today; writes regime_scores['meta_live'] for forward validation) + LIVE_FEATURE_COLS
  + the breadth_rvol-missing fail-open guard.
- trading_execution.py: TRADING_REGIME_GATE_ENABLED / _P_MIN settings + _regime_reject()
  (cached per session) + a REGIME gate after SESSION in _evaluate_entry_gates.
- docker-compose.yml + truenas-compose.yml: TRADING_REGIME_GATE_ENABLED=1.
- tests/unit/test_regime_gate_live.py: block / pass / fail-open(x3) / scored-once-per-day.

KNOWN train/serve skew (documented, validate forward via meta_live vs EOD label):
model trains on full-day breadth_rvol, live uses setups-so-far. Small edge on a
~266-session sample; paper only. Suite: 504 passed, 15 pre-existing loopback fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:11:50 -05:00
momentum devandClaude Opus 4.8 092d6072e8 Regime gate (plan 011): deploy-wire the shadow pipeline + on-demand admin trigger + prod backtest
The session-level metalabel P(our ORB profits today) — the one lever that
survives out-of-sample. Backtested on 266 prod sessions (walk-forward):

  trade-always (trained subset): -0.111R/session, 36% hit
  meta_v1 GATED (trades best ~32% of days): -0.004R/session, 48% hit
  delta +0.107R in-sample, +0.128R OOS (recent 80 sessions), improves across folds

It does NOT make the bot profitable — it takes a bleeding strategy to roughly
flat by sitting out the worst ~2/3 of sessions. threshold_v1 rule gates too
little to matter; meta_v1 (walk-forward GBM) is the signal. Leaky full-fit
(+0.61R) >> walk-forward confirms the discipline was load-bearing; the OOS
holding is the evidence the +0.078R is real, not look-ahead.

Changes:
- research/regime.py: batch minute-bar load per session (64k per-fire round-trips
  -> ~266 queries; fixed a >5min build to ~3min) + incremental label pass (skip
  already-labeled sessions; --rebuild forces full). Nightly cost now ~one new day.
- docker-compose.yml + truenas-compose.yml: scheduler runs build-features + score
  (both models) before `experiments run`, nightly. ALL-SHADOW — does NOT gate live
  trading (that stays owner-gated Step 5).
- admin_api.py + admin.html: POST /api/regime/run (detached) + GET /api/regime/status
  + a Run/Status button — the panel had no way to trigger this.
- plans/011 + docs/research/regime_notes.md (6-book distillation) + plans 008/009 docs.

Prod state (shadow, rollback = DROP regime_features/regime_scores + DELETE
experiments WHERE id LIKE 'regime%'): 266 feature rows, 532 scores, 2 experiments
registered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:05:34 -04:00
momentum devandClaude Opus 4.8 275c56b545 Plan 011 (build+test only): regime gate — session-level ORB metalabel
research/regime.py: Step 0 probe (labelable-session/SPY-coverage STOP gate),
Step 1 build-features (regime_features: pre-open breadth/tape + prior-close
SPY momentum/vol + trailing bot expR, label = mean _sim_baseline_r over the
session's setup fires) + walk-forward threshold_v1 rule, Step 2 walk-forward
meta_v1 (shallow sklearn GBM), Step 4 OOS/leakage-audit helpers
(oos_split/sequential_folds/session_expr_delta/full_sample_fit_threshold).
Step 3 (HMM) deferred per scope — statsmodels stays absent.

replay.py: new iv_regime_gate intervention (keep-or-zero a fire on a
pre-computed session regime score, fail-open on no score row) wired into
_compute_fire + run(), mirroring rank_one_bullet's shape.

experiments/regime_threshold_v1.json + regime_meta_v1.json register the two
model variants against the shared population/bars/fidelity contract.

All new code is unit-tested against isolated EventStore(":memory:") schemas
only — no production DB read or written. Live-trading paths untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:47:53 -04:00
momentum devandClaude Fable 5 ab2810896c experiment-os: generic exit-config intervention + v1/pm1 exit experiments
iv_exit_config replays ANY target/breakeven/trail combo through the shared exit
engine vs the live-exit baseline — reusable way to adjudicate exit numbers.
Counterfactual over setup fires: v1 (slow, 1.37R median winners) gains most from
prior_low trailing (win 17->24%, +0.036R); pm1 (fast, 2.64R median, 10R tail)
gains most from NO target + step0.5 (+0.273R, win 24->34%) — the fixed target
throws away pm1's fat tail. Registered both as tracked experiments. +test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 22:24:18 -04:00
momentum devandClaude Fable 5 dbaeb701e2 conversion(009): committed go/no-go signal probe (Step 5 instrument)
research/conversion/{__init__,dataset,signal_probe}.py — the leakage-safe probe
plan 009 Step 5 re-runs scoped to a live setup_version:

- dataset.load_dataset: setup_events JOIN outcome_labels; label =
  reached_1r_before_minus_1r; features = trigger-time-knowable setup_events
  columns ONLY, dropping >70%-null and <2-distinct columns. HARD leakage rule —
  every outcome_labels column is blocklisted AND asserted absent from X, so a
  schema change can't leak the answer in.
- signal_probe.run_probe: expanding-window CV over ~5 sequential session-date
  blocks (whole days, train strictly precedes test), HistGradientBoostingClassifier
  (max_iter=250, lr=0.05, max_depth=4, l2=1.0, categorical_features=indices),
  per-fold + mean ROC-AUC, top-decile +1R lift vs base, permutation importance on
  the last fold. Drops columns constant within a train slice (HistGBM binning).
  CLI: python3 -m research.conversion.signal_probe [--setup-version v1].

Tests: the critical leakage guard (zero outcome columns in features), AUC>0.5 on a
synthetic signal + ~0.5 on pure noise, and a static live-path guard. All :memory:.
No live-path change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:32:18 -04:00
momentum devandClaude Fable 5 5b885a719f ingest(009): nightly SPY/QQQ ingest + catalyst enrichment ON as feature source
Step 3 of plan 009 (config + code; no live-path change):

- research/ingestion/market_context.py: pulls SPY/QQQ full-session minute bars
  (SIP, post-close embargo-safe) + daily bars via the existing upsert helpers, so
  the labeler's market-context features have a FORWARD source (index ETFs never
  enter the gapper universe the live loop ingests). Mirrors sip_backfill's shape.
- both compose scheduler blocks: run it after premarket_eval, guarded || echo.
- NEWS_ENRICH_ENABLED on: added to docker-compose x-app-env anchor (loop +
  scheduler inherit it); truenas-compose + .env.example already had it. Catalyst
  is a FEATURE source only, never a live gate; enabled-but-down never blocks
  trading. Enrichment OUTPUT is verified by the orchestrator (needs live Ollama).

Compose statically validated (yaml.safe_load). Ingest self-checked via a
fake-client :memory: round-trip through _preload_spy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:27:49 -04:00