Reshapes FundamentalsPanel to consume the additive API v1, within the app's
existing dark-glass language.
- types.ts updated to the exact v1 shape (metrics/earnings/valuation/reads +
legacy fields preserved).
- The quarter tape is the single distinctive device: per-metric 4-cell tape
(revenue/EPS growth, operating + FCF margin, share count) with the latest cell
toned by the deterministic read; color is always paired with the read text.
- Restrained peer strips for Net debt/EBITDA, P/E, FCF yield: value + a
polarity-aware percentile bar with a median marker + the read; hidden ("peers
n/a") when industry is null (< 5 peers).
- Earnings: next date/session/countdown + last-N beat/miss arrows (▲/▼/·) with
text aria-labels; explicit "no date" state.
- Explicit n/a, insufficient-peer, and no-earnings states; header shows the
deterministic sentence. Removed the hard-coded "FMP" source label.
Frontend tsc -b passes; backend suite 778 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the only qualifying R:R scan to 15:30 ET with chained Telegram alerts,
put outcome eval after a final-bar OHLCV fetch, enforce NY trading-day
requalify semantics, stamp paper trades fill_mode=near_close, and migrate
stored schedule_* keys to America/New_York.
Ship greenfield min_rr=2.0 and conf=0, read-only Structural S/R, indicator
cache invalidation, and UI/gate language that treats GTL as screening not exit.
Align strategy_rank missing-vol fallback live vs backtest, single-source
PRIMARY_TARGET_MIN_RR, expand prod parity tests, and drop dead FE clients.
Wikipedia no longer uses plain symbol table cells; parse exchange links and NyseSymbol templates, surface the list source in bootstrap results, and keep legacy cell parsing as a fallback.
The UI told a swing-trade story (entry -> target -> stop) while the engine runs
a momentum portfolio (buy strength, trail out, re-rank). The selection was
honest; everything around it was borrowed from a strategy we don't run.
The target is never an exit under `atr_trailing`: `_atr_trailing_close()` does
not even take it as a parameter. It exists only to compute the R:R and touch
odds that admit a setup through the activation gate. Backtested exit reasons for
the production strategy: 144 initial stop, 98 trailing stop, 78 max hold —
target 0. See docs/research/sr-levels-and-exits.md.
What changed:
- New ExitPlanPanel on every setup card states the rules that actually close the
trade: initial stop (1R), the price at which the 3x ATR trail takes over from
it, the trail width in R, and the max hold. Derived in lib/exitPlan.ts from the
live exit policy, so it follows Admin rather than hardcoding the default.
- New BaseRatesPanel replaces per-target "probability" as the answer to "what
usually happens": win rate, average hold, best/worst R, and how trades actually
ended — measured under the real exit, from the backtest report.
- "Target"/"target probability" relabelled to "level"/"touch odds" and grouped as
gate metrics, with the R:R. On the dashboard focus card, residual momentum
(the actual signal) takes the headline stat those two used to occupy.
- The take-trade dialog no longer offers a target dropdown whose value the exit
ignores; it states the trailing plan instead. The picker returns only when the
live policy is mode='target', where the choice is real. The stored target is
now the setup's own, not whichever row was last clicked while exploring.
- "Played out" is gone. A setup was declared dead once price reached the target —
backwards under a trailing exit, where reaching a level is the good case and
the trade keeps running. Only the stop invalidates a setup now; running past
the entry is an "extended" warning, measured in R (you'd be chasing).
The levels ladder, the price rail and the chart overlay all stay fully
explorable — clicking a level still drives them. It is framed as overhead
structure, which is what it is, rather than a menu of exits.
Adds a parity guard: the UI recovers ATR as |entry - stop| / 1.5, so the test
fails if the scanner's stop width ever moves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three follow-ups to the gate probability floor (8f41143):
- Signals table shows the starred primary target (shared primaryTarget
helper) instead of an independently computed max-probability best,
so Overview, Signals and ticker details agree by construction.
- Targets pinned at the 3% probability clamp floor collapse to the
nearest one (enhance_trade_setup + backtest candidates in parity):
floor-pinned levels are indistinguishable to the model, so farther
ones were duplicate 3% rows inviting lottery headlines.
- get_trade_setups only returns setups re-emitted within
LIVE_SETUP_MAX_AGE_DAYS (3): an older latest row means the daily
scan no longer confirms the setup, and such rows otherwise surface
forever on Overview/Signals/ticker/alerts. History endpoints keep
full history.
Backtest on the Jul-3 snapshot is metric-identical to the gate-floor
run on all qualified stats (1089 qualified, Sharpe 2.02, CAGR +49.6%,
DD -15.8%): the prune only removes noise the gate already rejected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A qualified setup's primary target must now clear MIN_TARGET_PROBABILITY
(20%), shared with the primary-selection floor in recommendation_service
and mirrored in the frontend gate. Closes the read-time hole where a
stale pre-c7a198b row starring a far lottery target (probability pinned
at the 3% clamp floor, R:R inflated by the same distance) qualified
forever: the scanner emits no replacement row and live R:R never decays.
A/B backtest vs c7a198b baseline (same July-3 snapshot): 7 of 1096
qualified setups removed; qualified net avg R 0.202 -> 0.207, hold
Sharpe 2.00 -> 2.02, CAGR +48.8% -> +49.6%, max DD unchanged at -15.8%.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the citron/green glass theme with the Horizon direction
(mockup at /design-horizon): space-void background, starfield, dim
Mars horizon, rim-cyan accent, rose for negative. Palette validated
for CVD separation and contrast on the dark surface.
- tailwind.config: gray -> cool space neutrals, blue/emerald -> cyan
scale, red -> rose; display font Space Grotesk
- globals.css: Horizon tokens, atmosphere, denser glass, cyan
buttons/inputs, price-rail / R-bar / radar chart CSS
- Dashboard rebuilt: verdict hero, top-pick card with spatial price
rail, KPI tiles, open positions as diverging R-bars with expandable
detail + trade chart (OHLCV since entry, entry/stop/target levels),
radar list with per-setup disqualify reason, watchlist chips
- OpenTradesPanel: table replaced by R-bar rows; all fields kept in
the drill-down (shares, P&L, alpha, trailing stop, sell)
- qualification: disqualifyReason() mirrors qualifiesSetup rule order
- CandlestickChart: canvas colors moved to Horizon palette; S/R now
cyan/neutral so rose stays reserved for the stop level
- ScoreCard: radar score fingerprint with hover values
- Design mockup pages included (routed in App.tsx)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A local backtest (offline prod snapshot, 506 tickers) evaluated blue-sky
projected targets under the PRODUCTION exit (3x ATR trailing + 30d max hold,
paper_trade_service DEFAULT_EXIT_MODE="atr_trailing"). Blue-sky setups are
dilutive: the qualified book scored 328% return / Sharpe 1.84 / DD -21.0%
WITHOUT them vs 300% / 1.58 / -18.7% WITH them. They rank high on momentum by
construction, so they grab slots from S/R setups that catch bigger runs under
a trailing-stop exit (only ~2pp worse drawdown doesn't justify the lost return
and Sharpe).
Reverts the scanner/TargetGenerator measured-move projection, the stricter
projected activation gate, the frontend qualification mirror, the `projected`
type field, and the projected tests -- all backend files are now byte-identical
to the pre-blue-sky commit.
Keeps the played-out "No current setup" UX (RecommendationPanel): when price
has run past the target (played out) or through the stop (invalidated), the
panel shows a plain no-setup state instead of a stale actionable card. This is
frontend-only (reads last close + existing setup fields) and is what actually
fixes the reported stale-below-price bug -- no backend change or rescan needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes stale below-price setups showing as current recommendations. Three
distinct causes share the symptom (get_trade_setups returns the latest stored
setup per direction and never expires it):
- Genuine blue-sky (no overhead S/R): scanner + TargetGenerator now project a
measured-move target (entry +/- 3*ATR, ~2:1 R:R), flagged projected with a
low sr_strength probability haircut. Overhead check keys on level tag OR price
so it never projects through a straddling resistance cluster.
- Projected targets clear a stricter activation bar (long-only, momentum >= 90,
confidence >= min+10), independent of the general momentum gate. Mirrored in
frontend qualification.ts.
- Played-out UX (fixes the reported TTWO case, which is R:R-starved under a
resistance cluster, not blue-sky): when price is at/past target or through the
stop, RecommendationPanel shows a "No current setup" state and softens the
stale ticker-level header/reasoning, instead of a stale actionable card.
No migration: the projected flag rides in existing targets_json. 504 backend
unit tests pass; frontend typechecks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backtest report now includes research-only hold-to-horizon portfolio variants comparing raw vs residual 12-1 momentum, cutoff 80 vs 90, max 10 vs 15 positions, and SPY-200 risk scaling. A dynamic research recommendation panel flags residual momentum, cutoff 90, or regime scaling only when transparent promotion rules pass.
Adds signal_context_snapshots with migration 016 and captures one point-in-time context row per newly generated TradeSetup: setup fields, composite/dimensions, latest sentiment, latest fundamentals, and strategy_version=momentum_12_1_rr_time_v1. This is forward-only; no historical sentiment/fundamental backfill is attempted.
No live gate, paper-trade exit, or production ranking behavior changes.
Verification: 458 backend tests pass, ruff check app/ clean, frontend npm run build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Production strategy change based on the July 2026 backtest: paper trades now default to a 30-trading-day hold with the initial stop (classic momentum hold-and-rerank), while target and trailing exits remain available in Admin. The exit policy API/UI now carries hold_days and close_reason can be 'time'.
The activation confidence floor default is now 0/off because the gate ablation showed it added no per-trade edge while filtering out usable setups. Migration 015 clears stored activation_min_confidence and paper_exit_mode so the new defaults take effect; this intentionally resets Track Record comparability from this deploy.
Verification: 451 backend tests pass, ruff check app/ clean, frontend npm run build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The robustness warning was computed on the target-model distribution
while the same panel recommends the hold exit — internally inconsistent.
_robustness_stats (median, profit factor, ex-top-5% expectancy) is now
shared by _bucket_stats and _time_exit_bucket, the time-exit table shows
Median Net R and Ex-Top-5% per hold length, and _build_recommendation
reads the trimmed expectancy from the recommended exit's bucket (falling
back to the target model when no hold is recommended).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Robustness (answers 'is the edge just outliers?'):
- _bucket_stats gains median_net_r, profit_factor, and net_avg_r_ex_top5
(expectancy with the top 5% of winners removed); shown as stat tiles.
- Portfolio sim gains per-calendar-year returns, shown in the sim table.
Dynamic recommendation ('What this backtest recommends' panel):
- _build_recommendation derives advice from the report's own numbers on
every run — exit policy (target vs best hold, with sim CAGRs), which
gate floors earn their keep (ablation Hold column), best momentum
cutoff, book-vs-SPY verdict, and an outlier-dependence warning when
the trimmed expectancy goes non-positive.
Retired (conclusions reached, tables removed from report + UI):
- Take-profit sweep (no interior optimum — fixed TP is the wrong tool
for momentum), trailing sweep (converged to the hold-to-horizon exit),
probability calibration (model is display-only by decision).
- _tp_primitives slimmed to _risk_and_stop_day; trailing machinery gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-trade additions to the report:
- Gap-through-stop fills: stops now fill at the worse of the stop or the
bar's open across every exit model (target, TP, trailing, time), so a
loss can exceed -1R; targets never fill better than their level.
- best_r / worst_r, avg holding days, and net R per day of capital
deployed on the summary buckets and the time-exit sweep.
Portfolio simulation (the stats a per-setup replay cannot give):
- One capital-constrained book over the qualified setups: 10k start, max
10 concurrent positions (one per ticker, best momentum first), 1%
fixed-fractional risk with a 20% no-leverage notional cap, entries at
the detection close, 0.1%/side costs, daily mark-to-market.
- Two exit policies compared: S/R target race vs hold-to-horizon.
- Equity-curve stats: final equity, total return, CAGR, max drawdown,
annualized daily Sharpe, win rate, avg P&L, best/worst trade, avg
hold, entries skipped on a full book, and SPY price return over the
same window (benchmark history refreshed to cover the replay span).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ablation judged floors under the target/stop model, but the exit
sweeps point at replacing that exit with a fixed hold — under which the
R:R floor's rationale (bigger payoff at the target) may not apply. Each
ablation row now also carries hold_avg_r / hold_net_avg_r / hold_total_r
(30d hold, initial stop only), so the Phase 3 gate decision can be read
under the exit policy that would actually be used.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 of the strategy-measurement plan — report-only, no production
trading behavior changes:
- Cost haircut: every bucket/sweep now reports net_avg_r/net_total_r
alongside gross (COST_PER_SIDE=0.1% of notional, converted to R via
each setup's stop distance); params carry cost_per_side_pct.
- Gate ablation table: re-qualifies candidates at the current momentum
cutoff with one floor removed per row (confidence / R:R / NEUTRAL /
momentum-only) to show which floors earn their keep.
- Time-based exit sweep: hold 5/10/21/30 days with the initial ATR stop,
exit at the day-N close — the classic momentum implementation, to
disambiguate the wide-trailing result.
- TP sweep extended to +40/+50%, trailing to 25/30% so the optima are
interior instead of starred at the sweep edge.
- BacktestPanel: Net Avg R columns everywhere, gate-ablation and
time-exit tables, stars now mark best net avg R; stale cached reports
still render (all new fields optional/guarded).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Store an optional company name on Ticker (migration 014) and backfill it from
Alpaca's asset list in a single Trading-API call for the whole universe — no
per-ticker fetch. Runs automatically at the end of universe bootstrap and via a
manual "Backfill Names" button (admin) / POST /admin/tickers/backfill-names.
The name ships on /tickers; a shared symbol→name map (useTickerNames) lets any view
show it without its own request. Displayed subtly next to the symbol — in the global
search, the ticker header, and as a small muted line under the symbol in Top Setups
and Open Trades (no extra column, truncated so it never widens the table).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Going from no sentiment to a bullish read used to be able to *lower* the composite:
sentiment was blended into the weighted average as an absolute level, so a bullish
75 diluted a ticker already scoring 78. That's backwards for a directional signal.
Now the non-sentiment dimensions form a re-normalized weighted-average base, and
sentiment is applied as a signed adjustment around neutral (50):
composite = clamp(base + MAX_ADJ * (sentiment - 50) / 50)
MAX_ADJ = sentiment weight * 100 (default weight 0.10 → ±10)
Neutral leaves the base unchanged, bullish adds and bearish subtracts (scaled by
confidence, since a 50%-confidence call maps to 50 → no effect), and no sentiment
never penalises. Default sentiment weight 0.15 → 0.10; the weight now means "max ±
points." Composite breakdown exposes base_score/sentiment_score/sentiment_adjustment,
and the ScoreCard shows "Base 78 · sentiment +5.0" plus the per-dimension adjustment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applies the backtest-validated trailing stop to live paper trading, and surfaces
it transparently.
Exit (A):
- New paper-trade exit policy (paper_exit_mode=trailing, paper_trailing_pct=12),
tunable in Admin → Paper-Trade Exit. resolve_open_trades runs a trailing stop
(initial stop as floor, ratchets up from the peak; target ignored — the
validated rule) and records close_reason (trailing|stop|target|manual; +migration
013).
- list_trades enriches open trades with the live trailing-stop level + distance %.
Open Trades panel shows the active tactic and a Trail Stop column.
Alerts (B):
- Daily digest now lists open trades with unrealized gain, trailing stop, and how
far away it is.
- New "trade closed" alert: one summary per auto-close (trailing/target/stop, not
manual) — direction, reason, days held, P&L abs+%/R — covering wins AND
stop-loss losses. Deduped by trade id; toggle in Admin alerts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third exit model alongside target-vs-stop and the fixed take-profit. The TP sweep
showed the edge lives in the fat tail (avg R keeps rising as you let winners run),
but a fixed wide target is win-rate-brutal and gives everything back on a reversal.
A trailing stop harvests the tail while protecting gains.
Per setup the replay computes the realized R for several trail widths (3/5/7/10/
15/20%) in a single conservative pass — stop ratchets up via max(initial_stop,
peak*(1-trail)), exit on the pullback or at the horizon close, R vs the initial
risk. Aggregated into a trailing sweep (win rate = share closed in profit, avg R,
total R) over the qualified set and shown as a new table in the Backtest panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The target-vs-stop model counts a near-miss of a far S/R target as a full loss
and ignores the partial gains you actually bank — so it measures a different
strategy than "scalp the early pop, take +8%". Add a realistic take-profit exit
model next to it (original untouched).
Per setup the replay now also records risk%, whether the stop was hit, the
favourable excursion reachable before the stop (MFE), and the horizon-close move.
From those a fixed-take-profit sweep (4/6/8/10/12/15%) is scored in R: bank +X%
if reached before the stop, else -1R, else the horizon close. Hit rate = how
often +X% was banked (the MFE CDF), so you can pick the EV-optimal TP without
top-ticking fantasy. Shown as a new table in the Backtest panel; the IC,
calibration and momentum sweep are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A NEUTRAL ("No Clear Setup") recommendation means the engine found no clear
directional trade, yet such setups could still qualify and even be crowned the
top pick purely on momentum rank (e.g. an extended momentum leader with a far,
5%-probability target). A NEUTRAL signal isn't actionable, so it shouldn't
qualify.
New `exclude_neutral` activation flag (default on): setup_qualifies drops setups
whose recommended_action is NEUTRAL. It lives in the shared gate, so it flows
through the dashboard's qualified/top-pick selection, the track record's
qualified stats, and the backtest (which computes recommended_action and gates on
meets_core). Toggleable in Admin → Settings → Activation; the frontend mirror and
activationSummary ("directional") match.
Re-run the backtest after enabling to confirm it holds/improves expectancy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fetching a symbol the provider doesn't cover (e.g. RHM/Rheinmetall — Alpaca
serves US listings only) returned 0 bars but reported "complete · Successfully
ingested 0 records", which the UI showed as green success.
fetch_and_ingest now returns a distinct `no_data` status when the provider
returns nothing AND the ticker has no history (vs. "already up to date" when bars
exist). The fetch endpoint maps it to a `warning` source status, and the fetch
toast renders it as ⚠ with the provider message instead of success.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The outcome stats were dominated by quick stop-outs: near stops resolve as losses
within days while far targets take weeks, so a young sample (mostly pending,
0 expired) skewed sharply negative (e.g. 13.8% hit / -0.46R vs the backtest's
35.8% / +0.18R) — a maturation artifact, not a real result.
get_performance_stats now counts only setups whose full ~30-day window has
elapsed (_MATURITY_DAYS), so winners had as long as losers (unbiased, and
comparable to the backtest). A new `maturing` count reports the younger setups
held back. The Track Record UI relabels "Evaluated" -> "Matured", shows the
maturing count, and explains the window in the empty state + methodology note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three usability fixes:
1. Global ticker search in the sidebar (TickerSearch) — typeahead over the
tracked universe that opens a ticker's detail page without adding it to the
watchlist. Also wired into the mobile nav.
2. Watchlist table shows the ticker's 12-1 momentum percentile (the top-pick
selector) instead of the noisy full S/R-level list. Enriched from the setup
already loaded in watchlist_service._enrich_entry — no extra query.
3. Alpha vs the S&P 500 on paper trades (open + closed). New benchmark_prices
table + benchmark_service store SPY daily closes (a standalone series, not a
Ticker, so it never enters the scanner / momentum ranking / rankings) via a
new daily-pipeline step. paper_trade_service computes per-trade
benchmark_return / alpha_pct / alpha_usd over each holding period; the open-
trades table, dashboard, and closed-trades panel surface per-trade and total
alpha. The list read path never makes a provider call.
Deploy: alembic upgrade head, then run the benchmark/daily job once to populate
SPY closes (alpha shows "—" until then).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two read-only pills in the ticker header, beside the watchlist toggle:
- "Top Pick" when the ticker is the current #1 — the same ranking the dashboard
highlights, via a shared topPickSymbol() helper so the two stay in sync.
- "Open Trade" when an open paper trade exists on the ticker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fires once when the regime monitor shifts quadrant (regime index x early
warning), so you don't have to watch the tab. Two guards against spam:
- Hysteresis: each axis only flips once the value crosses its divider by a
margin, so a point parked on a boundary keeps its quadrant instead of
flip-flopping day to day.
- Cooldown: a genuine change stays quiet for a few days after the last alert.
Seeds the baseline silently on first run; reuses the existing Telegram dispatch
+ AlertLog. New per-trigger toggle in Admin → Alerts (on by default).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plots the index, early-warning, and combined scores over time beneath the live
gauges, with a 1M/3M/6M/All range toggle and band reference lines — so the trend
and any divergence between the scores is visible, not just today's snapshot.
- Backend: GET /regime/history + get_regime_history (the three scores per
snapshot date from regime_snapshots).
- Frontend: recharts line chart, lazy-loaded so recharts ships in its own
regime-tab chunk instead of nearly doubling the main bundle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The event study showed the breadth-divergence signal genuinely leads (warned
before 7/11 drawdowns, ~6 weeks median, where the coincident baseline almost
never did). Surface it live to observe before deciding how to embed it — kept
separate from the index, not folded into its weights.
- regime_monitor daily job now computes breadth-divergence live and attaches a
separate early_warning score plus a combined blend (weighted mean, default
0.6/0.4, configurable via combined_weights) to each snapshot, including the
backfill so the 7/30-day trends populate immediately. Stored in breakdown_json
— no schema change. Best-effort: a breadth failure can't break the index.
- get_regime_monitor returns the index, early_warning, and combined scores each
with 7/30-day deltas.
- Regime tab shows three gauges (generalized ScoreGauge): coincident index,
early warning, and a compact combined blend. Stale snapshots render "—".
Note: the daily regime job now also does a universe-wide breadth scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first run gave only 2 events (N=2 is anecdote, not evidence) and an unfairly
weak coincident baseline, so the +42d lead couldn't be trusted. This makes the
measurement meaningful:
- More, cleaner events: default drawdown threshold 15%→10%, and dedup switched
from "recover to the high" to a rising-edge + cooldown (40d), so distinct
drawdowns each register instead of merging.
- Fair comparison: each indicator now warns at its OWN 80th percentile instead of
a shared absolute 60, removing the artifact that muted the coincident baseline.
- Per-event breakdown (date · depth · breadth lead · coincident lead) so a median
over a tiny sample can't hide an apples-to-oranges comparison — you see whether
both warned on the same drawdown.
- Surface precision/recall (best row) + base rate per indicator — the honest edge
read, not just lead time.
Re-run the Event Study job to regenerate the cached report in the new shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a leading-by-construction candidate and the harness to measure whether it
actually leads regime breaks, before any of it earns weight in the live index.
- breadth_service: % of the stored universe above its own 200-DMA + a divergence
score (benchmark price up while breadth falls, nudged by low breadth). Genuinely
leading because it keys on divergence, not level. Not wired into the live score.
- event_study_service: detect drawdown events on the benchmark, then measure each
indicator's median lead time (event-centered) and precision/recall vs. the base
rate (signal-centered). Compares breadth-divergence against the deterministic
coincident price composite (reuses the regime price sub-scores). Price/breadth
only — reproducible, no LLM/FRED.
- Manual "Event Study" job (Admin → Jobs), GET /regime/event-study, and an
inline early-warning panel on the Regime tab with an honest small-sample caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new /regime tab scoring how far the AI/Tech bull regime has deteriorated
toward a re-rating as a single 0-100 index with per-signal breakdown and a
7/30-day trend. Intentionally decoupled: nothing reads its output to gate or
score trades — the daily-pipeline membership is scheduling only.
- regime_monitor_service: price sub-scores (P1-P6 via Alpaca, like
market_regime), VIX + HY credit spreads via a small FRED helper, weighted
aggregation over available signals (missing source -> n/a, dropped from the
denominator), one snapshot row/day, and a ~90-day history backfill by
replaying the already-fetched series as-of each past day.
- F1/F3 fundamentals proposed by the configured grounded LLM (reuses
sentiment_provider_service config resolution), with a manual override + lock.
- regime_snapshots table (migration 011); endpoints on the existing market
router; admin-editable weights/threshold; standalone /regime page.
Data needs: prices via Alpaca, VIX/credit via FRED (optional key — signals show
n/a without it). No LLM needed for history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
min_target_probability is gone: it filtered on the probability model the
calibration has repeatedly shown to be weak and overconfident, it was redundant
with the momentum gate, and as an off-by-default knob it just invited bad tuning.
Removed from the backend gate, activation config/schema, the frontend mirror
(qualifiesSetup / activationSummary), and ActivationSettings. The probability
model stays where it does real work (primary-target selection + display).
Charts: with multi-year history the all-bars default was unreadable. Added
time-range presets (1M / 3M / 6M / YTD / 1Y / 3Y / 5Y / All), defaulting to 1Y;
clicking a preset always re-applies (snaps back after a manual zoom). Y-axis
autoscale and wheel-zoom / drag-pan were already there.
339 backend tests pass; frontend build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Part 1 — long-only. The momentum edge is long top-momentum; the gate was
qualifying shorts on high-momentum names (fighting the trend), which showed as
the -0.13R Short(qual.) drag. While the gate is active, shorts no longer qualify
(backend qualification, backtest _momentum_qualifies, and the frontend mirror).
Part 2 — production wiring. Live setups now carry a real momentum rank, so the
dashboard, the Track Record's qualified stats, and outcome evaluation all gate on
the same value instead of deferring to floors:
- new momentum_service.compute_momentum_percentiles: 12-1 momentum per ticker,
ranked across the universe into a {symbol: percentile} map.
- the daily R:R scan ranks the universe up front and stores each setup's
percentile (new trade_setups.momentum_percentile column, migration 010).
- enhance_trade_setup mutates the same row, so the percentile is preserved;
_trade_setup_to_dict + TradeSetupResponse expose it to the API.
Until a fresh scan runs, pre-existing setups have a null percentile and the gate
falls back to floors for them (longs) / excludes them (shorts) — they fill in on
the next scan. 341 backend tests pass; frontend build clean.
Needs the alembic upgrade (migration 010) on deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>