Commit Graph
100 Commits
Author SHA1 Message Date
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 98805279d1 feat: real-time press-release and SEC filing recorder (prwire)
Premarket news timing could not be tested on history. Alpaca's Benzinga feed has
almost no PR-wire items for microcaps, and wire RSS feeds only keep the latest few
dozen items. So from now on, every item is recorded with our first-seen time.

- Polls PR Newswire, GlobeNewswire, Business Wire and SEC EDGAR's current 8-K, 6-K
  and 424B feeds every 20 s, in parallel. A failing feed backs off up to 10 min.
- Each new item goes to data/prwire/<date ET>.jsonl with seen_utc, feed, title,
  link, tickers (exchange-tagged, e.g. "(Nasdaq CM: ABCD)"; SEC filings via CIK)
  and form. Restarts do not re-log items already recorded.
- Runs on the workstation via Task Scheduler "Momentum\PrWireRecorder" (pythonw,
  no console window; re-fired every 10 min with IgnoreNew so it self-heals).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 08:12:10 -05:00
momentum devandClaude Opus 5.5 8dc97a53f8 feat: judge paper trader runs a software stop alongside the broker stop
It sells as soon as any trade prints at or through the stop, or below the 6% trail
once +1R, walking the new prints in order exactly as the validated simulator does.
Whichever acts first wins: the broker stop or this one.
Why: Alpaca's paper engine filled triggered stops 6-60 s late on 2026-09-30 (CMCT,
PWCM; judge.review), which makes the trial look worse than the simulation for reasons
that are not the strategy's. Brokers also run no stop orders outside regular hours,
so any premarket trading needs this.
JUDGE_SOFT_STOP=0 turns it off. judge.review treats soft_stop/soft_trail as
stop/trail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 07:40:24 -05:00
momentum devandClaude Opus 5.5 5315e99978 feat: judge.review compares each paper trade with the simulator
For a paper day, every order is lined up with what the validated simulator does on
the same recorded ticks: the sim exit from the real fill (stop, 6% trail after +1R,
20-min time stop, sell at the bid 300 ms after the trigger) vs the paper exit,
plus the sim R of orders that did not fill. Trades are flagged when R differs by
more than 0.3R, the exit reason differs, or the entry slipped. The report goes to
data/judge/reviews/<date>.md.

On 2026-09-30: paper -8.25R vs sim -3.97R on the same six fills. The gap is two
trades where Alpaca's paper engine filled a triggered stop 6-60 s late (CMCT,
PWCM; PWCM's own exit was also blocked by the stop-id bug fixed in f3c8be1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-01 07:11:54 -05:00
momentum devandClaude Opus 5.5 358b859c59 fix: give Postgres a 5-minute start_period in both TrueNAS composes
After an unclean shutdown, WAL replay outlasted the 5 x 10 s healthcheck window,
so compose marked Postgres unhealthy and refused to start any dependent service.
The paper app sat in 'Deploying' on 2026-09-30.

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 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 f3c8be11b6 fix: judge paper trader tracks replaced stop orders
On 2026-09-30 PWCM's stop was moved, and Alpaca's replace returns a NEW order id.
The trader kept the dead id, so reconcile treated the stop as gone and tried to
arm another one (refused with 403, shares held) about 20 times in 80 s, and the
trail exit could not sell because it cancelled the stale id.

- Adopt the id returned by replace. Reconcile follows replaced_by, and before
  arming it adopts any working sell stop already at the broker.
- Exits cancel every open order on the symbol before close_position.
- If the price is already through the stop, close instead of arming a stop the
  broker would reject.
- tests/unit/test_judge_live_exits.py covers these cases and the 2026-09-29 ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-09-30 15:58:52 -05:00
momentum devandClaude Opus 5.5 347031e2bb feat: judge paper trader survives a crash mid-session
On 2026-09-29 the trader's console was killed at 10:21 ET (exit 0xC000013A) and
it sat dead for the rest of the day. The scheduled task now re-fires every 5 min
from 08:10 to 15:00 local, and IgnoreNew makes that a no-op while the trader is
running. It also catches up if the machine was off at 08:10.

A relaunched trader now picks up where the dead one left off:
- positions still held at the broker are adopted, with the entry, original
  stop, fill time and high-water taken from today's journal and the backfilled
  tape, so the trail, time stop and 15:55 flat still apply. The working stop leg
  is reused; if none is working a new one is armed. A position with no known
  stop is closed rather than guessed at.
- the per-symbol order count is rebuilt from the journal, so the cap holds
  across restarts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-09-29 21:21:42 -05:00
momentum devandClaude Opus 5.5 c518747387 fix: judge paper trader reconciles with the broker before exiting
Day 1 of the paper trial (2026-09-29) exposed two exit bugs:
- When the broker stop had already filled, a trail/time exit tried to cancel the
  filled leg, got a 422, returned without clearing the position, and retried
  every second forever (BKYI). The leg-filled check sat after a `continue` and
  never ran.
- A partial fill whose parent was cancelled took its OTO stop leg with it,
  leaving the shares unprotected (MSGY, 82 of 133 filled).

Now every 5 s, and after any failed exit or stop move, the trader checks the stop
leg and the broker position: a filled stop or a flat position is recorded as the
exit, and held shares with no working stop get a standalone stop. Partial
fills get their stop re-armed straight away. Failed exits retry every 5 s instead
of every second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:18:41 -05:00
momentum devandClaude Opus 5.5 f2c7e52c79 feat: judgement-model (v2) paper trader on the Alpaca "Momentum V2" account
A gradient-boosted model scores burst / micro-pullback / whole-dollar signals on
squeeze stocks from tick data, and only its top-scored regular-hours signals
are traded. Validated in data/ticksim before any live use:
  - v1 on a fresh 600-day sample: real ranking (AUC 0.63), thin edge. FAIL.
  - v2 (trained on Jun-Jul of samples 1+2), pre-registered, on 500 NEW Aug-Sep
    symbol-days: +1.11%/trade after the spread (day-block 90% lo +0.54%),
    n=332 over 38 days. PASS. It held at 2 s latency and +0.2% extra cost.

- judge/engine.py: the validated signals, execution gates and features, as one
  shared source.
- judge/model.py: trains and freezes v2 (cutoff = the training top 20%).
- judge/decide.py: pure decision logic.
- judge/replay.py: a live-style replay of recorded days. Parity: 40 of 41 research
  trades matched with identical model scores.
- judge/live.py: the paper trader. Watchlist from the snapshot scan with
  backfill, SIP trade/quote websocket, decisions each second, a limit at the ask
  with a broker OTO stop (cancelled if unfilled in 3 s), trail 6% after +1R,
  20-min time stop, flat by 15:55. $20 risk and $1,000 max notional per trade.
  Everything is journalled and the day's ticks are kept for paper-vs-simulation
  review. Weekends and holidays exit.
