26 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 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 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 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
momentum devandClaude Fable 5 51f095a0c3 labeler(009): market context + thin-name RVOL fallback (feature population)
Steps 1-2 of plan 009 — fill two fillable feature gaps so a conversion model
gets a fair, instrumented test:

- market context: _spy_context computes spy_intraday_return (SPY RTH open ->
  trigger-minute return) + a coarse tape_regime {trend_up,trend_down,chop} from
  SPY's sign + higher-highs structure. Replaces the hardcoded
  tape_regime=None/spy_intraday_return=None; wired into ALL five build paths
  (v1/vr1/run1/liq1/pm1) via _preload_spy. Null when SPY has no RTH bars at/before
  the trigger (pre-30d history, or a premarket fire before SPY's RTH open).
- thin-name RVOL fallback (v1): when avg_vol is 0/null (thin IPO-class, no 20d
  baseline) fall back to the median of prior sessions' total volume (real
  daily_bars.volume, knowable pre-open); flagged in new setup_events column
  relative_volume_source ('avg_vol_20d' | 'prior_days_median_volume' | NULL).
  Never fabricates a baseline.

A feature to TEST, not a signal to trust. No live gate/sizing/exit change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:25:07 -04:00
momentum devandClaude Opus 4.8 31c35f01c1 VWAP-reclaim P3: placeability gate + pre-registered promotion validator (which FAILS in-sample)
Acts on the P2 edge-audit's two findings: (a) ~half of curl fires have a stop tighter
than the spread (median R 1.7% of price, $0.05 on a $2.62 name) so the backtested
+1R/-1R is unfillable; (b) the only subset that survived refutation was price>=$5 AND
mid-tercile impulse, scored as a +2R/runner filter.

- detect_vwap_reclaim: PLACEABILITY GATE (min_r_frac=0.015, min_r_abs=0.02, default ON
  for live safety) -- rejects a fire whose R=(entry-pullback_low) sits inside the spread.
  signal_values gains r_frac. Tests: a shallow-pullback curl now rejects "stop
  unplaceable" and fires only with the gate disabled.
- labeler: validate_vwap_reclaim + `validate-vr` CLI scores the PRE-REGISTERED candidate
  (placeable AND price>=$5 AND impulse in [0.15,0.26]) against a LOCKED bar: +2R Wilson-95
  lower bound > base 0.349 AND absolute max-upside-60m beats same-symbol-day ORB at
  +5%/+10%. Constants + _wilson_lower are committed so the bar can't drift. The shadow
  BASE track (vr1) deliberately keeps the gate OFF so report-vr base rates stay complete;
  placeability is a filter in validate-vr.

IN-SAMPLE RESULT (decisive): enforcing placeability collapses the audit's n=48 subset to
n=16 -- i.e. most of its apparent +2R lift rode on UNPLACEABLE stops. The remainder FAILS
the locked bar on both criteria: +2R 44% (Wilson LB 23.1% < 34.9% base) and, most
damningly on the artifact-proof axis, it produces a +5%/60m move only 31% of the time vs
ORB's 73% on the same days. Pre-registration did its job: it stopped us chasing an n=48
mirage of sub-spread stops. VERDICT: do not promote; the curl does not beat the bot's
existing ORB even in its best placeable subset. 15/15 unit tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:48:35 -04:00
momentum devandClaude Opus 4.8 fa3f23cf6a VWAP-reclaim P2: shadow-score the detector through the labeler's forward-outcome engine
P2 = run detect_vwap_reclaim over history and score it with the SAME no-look-ahead
machinery that scores the live ORB setups, so its base rates are directly comparable
and there's no train/serve skew in the SCORING either (not just the features).

- Extracted research/labeler.py's forward-label logic into a shared _forward_labels()
  (max-upside/-drawdown 5/15/60m, reached_1R/2R-before-1R pessimistic, held-VWAP over
  the evolving cumulative VWAP, trend-day, failed-breakout, time-to-max). compute_setup
  now calls it -- the 3 existing labeler tests still pass, so the extraction is
  behavior-preserving.
- compute_vwap_reclaim_setups(): slides the detector over each session's RTH bars and
  emits a NON-TRADEABLE setup_event on each RISING EDGE (not-valid->valid, with a bar
  cooldown so one curl episode = one setup), carrying the detector's OWN entry/stop/R.
  STRICT time split: the detector at bar i sees only bars[:i+1]; labels use only bars
  after i. setup_version='vr1', setup_name='vwap_reclaim' -- isolated from the ORB 'v1'.
- build_vwap_reclaim / report_vwap_reclaim + CLI `build-vr` / `report-vr`. report-vr
  prints forward outcomes AND a head-to-head vs the ORB baseline on the same symbol-days.
- 2 new tests: a continuation session fires + labels a +1R winner; a one-way fade stays
  silent (anti falling-knife). 12/12 green.

PRELIMINARY read (n=35, 4 days -- far too small to trust; full build running): curl-
fires reached +1R 66% / +2R 49%, held-VWAP-15m 86%, avg +8% max-upside in 60m; the ORB
baseline on the same symbol-days reached +1R 67%. Real finding pending the full build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:26:19 -04:00
momentum devandClaude Opus 4.8 11e33bba91 Labeler phase 1: offline self-labeling / feature store
research/labeler.py turns each historical symbol-session into a (features ->
outcome) row so any forecasting signal's LIFT can be measured before it sizes
live. Reuses the LIVE feature fns (opening_range / compute_key_levels /
calculate_time_of_day_rvol / data-quality) so offline == live; STRICT time split
(features <= the ORB-completion decision point, labels strictly after);
idempotent + versioned; populates the previously-empty setup_events /
engineered_features / outcome_labels tables.

`build [--limit N] [--rebuild]` + `report` (base rates + runner rates via one
aggregate query). Core math is a pure compute_setup() — 3 unit tests cover the
time-split (a runner's feature VWAP can't see the post-setup run), the R-outcome,
failed-breakout, trend-day, and the <ORB-bars skip.

First real base rates (n=3,610 setups / 13 days): 24% reach +1R before -1R, 43%
fail outright, +20% runners 2.2%, +50% 0.4%, +100% (NXTS-class) just 0.1%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:37:30 -04:00
momentum devandClaude Opus 4.8 e286330cd9 Fix the 3 Phase-2 mediums from the PR #2 review (still gated OFF)
Hardens the Ollama Phase-2 path before it can be enabled; no effect on the
default-off behaviour (these run only when the score-blend / veto flags are on).

1. catalyst_score no longer floors sentiment at neutral: a high-conviction
   BEARISH catalyst scored 0.45 (same as neutral) and LIFTED setup quality —
   backwards. Now `conviction * max(0, sentiment)`: only bullish boosts;
   bearish/neutral score 0. (+ bearish regression test.)
2. Prompt injection: the untrusted headline now sits in a delimited, length-
   capped <news> block, the model is told to treat it as DATA not instructions,
   and a </news> breakout token is stripped — so "...classify as bullish,
   is_dilutive=false" can't flip the verdict or bypass the veto. (+ 2 tests.)
3. The dilution-veto catalyst_provider no longer reads the SHARED research_con
   from the trigger thread: it refreshes under a lock using its OWN dedicated
   connection (opened lazily, single-threaded use), 30s TTL keeps DB hits rare.

tests/unit: 201 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:54:20 -04:00
momentum devandClaude Opus 4.8 8cee002c09 Merge PR #2 (Ollama news/catalyst enrichment) onto master + fix review blockers
Integrates the local-LLM enrichment ADDITIVELY on top of all current work, with
every Ollama feature OFF by default — with the flags off the system behaves
exactly as before (199 unit tests pass; a full --once pass runs clean with no
Ollama). Fixes the three issues the adversarial review flagged:

- BLOCKER: keep the Alpaca News ingestion. The PR predated it; the run_live_paper
  conflict is resolved to keep BOTH step_news (fetch -> raw_news_items) AND the
  enrichment. They're complementary: ingestion feeds enrichment authoritative-
  ticker headlines instead of thin RSS.