- Runs from the workstation (Task Scheduler, weekdays 09:10 ET) with the
  research venv. Paper only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
2026-09-28 18:25:26 -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 0c8a27e23f feat: switch to turn off the >=150% gap squeeze exception (default on)
TRADING_GAP_SQUEEZE_ENABLED=0 rejects >=150% gappers in the population gate
(watcher pre-filter and entry gate) and zeroes the trigger book's squeeze
arm. One switch covers all three paths.

Why: while testing the trader's "trade the leading gainers" idea on 1,093
live-band v1 ORB breaks (Jun-Sep 2026, current exits), top-3 gainers at the
break did WORSE (diff -0.48R Jun-Jul, -0.40R Aug-Sep, 90% CIs excluding 0).
That turned out to be almost entirely the >=150% squeeze-exception names:

  gap 10-35%   Jun-Jul +0.27R (n=342)   Aug-Sep +0.13R (n=327)
  gap >=150%   Jun-Jul -0.21R (n=65)    Aug-Sep -0.30R (n=53)

Our own squeeze fills: -0.31R / -0.33R realized (n=16). By the time a squeeze
name breaks its opening range the move has happened; the trader catches these
names premarket, which the bot does not. This finding was post-hoc (not the
pre-registered direction), but it held in both halves and in the real fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
2026-09-28 09:45:51 -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 53725854f4 feat: fail-fast exit for breakouts that fail inside their first minute (off by default)
At the first exit-thread pass >= TRADING_EXIT_FAIL_FAST_SECONDS after the fill,
flatten if price has already traded TRADING_EXIT_FAIL_FAST_R x R below entry.
It is checked once per position. A position first seen after its window (a
restart or a carry) is never judged late. 0 seconds = off (the default).

Evidence, from tick data on 1,093 live-band v1 ORB breaks (Jun-Sep 2026) with
outcomes under the current exit rules:
  - 60 s / 0.3R: +0.060R per trade Jun-Jul (90% CI +0.015..+0.102) and +0.062R
    Aug-Sep (+0.029..+0.094). It fired on 29% of trades and cut 22 runners.
  - Every threshold from -0.15R to -0.4R at 60 s was positive in both halves,
    a plateau rather than one fitted cell.
  - On our own 194 real fills (10-s bars): +0.015R. Same sign, not yet
    significant.

It is deliberately narrow. The same study found that later "bail out below
+1R" rules kill runners: the LLM trade-manager pilot, the hybrid bailout, and
orderflow H1 (Jul 17). The first minute is the one window where failure tells
you something. Tape-flow features (aggressor buy share, print rate, big
prints, quote imbalance, spread) separated nothing (AUC 0.44-0.59, not stable
across halves).

Live-only: it needs sub-minute prices, so simulate_exit (minute bars) does not
model it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
2026-09-26 19:20:17 -05:00
momentum devandClaude Opus 5.5 3d0ee52214 Merge: nightly Ross-recap learning loop on local Ollama
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:24:02 -05:00
momentum devandClaude Opus 5.5 91af4c2e93 feat: nightly Ross-recap learning loop on the owner's local Ollama
Why: the pilot in data/ross showed that Ross Cameron's daily recaps, joined to
the SIP tape and to what our bot saw, answer "which of his trades could we have
taken and why didn't we". It was a set of one-off scripts run by hand; the
server-side youtube_claims pipeline has been dead since ~08-24 (YouTube blocks
the TrueNAS IP) and its captions fallback hid that. This makes the pilot a
nightly, local, cumulative loop without a paid LLM API.

youtube_claims/recap/ (python -m youtube_claims.recap.nightly):
- channel: yt-dlp flat listing of @DaytradeWarrior, m4a audio, faster-whisper
  small.en on CPU, pilot-format bundles (meta + our scanner list + transcript).
- extract: Ollama structured output (JSON schema) per EXTRACT_SPEC, chunked
  with overlap for long transcripts and merged. The verbatim rule is enforced in
  code: every quote must be found (token-level, one-word tolerance) and is
  snapped to the transcript's words; unquoted items, trades with no surviving
  quote, prices never spoken, and passed-on names never said are dropped.
- llm: endpoint pool over RECAP_OLLAMA_HOSTS (round-robin + failover, per-host
  model/ctx/think pins) since the owner runs several Ollama boxes.
- tape/join/ourside: ported join.py/ourside.py -- tickers are price-verified on
  the Alpaca SIP tape (plus an order-free check for his own named ticker), then
  paper (:5433) and live (:5434) event views are attached. Bars cached on disk.
- store + schema.sql: idempotent recap_* tables in the transcripts schema,
  upserted per video_id. report: the pilot's sections + "what changed since last
  run" against the previous day's metrics JSON.
- evaluate: scores any model/prompt against the 36 gold extractions.

Model choice (36 gold videos, final code path, P/R):
  gpt-oss:20b on the workstation GPU, two focused calls, think=low:
    trades P .84 R .70; tickers P .82 R .77; entry +-2% P .54 R .50;
    passed-on P .66 R .76; temperature 16/31; kind 30/36; ~49 s/video.
  tools:latest (4B, server) one call: trades P .81 R .72, tickers P .83 R .85,
    passed-on P .80 R .75, temperature 20/31, but it invents prices
    (entry P .42) and occasionally runs away (5-9 min). Prices drive the tape
    verification, so the more precise local model is the default.
  Iterations: prompt v2 + spoken-price rescale (699 -> 6.99) + split calls took
  gpt-oss trades F1 .74 -> .76-.78 and tickers .78 -> .79-.80; think=medium
  blew the token budget. qwen3-30b-a3b on mason only finished 5 videos (host
  saturated) -- inconclusive.

Also: youtube_claims/transcription.py::_captions_fallback now picks the
youtube-transcript-api shape by feature detection instead of retrying the
removed get_transcript on any error, and transcribe() reports both the whisper
and captions causes -- the IpBlocked error was being masked as AttributeError.

No trading code touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:23:18 -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 Opus 5 42ca523053 fix: stop gating entries on minute-bar completeness
TRADING_MIN_QUALITY_SCORE 0.60 -> 0.20.

setup_evaluator.py:164 emits "quality_score": quality.score, where quality comes
from calculate_data_quality_score(bars). That module's own docstring reads "Data
quality scoring for minute-bar coverage and integrity" -- it measures whether the
bars are complete, not whether the setup is good. The name collision meant a
coverage metric has been gating live entries.