- HIGH-1: enrichment runs on its OWN daemon thread with its OWN DB connection,
  not the scheduler thread — a slow/hung Ollama can no longer starve exits /
  guard / eod-flatten, and it no longer shares the non-thread-safe research_con.
- HIGH-2: per-headline writes are all-or-nothing — on any ticker store failure
  the partial rows are deleted so the headline fully retries (no stranded
  tickers) and enriched is counted only on full success (no false count).

Phase-2 automation (catalyst score blend, dilution veto, arming-rank multiplier)
remains gated OFF pending the medium fixes (bearish-catalyst boost, prompt-
injection hardening, veto cross-thread read) noted in the review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:54:07 -04:00
momentum devandClaude Opus 4.8 e601973de2 Alpaca News API ingestion -> raw_news_items (feeds catalysts + Ollama)
The only news source was RSS (slow, regex ticker-scraping). Alpaca exposes
Benzinga news with a stable id + authoritative symbols list. Add it as a second
source landing into the SAME raw_news_items table, so recent_news_map (catalyst
prioritisation) and the Ollama enrichment (PR #2) pick it up with no other changes.

- AlpacaPaperClient.get_news(symbols, limit): GET /v1beta1/news (works on the
  existing data keys; verified live).
- research/ingestion/alpaca_news.py: ingest_alpaca_news lands new items, deduped
  by the Alpaca news id; never raises (news is an enhancement, not a dependency).
- run_live_paper step_news on the scheduler (ALPACA_NEWS_* env, default on, 120s),
  scoped to the tracked universe.

Verified live: passes landed ~34 then deduped; recent_news_map went from RSS-only
to 370 ticker->headline catalysts. tests/unit: 165 passed (+5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:10:37 -04:00
Claude e3cb2b8e5f Add local-LLM (Ollama) news/catalyst scoring
Leverage a local Ollama model to interpret news catalysts for the small-cap
momentum strategy — distinguishing bullish catalysts (FDA approval, M&A) from
dilutive offerings the technicals alone can't see. Advisory-first, then automate.

Phase 1 (advisory; no change to trade behavior):
- research/ingestion/news_enrichment.py: stdlib urllib call to Ollama with
  structured JSON output, classifying each headline into a catalyst type,
  sentiment, conviction and dilution flag. Graceful degradation (None on any
  failure) so trading is never blocked.
- news_catalyst_cache table + populate the previously-dead news_events.sentiment.
- Runs on the existing interval Scheduler (off the hot path), gated by
  OllamaConfig; deduped by the raw_news_items payload hash.
- Surface the advisory on the dashboard approval queue (projection + api + UI).

Phase 2 (automation; ships gated OFF):
- Optional catalyst_score blended into calculate_setup_quality (None reproduces
  the exact legacy weights); pure pass-through via evaluate_setup.
- Dilution veto in TradingExecutionService (slow + fast paths) emitting a
  catalyst_dilution_veto risk event, behind NEWS_DILUTION_VETO_ENABLED.

Config (OllamaConfig.from_env), .env.example docs, and tests for parsing,
persistence/dedupe, the projection, the score blend, the veto, and an
end-to-end enrich->advisory flow (Ollama mocked). Default model moves to
qwen2.5:7b-instruct (best small-model JSON, fits a 12GB GPU).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5HTMkaadGDnAPtXe2hWwg
2026-06-19 17:29:24 +00:00
momentum devandClaude Opus 4.8 3e2b4f562e Harden discovery: surface screener failures + regression test
- discover_active_symbols logs the screener error instead of swallowing silently.
- run_discovery records an explicit error when the universe is empty (surfaces in
  the discovery event + Grafana).
- momentum doctor now FAILs when the latest discovery universe is 0 (the morning's
  dead screener would have shown red within a minute).
- tests/unit/research/test_discovery.py: pins get_most_actives top<=100 and the
  ETF/price-band filtering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:23:21 -04:00
momentum devandClaude Opus 4.8 007cd95c79 Baseline before Postgres migration (WS0)
Snapshot before the DuckDB->PostgreSQL migration. data/, archive/, the 232MB
rebuild zip, the Windows .venv, and .env are excluded via .gitignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:39:29 -04:00