Tested against realised outcomes over 62 trades: r = 0.0845. Grade A averaged
+0.13%, grade B -0.71%, grade C -0.17% with an 80% win rate -- grade C beat grade
A. There is no signal in it. On 2026-09-14 it blocked SNDQ ("ready below quality
gate (score 0.56 < 0.60)") purely for gappy bars.

0.20 keeps it as a guard against genuinely broken data while removing it as a
setup filter. Renaming the field so the collision cannot recur is the proper fix
and is left for a follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-16 20:01:57 -05:00
momentum devandClaude Opus 5 f7bec45c32 chore: arm the equity-floor circuit breaker at $215
The 2026-09-13 exit retune is $7.50 behind the config it replaced across its
first 8 live trades. Its thesis -- surrender small frequent gains to keep a rare
large tail -- predicts exactly this early shape, and the backtest behind it was
234 trades across two stacks with train/test splits. But 8 losing live trades are
also what a wrong call looks like, and nothing in the data separates the two yet.

So pre-commit the exit rather than deciding it mid-drawdown. TRADING_EQUITY_FLOOR
halts the session and flattens the book at $215, about -11% from the 241.75 the
change started at. The check reads broker equity directly and returns early on a
0.0 reading, so it cannot trip on the overnight-maintenance nulls.

Owner decision 2026-09-16: keep the retune, on the view that the same day's
risk-equalisation fix (0.05 -> 0.015 per trade) removes most of what was actually
doing the damage -- the four widest-stop entries produced -$8.57 of the -$8.84
week, and those are the positions the sizing change shrinks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-16 14:58:19 -05:00
momentum devandClaude Opus 5 60e009f0c6 fix: equalise dollar risk per trade — the position cap was doing the sizing
TRADING_RISK_PER_TRADE_PCT 0.05 -> 0.015.

On a ~$240 account the 5% risk budget ($12) is unreachable: a 4.5% stop would
need $267 of notional against a $38 position cap (max_position_pct=0.16). So
calculate_position_size never bound -- the POSITION CAP did, on every trade.
The consequence is that every position was the same NOTIONAL (~$34 average) and
the dollar risk floated with stop width instead of being held constant:

    measured over 64 live trades: risk ranged $0.52 to $8.79 -- a 17x spread

That is the opposite of what risk-based sizing is for, and it concentrated the
damage exactly where the stops were widest. On 2026-09-15/16 the four widest-stop
entries (CANG 13.5%, FTFT 21.6%, ZTG 13.2%, VNCE 19.8%) produced -$8.57 of an
-$8.84 week. It also interacts badly with the -10% catastrophe floor: any stop
wider than 10% is effectively a 10% stop, so a wide-stop trade cannot be saved by
its own stop but still realises a full 10%+ loss on the same notional.

At 1.5% the risk calc binds for wide stops while the position cap still binds for
narrow ones, compressing the spread to 6.8x. Replaying the same 64 trades:
-$4.09 -> -$0.32.

Checked that this is risk EQUALISATION and not merely trading smaller: against a
plain proportional shrink to the same average notional, equalising is worth
+$3.27 at 1.25%, +$2.30 at 1.0%, +$2.45 at 0.75%. The benefit holds at every
matched size.

Below 1% trades start being skipped outright as shares round to zero (1 of 64
lost at 1.0%, 13 of 64 at 0.5%) -- a hidden cost that looks like improved P&L.
1.5% is the tightest setting that drops no trades. Expressed as a percentage
rather than TRADING_MAX_RISK_DOLLARS so it scales with the account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-16 14:36:13 -05:00
momentum devandClaude Opus 5 11b0fcd7b7 fix: read the session open from the first regular-hours bar, not bars[0]
_population_reject computed the static open gap from bars["open"].iloc[0].
Premarket that is a PREMARKET bar, so the gap is fabricated. Live on
2026-09-14: FTFT opened 3.30 against a 2.88 previous close (+14.6%, well
inside the tradeable 10-35% band) but its premarket bars started near 2.52,
so the filter read -12.5% and benched it at 09:12 -- before the session had
even opened.

It never self-corrected. The frame keeps its premarket bars all session, so
every later pass recomputed the same wrong number, and _transition only emits
on change, so the symbol stayed out_of_population for the day. Several of the
day's rejections show negative gaps (-12.5%, -17.4%, -1.9%) that look like the
same artifact.

Trading was unaffected -- the trigger path does not consult watcher state, and
FTFT was still evaluated and rejected on its own merits (over_extended_day, it
was already +59% off the open). The damage is diagnostic: valid candidates lose
their criteria trail, which is the blank-reasoning-panel problem a previous
commit set out to fix.

Read the open off the first is_regular_hours bar; return None (fail open) when
the session has not opened, since the static gap is not yet knowable. A frame
with no session flag is treated as already regular-hours-only, which is what
tests and replay hand it.

Also drop the pm-range=N counter from step_arm's return string. The arm step
never emits a module_tick -- verified against both 2026-09-11 and 2026-09-14,
where only discovery, ingestion, enrichment and triggers do -- so it reached
container stdout and nothing else. Adding a tick purely to carry it would put
~4k events/day back after the population filter had just removed 88% of them.
pm_range_pct is already on the armed candidate rows for the board; persist it
on the approval payload if it needs to be measurable per trade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-14 15:27:01 -05:00
momentum devandClaude Opus 5 4e0559501a fix: stop boosting names because a headline already reported the move
Three independent tests over 2026-08-01+, all negative:

  premarket news, any window      7.9% vs 8.6%  ran >=50%
  ...controlling for PM range    11.3% vs 21.0% ran >=50%
  intraday arrival, hour-matched 16.8% vs 24.6% ran >=10% forward

The feed explains it. Of 1,621 headlines delivered for screened names during
sessions: 61.7% reactive roundups ("Why X Shares Are Trading Higher", "20
Stocks Moving In..."), 2.7% law-firm spam, and 0.9% genuine catalyst PRs. The
wire reports the move after it happens, so a boost on "has a headline" buys the
late stage of a run. 11 of the 14 biggest runners (up to +912%) had no coverage
at all.

Set NEWS_RANK_BOOST to 1.0 (neutral). This also neutralises the binary fallback
restored in 9b4c8e5 -- correct in intent (a dead enrichment must not silently
disable news ranking) but it handed a flat 2x to any unclassified headline, and
62% of them are lagging roundups.

Deliberately unchanged:
  - the LLM path. catalyst_score = conviction * max(0, sentiment), so a roundup
    (type none, sentiment <= 0) scores 0 and earns nothing, while a real bullish
    catalyst still lifts. It targets the 0.9% correctly.
  - the dilution veto, which sinks confirmed offerings and is a separate,
    sound mechanism.
  - the enrichment-down alarm from 9b4c8e5.

Also evaluated and NOT shipped: halt-resumption as a catalyst feature. It is by
far the best signal in this universe -- a >=3min bar gap resuming >=2% up ran
>=25% afterwards 40-47% of the time vs ~11-20% baseline, holding in every hour
of the session. But a time-aware A/B of the armed set moved mean forward run
only 27.09% -> 27.84% and 165 -> 173 runners per 725 slots, because gap x rvol
already encodes it (a halting stock has high relative volume by definition).
Same collapse premarket range showed: strong univariate, near-zero marginal.
The ranking is saturated; selection is not where the remaining edge is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-14 07:31:37 -05:00
momentum devandClaude Opus 5 45b0386347 feat: rank gappers by premarket range, the one measured predictor
Over 1,040 screened symbol-days (2026-08-01 on), asking which premarket
property predicts the session running >=50% open-to-high:

    premarket range   n     avg run   ran >=50%
    >= 30%           201     34.7%      15.9%
    15-30%           136     28.3%      14.7%
     5-15%           303     15.2%       5.9%
      < 5%           400     16.8%       4.3%

A name that already travelled >=15% premarket is 3.1x more likely to run
>=50% (15.4% vs 5.0%) -- the largest single lift in this dataset, and free:
it comes off minute bars the arm pass has already fetched.

Two findings constrain the shape:

  - It is a STEP at 15%, not a gradient (15-30% and >=30% are 14.7 vs 15.9;
    5-15% and <5% are 5.9 vs 4.3). So the multiplier is a threshold. A linear
    scale would invent precision the data does not support.

  - Premarket VOLUME carries NO signal (10.0/4.2/7.9/9.0% across participation
    buckets, non-monotonic) and raw premarket volume runs inversely to the
    outcome. scanner_snapshots.premarket_volume being NULL costs nothing;
    range is the field that mattered all along.

Do not expect 3x. Replaying armed-set selection over the same 29 sessions,
top-5 by mean/median run and >=50% runners captured out of 145 slots:

    gap x rvol (baseline)        39.77% / 10.97%   27
    x3.0 if PM range >= 15%      42.00% / 13.16%   32
    HARD FILTER >= 15%           37.27% / 12.67%   24   <- worse
    PM range alone               32.86% / 12.65%   20   <- much worse

gap x rvol already encodes much of the same information, so the marginal gain
is ~+2 points of mean run and 4-5 extra big runners per 145 slots. It is
BOOST-only for a reason: gating on this feature scored below baseline, and it
must never replace gap/rvol. Every threshold in 10-20% and gain in 1.5-5.0 beat
baseline -- a flat plateau, so not fitted. The shipped 15%/3.0x are the measured
step and the measured lift, not the sweep's peak cell.

Fail-open: an unknown range yields a neutral 1.0, never a penalty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-13 19:16:17 -05:00
momentum devandClaude Opus 5 ff63b19e91 Merge: exit retune, news pipeline restore, sizing and watcher fixes
Five fixes from the 2026-09-13 live-account investigation:

  aae7269  exits stopped amputating winners (9.3% of the move captured on
           trades that ran >=20%; winners held 10 min vs 19 for losers)
  9b4c8e5  news ranking had been off since 2026-08-03, two days before the
           first real-money fill -- and could not be seen to be off
  0910e29  track the live overrides seed that LIVE-CUTOVER.md references
  9034839  a zero equity reading sized off the $100k default (latent)
  97a3a1b  watcher stops scoring names the entry path can never accept
           (3.1 GB of a 7.6 GB events table)

Config changes are already applied to config_overrides on the live stack.
This merge is what ships the CODE half to the image.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-13 15:37:12 -05:00
momentum devandClaude Opus 5 97a3a1b29d 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
2026-09-13 14:27:06 -05:00
momentum devandClaude Opus 5 9034839661 fix: a zero equity reading sized off the $100k default
Tradier returns total_equity 0.0 during its overnight maintenance window --
sixteen times in the last fourteen days, all around 01:06. Three defects
compounded so that a zero could size a position off default_equity:

  account_equity = equity or config.default_equity

`or` cannot tell "not supplied" from "read as zero", so a real 0.0 became
100000. Then the position-value cap, derived from the same equity, was
itself 0 -- and the guard

  if max_position_value is not None and max_position_value > 0:

SKIPPED the cap precisely when the cap said "no room". Finally the auto
path (~96% of approval requests) had no `pos_cap <= 0` early-out, unlike
the manual path at request_entry_approval. Net effect on a ~$250 account:
risk 5% of 100k = $5,000, capped only by liquidity. The broker would have
rejected it for buying power, which is luck rather than design.

Never fired in live hours -- every zero read landed with the market closed,
and _current_equity caches the last good value -- so this is latent, not a
realised loss. The reachable path is a boot that coincides with the
maintenance window, when no good read has been cached yet.

Distinguish None from zero, refuse to size on non-positive equity, treat a
zero cap as "no room" rather than "no cap", and mirror the manual path's
early-out. TRADING_DEFAULT_EQUITY is set to 300 on the live stack so even
the untested fallback cannot oversize.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-13 13:34:53 -05:00
momentum devandClaude Opus 5 0910e29cc5 chore: track the live overrides seed
docs/LIVE-CUTOVER.md already points at this file, so the cutover procedure
referenced something that existed only on one machine. It carries no
secrets -- keys stay in .env (gitignored) and the LAN address it names is
already in .env.example, the compose YAMLs and the systemd unit.

Tracking it also pins the two settings this branch corrects, with the
reasoning inline: the exit retune, and the Ollama endpoint that had drifted
to a port with nothing listening. Untracked, a rebuild would have quietly
restored both faults.

Note the precedence: compose env < overrides.env < config_overrides (DB).
The database wins, so editing this file alone changes nothing on a running
stack -- it is the bootstrap seed and the record of intent, not the live
switch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-13 13:23:39 -05:00
momentum devandClaude Opus 5 9b4c8e5da5 fix: news ranking silently switched itself off for 40 days
step_arm ranks gappers by catalyst, but the enrichment feeding it stopped
on 2026-08-03 -- two days before the first real-money fill. raw_news_items
kept ingesting ~45k items/day throughout; news_events and
news_catalyst_cache have no rows after 2026-08-03. So every live trade to
date was ranked with the news multiplier pinned at 1.0.

Three faults, the third of which hid the other two:

1. Ollama was configured at a host/port with nothing listening, running a
   model that was never pulled.

2. _cat_mult returned a flat 1.0 for a symbol with no LLM advisory instead
   of falling back to the binary headline boost. With the scorer enabled
   and the cache empty that branch is reached for EVERY symbol, so turning
   on the graded ranker silently turned off the simple one it was meant to
   improve. Fall back properly, so news ranking degrades instead of
   disappearing.

3. enrich_recent_news never raises -- it reports failures in a returned
   counts dict, and enrich_loop discarded that return value. Every 120s
   for 40 days it received {enriched: 0, errors: 50} and threw it away: no
   log line, no event, nothing to distinguish a dead pipeline from an idle
   one. Consume the counts and emit a module_tick naming host and model,
   on the first failed pass and hourly thereafter.

Fault 3 is the one worth fixing; 1 and 2 are its first casualties.

No claim that news ranking pays: a binary "has a headline" flag tests
mildly NEGATIVE across 1,030 screened symbol-days (7.8% vs 10.4% ran
>=50%), and the graded classifier only overlaps bar data for five days,
too few to separate anything. Restoring the pipeline makes it measurable;
it does not make it trusted. Re-measure catalyst type and sentiment
against realised runs before giving the multiplier more authority.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-13 12:00:40 -05:00
momentum devandClaude Opus 5 aae7269538 fix: stop the exit stack amputating every winner
The live exit ran FOUR independent stop ratchets at once: a +5% breakeven
inherited silently from the env default, a 0.25R step-trail, a prior-low
trail, and profit-lock tiers. Stacked, they pinned the stop at or just
above entry within the first minute of any real move, so winners were
capped near +1R while losers still ran to the full stop.

Measured over 62 live Tradier + 172 Alpaca paper round trips:

  - trades whose stock ran >=20% after entry (avg +63.7%) kept 9.3% of it
  - median hold 10 min for winners vs 19 min for losers -- inverted
  - PCLA 2026-09-11: entered 12.8233, stopped 12.82, a 0.03% stop

breakeven_at_pct was the only exit lever in ExitConfig that defaulted ON
from the env (0.05) while its dataclass default and every sibling lever
defaulted to off, so the live stack ran it without ever setting it. Turn
the env default off so "not configured" means "not applied".

Replayed through simulate_exit -- same entries, same initial stops, only
the management varying -- with each stack split 70/30 by date. A 10%
trail running from entry looked best in-sample and went negative out-of-
sample; what survives is a trail armed only after +1R. The 8-20% range
all beat the old config in all four train/test cells (mean R 0.20-0.25 vs
0.125), so the live value of 12% is a plateau pick rather than a fitted
one.

Live config_overrides updated separately (market closed, no open
positions); this commit is the code default and the regression guard.
Risk per trade is unchanged and both safety backstops -- the -10%
catastrophe exit and naked-stop enforcement -- are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
2026-09-13 12:00:29 -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 390d6660ea fix: halt-aware flatten — let a working market sell rest instead of churning
A market sell that sits unfilled means the symbol is halted (or session
closed); cancelling and resubmitting it every exit-manager pass fills no
faster and spams the broker — YXT 2026-08-05: 87 market sells across a
10-minute LULD halt, 86 cancelled, plus one exit_naked_stop journal event
every ~4s. On a halt-down the resting sell is also the position's only
protection.

cancel_protective_and_close now returns False (nothing submitted) when a
market sell for the symbol is already working, and only cancels
non-market protective sells before closing. Exit-manager paths
(catastrophe / naked-stop / first-red / trail) journal the exit only when
a close was actually submitted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 19:01:18 -05:00
momentum devandClaude Fable 5 3871bda616 fix: label broker sync events by actual execution broker (tradier stack)
Tradier stack emitted account/positions/orders events hardcoded as
broker_name=alpaca_paper while the executor filters snapshots by
client.broker_name (tradier) — position cap and re-entry block read zero
positions, and the dashboard showed 'Alpaca Paper' over real Tradier values.
Mode label now defaults from BROKER/TRADIER_ENV (tradier_live) instead of
alpaca_paper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:52:01 -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 9390f3c2a6 perf: gate-receipts and loop-session projections stop shipping fat rows
/api/snapshots ran 33-60s even after retention pruned the events table —
profiling each projection found query_gate_receipts (21s) and
query_loop_session (10s) fetching thousands of full payload rows to
Python to keep 100 (or ONE). Both now select newest-first with SQL
LIMIT / single-column reads: 0.12s and 0.07s against prod. Remaining
projections (watch_states 6s, daily_performance 5s, approval_queue 3.5s)
are the same class — later pass if the snapshot still feels slow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 22:33:01 -05:00
momentum devandClaude Fable 5 f0fa104511 feat: cap entry notional at broker buying power (cash-account GFV guard)
On the Tradier live cash account buying_power maps to cash_available —
SETTLED cash. Capping every entry's notional at it means the bot never
spends today's sale proceeds today, which is the entire good-faith
-violation rule. That replaces the blunt 1-trade/day cap: the live
account now trades like paper (cap back to 5/day), throughput
self-limits at ~settled/position-size sequential trades and refreshes
each morning at T+1. On margin accounts buying power is ~4x equity so
the cap never binds — one universal rule, no broker branches. Fail-open
when the field is absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 09:21:55 -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 72a9ecbd0b fix: reasoning panel flicker + stuck loading on live ticks
Every SSE snapshot re-render rebuilt the expanded panel's HTML back to
'loading…' and re-fetched — panels flickered per tick and never looked
settled. Rendered panels now cache per symbol: re-renders repaint from
cache instantly, background refresh at most once/min, single-flight
guard against fetch races. Watch panel and trigger panel share the
cache; toggleCriteria's duplicate direct fetch removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:36:31 -05:00
momentum devandClaude Fable 5 a6be643fbb ci: build image on dashboard static changes too
api/static/** ships inside the image but wasn't in the docker workflow's
paths filter — dashboard-only commits (a6da02f trigger-row reasoning,
81bf154, da17be8) silently produced no image and only reached production
by riding along with later .py commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:20:46 -05:00
momentum devandClaude Fable 5 a6da02f809 feat: reasoning timeline on armed-trigger rows (click to expand)
The day's relevant names live in ARMED TRIGGERS — the reasoning panel now
opens right there: click a trigger row for the score/rvol timelines,
criteria lanes, and worded reasons, same renderer as the watch panel.
Caret + hover affordance; open panels survive snapshot re-renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:11:02 -05:00
momentum devandClaude Fable 5 2be23673be feat: reasoning-over-time drill-down — score/rvol timelines + criteria lanes + worded reasons
Clicking a watch-state row now shows HOW the verdict evolved, not just the
latest flags:
- setup score across the session vs the 60% gate (SVG line)
- relative volume vs its 2.5x floor
- per-criterion pass/fail lanes over the whole session (the variance of
  each logical step at a glance)
- each criterion's latest reason in words (rvol 4.9 (min 2.5), gap 80.9%,
  price vs breakout ...) instead of bare pass/fail marks

Backend: query_symbol_criteria gains history[] (server-side strided to
~240 samples) + detail[]. The generic LIKE scan took 12s/click over weeks
of criteria rows; now a partial expression index on the extracted symbol
(idx_events_criteria_symbol, built at boot) + 24h window + SQL-side
stride = <1s. Latest-verdict fallback keeps stale symbols and historical
views working (covered by existing projection tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 19:37:35 -05:00
momentum devandClaude Fable 5 0c28cf921f feat: one-click source links on catalyst feed headlines
Every LLM catalyst read's headline now links to the raw article
(raw_news_items.raw_url joined by headline), styled as a distinct dotted
underline per NN/g citation guidance — any AI verdict traces to its
source in one click. Rows without a matching raw item degrade to plain
text. Join verified against prod: 0.29s for the full feed query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:40:49 -05:00
momentum devandClaude Fable 5 81bf1540ef feat: explicit + prefix on positive P&L values (CVD accessibility)
Gain/loss no longer rides on green/red alone — red-green color vision
deficiency affects ~8% of males; the minus sign already printed, now the
plus does too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:36:41 -05:00
momentum devandClaude Fable 5 da17be8969 feat: dashboard readability pass — type hierarchy, P&L masthead, trigger-row cleanup
- Two-texture typography: labels move to the UI sans (11px, brighter muted
  #93a0bd), values stay tabular mono — label/value pairs parse instead of
  dissolving into uniform mono soup
- TOTAL P&L is the one dominant instrument: 34px + green/red state wash and
  accent bar on the cell (CSS :has on the existing pos/neg classes, no JS)
- Equity tile: available funds as a small sub-line (fixes the two-line wrap
  that broke the strip height)
- Armed triggers: single-line ellipsized meta/levels (was a 3-line crumb
  wrap), 15px symbols, ARMED/WEAK state chips, subtle zebra, thinner bars,
  wider levels cell so entry/stop/now never clip
- Verified with headless before/after renders against live data; zero JS
  logic changes, node --check clean

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:19:16 -05:00
momentum devandClaude Fable 5 47a64688d8 fix: replay universe = scanner-discovered + historical gappers; add year-scale targeted backfill
eod_replay's universe was today's top-100 by ABSOLUTE volume — small-cap
gappers never crack that list, so 2026-07-28 replay found 1 gapper on a
day the live loop signaled 8 names. Universe is now the union of (a) the
session's symbol_discovered events, (b) historical_gappers for that date,
(c) most-actives only on a same-day replay (past-date most-actives would
be look-ahead). Verified: 7/28 replay now screens 6 gappers / 4 trades,
matching the live day. Historical sessions also fetch that day's
04:00-20:00 ET SIP bars explicitly instead of a from-now lookback window
that pulled the wrong day.

backfill_gapper_days.py: for each of the ~260 recorded gapper sessions,
pull SIP minute bars (premarket incl.) + 40d of daily bars for that day's
gappers only, then build premarket_structure. Idempotent (skips sessions
already covered). Fattens n for premarket H1/H2, pyramid_1r_v1, and the
orderflow experiments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:20:38 -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 3d6bc36955 feat: Equity/Avail funds tile in dashboard P&L strip
Shows total_equity and buying_power (settled available cash) from the
latest account snapshot. On the Tradier live cash account these diverge
for days after a deposit or sale (T+1) — the bot sizes off equity but
buys need settled cash, so the gap explains rejected entries at a glance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 13:44:49 -05:00
momentum devandClaude Fable 5 3a28b0f6a7 docs: add regime-gate flip requirement to Tradier live runbook
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:17:56 -05:00
momentum devandClaude Fable 5 4f1ccde370 docs: Tradier real-money flip runbook (00 cash-account delta)
Delta to LIVE-CUTOVER.md (Alpaca-era): 3-line env flip on the existing
tradier stack, cash-account GFV cap (1 entry/day), equity floor 270,
sandbox-record truncation before first live boot, day-1 naked-stop drill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:17:33 -05:00
momentum devandClaude Fable 5 12f7fefec0 feat: nightly event retention + external telemetry push
- runtime/retention.py: prune 8 high-volume debug event types older than
  RETENTION_DAYS (default 14) once/day at RETENTION_HOUR (default 03:00).
  Audit events (orders, fills, closes, risk rules) are never pruned. The
  events table hit 6 GB in 3 weeks at ~340k rows/day; this caps it.
- runtime/telemetry_push.py: POST a JSON snapshot (equity, day realized
  P&L, trades/wins, open positions, last-event age) to TELEMETRY_PUSH_URL
  every TELEMETRY_PUSH_INTERVAL_SECONDS (default 300). Unset URL = step
  disabled. Push failures are reported in the step result, never raised —
  an external outage can't touch the loop. Works as healthchecks.io
  dead-man ping + payload log in one.
- wired both as scheduler steps in run_live_paper (retention gated by
  RETENTION_ENABLED, telemetry by TELEMETRY_PUSH_URL presence)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:25:09 -05:00
momentum devandClaude Fable 5 c5b43364be fix: don't fabricate last_equity for Tradier — realized P&L read $0 forever
Dashboard realized = broker equity delta (equity - last_equity). Tradier
balances carry no prior-session-close field and translate_balances echoed
current equity, making the delta permanently zero (win rate populated from
closed events while dollars showed $0). Absent last_equity now routes the
projection to its closed-events fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 09:39:48 -05:00
momentum devandClaude Fable 5 80422d91cd fix: bracket fills stay on legs; by-tag lookup surfaces entry-leg fill
General translation must not echo leg fills onto the OTOCO parent —
flatten_fills adds parent AND legs, double-counting entries in journal
reconstruction. The cancel-vs-fill reconcile still sees executed shares:
get_order_by_client_order_id surfaces the entry leg's fill on its result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 09:36:52 -05:00
momentum devandClaude Fable 5 a2de50479a fix: three Tradier sandbox incident fixes (2026-07-28 reject loop)
Incident: entry-timeout cancels raced lazy sandbox fills -> untracked
positions; naked-stop guard then force-sold but every sell rejected
('more shares than your current long position') for hours.

1. translate_order: Tradier OTOCO/OTO parents carry no top-level
   symbol/side/qty (legs only) — inherit from the entry leg so
   symbol-scoped lookups can see brackets; get_orders symbols filter
   also matches by leg symbols. Without this the stop-leg search read
   a fully-protected position as naked.
2. close_position: cancel the symbol's resting sell orders/legs first —
   Tradier reserves held shares against open sells, so selling without
   releasing them always rejects.
3. cancel_entry (broker-agnostic): after cancelling, re-check the
   broker's final word by client_order_id; journal a fill for any
   executed shares instead of orphaning a real position (2026-07-06
   class, same race exists on Alpaca).

49 tests pass incl. new coverage for all three.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 09:11:00 -05:00
momentum devandClaude Fable 5 bd7183c861 fix: mark Tradier positions off Alpaca real-time trades, not delayed quotes
Exit manager reads position current_price for breakeven/trail ratchets and
the stop-breach guard; Tradier sandbox quotes are 15-min delayed, so marks
lagged reality. Prefer the Alpaca data client's latest trades (real-time),
fall back to Tradier /markets/quotes per missing symbol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:39:46 -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 13c4b61e77 docs: tradier stack fresh-DB gotchas (missing research tables, knob seeding)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:04:50 -05:00
momentum devandClaude Fable 5 52144241b4 fix: dashboard broker badge reflects BROKER env (PAPER · TRADIER etc.)
Badge text was hardcoded to ALPACA; _broker_block now reports the active
broker name and derives paper/live from the Tradier sandbox host when
BROKER=tradier, keeping the base-URL-truth rule from plan 007.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:38:37 -05:00
momentum devandClaude Fable 5 154e44e8d5 fix: tradier stack compose must pin ALPACA_DATA_FEED=sip
Fresh DB has no config_overrides row, so without the compose-level pin the
parallel stack falls back to the thin IEX feed and its rvol/volume decisions
diverge from the main stack (which carries feed=sip in the DB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:12:14 -05:00
momentum devandClaude Fable 5 df85201c45 feat: Tradier parallel-stack compose template
Self-contained shakeout stack beside the paper stack: own postgres (host
5434, disposable pgdata-tradier volume), own dashboard (host 8768), loop
with BROKER=tradier (sandbox). Data plane stays Alpaca; Telegram/deadman
disabled so main-stack alerts stay unambiguous; main data dir mounted
read-only (inherits learned_params/overrides.env). Filled local copy is
git-excluded; bootstrap_live_db.sh playbook reused for DB parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 08:07:32 -05:00
momentum devandClaude Fable 5 1cefb51c63 feat: Tradier broker mirror — config-switch redundancy for Alpaca execution
tradier_broker/ mirrors every trading call the live loop makes on
AlpacaPaperClient (account, clock, positions, orders, submit incl.
bracket->OTOCO, cancel, replace, close) with identical signatures and
Alpaca-shaped returns, so trading_execution / exit_manager / flatten /
execution / sync run unchanged. Market data always delegates to Alpaca.

- BROKER=tradier (+ TRADIER_* env) swaps execution client in run_live_paper;
  default path untouched
- client_order_id -> tag preserves the lost-submit reconcile
- TradierApiError subclasses AlpacaApiError so the executor's
  rejection-vs-transport fork works as-is
- sandbox-verified: balances/clock/positions/orders, OTOCO preview
  (result:true), place -> tag lookup -> cancel round trip
- 18 unit tests (translation + client paths); executor integration checked
- docs/TRADIER-FALLBACK.md switch runbook

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 11:01:50 -05:00
momentum devandClaude Fable 5 48d75816b6 feat: backtest page — live-taken vs not-taken split + per-trade live P&L column
/api/backtest now annotates each replay trade with the live loop's realized
P&L for the same (session, symbol) and returns a live_split summary (taken vs
not-taken n/sumR/avgR). Page grows two summary cells, a Live P&L column with
an amber ≠ marker when sim and live disagree on a trade's sign, and a footer
caveat: minute-bar granularity + adverse fills flip individual rows — trust
aggregates, not rows. Read-only annotation; cached replay result unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 13:58:24 -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 537e0c7e12 docs: cutover config model — knobs in config_overrides table, seed-from-env migration
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 20:31:33 -05:00
momentum devandClaude Fable 5 495a3fa5d6 feat: strategy knobs in the database — config_overrides table, admin-managed
Owner preference: the bulk of TRADING_*/TRIGGER_* logic switches should live
in the DB, not the compose file (keys + connection info stay in compose —
the DB address cannot live inside the DB, and secrets don't belong in a
table the stack can SELECT). Precedence at boot: compose env < overrides.env
< config_overrides table; every entrypoint already routes through
env_overrides.load(), which now applies the table last. Admin /api/config
and profile-apply write the table (secrets whitelist unchanged); the config
page shows source=database. DB down = silent fall-through to file/compose,
boot never blocks on config. One-shot migration:
  python -m runtime.env_overrides seed-from-env
run in any stack container to copy its current effective knobs into the
table, after which the compose lines are dead weight strippable at the next
redeploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 20:31:16 -05:00
momentum devandClaude Fable 5 108f61a9bc docs: config model — strategy knobs in data/overrides.env via admin panel, compose keeps infra+secrets only
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 20:21:45 -05:00
momentum devandClaude Fable 5 de1973fbff fix: telemetry heartbeat insert needs an explicit uuid id (NOT NULL, no default)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:36:19 -05:00
momentum devandClaude Fable 5 625abee66f fix: tuner heartbeat also recorded in DB — file-only heartbeat was unverifiable
The 07-22 'tuner NOT running' alert could not be diagnosed remotely: the
heartbeat is a file on the scheduler container's volume, invisible to
everything else, and indistinguishable from a stale scheduler image or a
DATA_DIR mismatch. _write_heartbeat now also inserts a telemetry_events
row (best-effort); the boot staleness check falls back to the DB row when
the file is absent, so 'tuner ran, no profitable config' stops looking
like 'tuner dead'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:35:50 -05:00
momentum devandClaude Fable 5 148e300af4 docs: live-money cutover runbook + one-command DB seed script
docs/LIVE-CUTOVER.md: full flip-day procedure — staged pieces, discovered
dependencies (second paper key pair, DB seeding, learned_params copy,
telegram token, ports/dirs), 15-min cutover with the paper stack running
throughout, verification list, rollback. bootstrap_live_db.sh seeds the
live stack's Postgres from the paper nightly dump and truncates the
money-record tables so research history carries over while live P&L
starts at zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:29:06 -05:00
momentum devandClaude Fable 5 5287c8ebfe feat: live-money prep — equity-floor kill switch + boot order reconciliation
TRADING_EQUITY_FLOOR (0=off): equity at/below an absolute dollar floor
permanently halts the session and flattens the book — the total-drawdown
backstop the per-day breaker can't provide (a slow bleed of -2.9% days
never trips it). Same flatten path, loud risk event, bounce does not
re-open.

TRADING_BOOT_RECONCILE (default on): at boot, diff the broker's open
orders against today's submitted events — an open BUY on an unheld symbol
this process never submitted is a crash-restart orphan (the 2026-07-06
duplicate-order incident) and is cancelled. Orders on held symbols are
never touched (cancelling a bracket parent strips its protective stop).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:13:26 -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 21b38109aa test: clamp test pins BACKTEST_MAX_SESSIONS env (default moved to 300)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 09:30:14 -05:00
momentum devandClaude Fable 5 9d0a9df60e backtest: raise session clamp 45 -> 300 (env BACKTEST_MAX_SESSIONS)
The stored minute-bar history is 273 sessions (~13 months); the 45-session
clamp blocked months-scale ranges now that the nightly labeler keeps
setup_events current. Requests stay serialized behind _backtest_lock; a
full-history run takes minutes by operator choice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 09:29:55 -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 5f33799a9a fix: actually wire loop_session into the snapshot payload
The prior commit imported query_loop_session but a botched idempotency
guard skipped inserting the snapshot field (the import string itself
matched the 'already present' check) — ruff unused-import failed CI and
the stale-session banner had no data. Field added; banner live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 05:22:49 -05:00
momentum devandClaude Fable 5 2e39b756ba dashboard: stale-session alarm banner + rvol_tod in armed triggers
The 07-21 morning outage (loop booted post-EOD on the 20th, survived
midnight, silently blocked every entry while signals went READY) had zero
UI surface. New query_loop_session projection parses the live session id's
embedded date; a red banner fires when it isn't today's — the exact day-2
poison signature — telling the operator to restart the loop. Armed
Triggers rows now also show rvol_tod (the arming signal since 5597250)
next to legacy rvol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 20:09: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 3fdf268227 fix: trigger fires require a fresh cross — stale breaks after a blind window are chases
SNXX 2026-07-21: 18s after a mid-morning restart the fast path bought a
15-minute-old break +1.6% above the trigger — fires() only checked
price >= trigger, so any blind window (boot, halt, feed outage) fired
instantly on already-extended moves. A trigger now fires only if the live
price was seen BELOW the trigger since arming (a real cross), with a
small at-the-break tolerance (TRIGGER_FRESH_CROSS_TOL, default 1.5%)
so a fast break between ticks still fires; beyond it the name must pull
back below the level and re-cross. Gap-through runners stay covered by
the slow marketable path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 08:50:41 -05:00
momentum devandClaude Fable 5 256bdc31dc fix: clean-exit at date rollover — post-EOD boot ran overnight, day-2 took zero entries
A process booted AFTER the day's EOD flatten never gets exit-after-flatten,
survives midnight, and every memory-only daily flag (_session_closed, entry
caps, benches) stays stale — the documented day-2 limitation, hit live at the
2026-07-21 open (booted 17:11 the day before; signals READY + book armed but
zero gate evaluations, both entry paths silently blocked by _session_closed).
Main loop now exits cleanly when the session date rolls over; compose restarts
a fresh process that rebuilds the new day's state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 08:42:04 -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 f10a8033a1 chart: add volume histogram + Bollinger Bands + EXIT markers
Owner asks on the candlesticks:
- VOLUME: histogram series (bottom 20% overlay), coloured by candle direction.
- BOLLINGER BANDS: 20-period 2-sigma, computed client-side from closes; price
  riding the upper band = strong, break back inside = fading (momentum context).
- EXIT markers were NEVER wired — the fills feed carries only entries
  (order_submitted), and the loop fills brackets via position-diffing so no
  order_filled fires. Exits live in position_closed; now surfaced as square
  markers with reason + P&L (green win / red loss). Entry 'expected' (unfilled
  submit) markers labelled distinctly. Fixed a self-inflicted bug: queried the
  event store AFTER closing it — moved the position_closed fetch inside the
  open-store block. 11 chart tests green incl. new exit-marker test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:28:15 -05:00
momentum devandClaude Fable 5 e7bd8fa279 fix: repair DashboardState.__init__ (my prior commit orphaned _backtest_lock)
The self-heal commit (a00f967) inserted the _research method INSIDE __init__ at
method-body indent, truncating it so _change_signal/_backtest_lock/_backtest_
cache never got set -> AttributeError on backtest. Moved _research to a sibling
method; __init__ now sets all attrs. Two chart/sparkline tests updated: a None
research_con now REOPENS lazily, so 'no research db attached' is reached only
when the reopen also fails (they monkeypatch open to fail). 22 api tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 12:06:19 -05:00
momentum devandClaude Fable 5 a00f967bdf fix: dashboard research-DB connection self-heals — the 'TradingView failing' root cause
The charts weren't broken — the data pipe was empty. dashboard_api opened the
research connection ONCE at boot; if the DB was unreachable then (container
started before Postgres, or a blip) it stayed None forever, so /api/chart and
/api/bars returned 'no research db attached' and every candlestick + sparkline
rendered blank. Live-verified: GET /api/chart returned bars:[] reason:'no
research db attached' on the deployed dashboard. Now DashboardState._research()
opens the connection LAZILY and reopens if None — same self-heal as the
event-store emit fix. 2 tests; chart/backtest endpoint tests still green.
No library change: Lightweight Charts v5.2 is fine, it just had nothing to draw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 12:04:43 -05:00
momentum devandClaude Fable 5 5f04d5584a ui: surface hidden values + UI-REVIEW fixes 1 & 2
Owner: hover-only values were invisible. Now VISIBLE:
- Trades panel: Entry + Exit$ columns (projection now carries entry_price/
  exit_price; were hidden entirely).
- Armed triggers: entry/stop/now levels shown inline (were tooltip-only).
UI-REVIEW fix 1 (color): collapsed 3 greens->1 (#34d17e) and dup reds->
canonical #ff5a60 across CSS AND canvas markers (one --green/--red now).
UI-REVIEW fix 2 (a11y): :focus-visible outlines on all controls + aria-labels
on the money-path buttons (approve/reject/exit/flatten) — keyboard operators
were flying blind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:54:21 -05:00
momentum devandClaude Fable 5 dd13c41fc8 test(019): de-flake watchlist concurrency test — assert single-snapshot consistency
The test compared len(w) and list(w) as two SEPARATE locked reads, so the
churn thread could mutate between them (30 vs 31) — a bug in the test, not the
Watchlist (each read is individually consistent; there is no torn-read
guarantee ACROSS two calls). Now snapshots once and asserts that snapshot never
tears + concurrent contains/slice never raise. 5/5 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:46:43 -05:00
momentum devandClaude Fable 5 b3a47d775d 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>
2026-07-19 08:44:10 -05:00
momentum devandClaude Fable 5 a619a99f05 plans(019): move discovery off the scheduler — telemetry-proven 6-30s loop blocker
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:33:49 -05:00
momentum devandClaude Fable 5 5896d291d8 feat(018): sub-second exits on a dedicated thread + fast decision pipeline
Exits move off the single-threaded scheduler onto their own daemon thread at
1s (mirrors the fast trigger thread). Dedicated research connection (shared
facade isn't thread-safe); scheduler exits step auto-disabled when the thread
runs (single-caller invariant — no racing instance state, no double-submit);
manage() already idempotent (stop ratchets >1c up only). 3 driver tests + 539
suite green. On fast small-caps this is the 'get out fast' the owner asked for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:32:46 -05:00