Commit Graph
100 Commits
Author SHA1 Message Date
dennisthiessenandClaude Opus 4.8 85b3ef618f Research: S/R levels, the target exit, and the entry gate
Investigated whether our support/resistance detection follows best practice
and whether we actually use it that way. Three findings, all backed by runs
against the prod snapshot and written up in docs/research/sr-levels-and-exits.md:

- The S/R target must NOT become an exit. Honoring it as a take-profit on top
  of the 3x ATR trail drops Sharpe 2.04 -> 1.47 and halves CAGR. Win rate rises
  (37.5% -> 40.0%), which is the tell: it truncates the right tail where
  momentum's edge lives.
- The clear-air fallback (synthesize a 3xATR target where no resistance exists,
  so 52-week-high breakouts stop being vetoed) looked strictly better in-sample
  (Sharpe 2.04 -> 2.07, CAGR 50.4% -> 62.3%, DD 21.4% -> 20.1%) but FAILED a
  real out-of-sample holdout: on entries after 2024-07-01 it is worse on Sharpe
  (2.78 -> 2.45) and Calmar, better only on raw CAGR. Not shipped.
- The detector itself is weak vs best practice (POC/VAH/VAL computed then
  discarded, HVN = any above-mean bin, 1.48x volume double-counting, "touch"
  counts pass-throughs, no round numbers), but its only causal path to P&L is
  the entry gate. Fix it for the displayed levels, not for returns.

Method note: nested lookback windows are NOT out-of-sample. The in-sample result
was clean, large, and consistent across five windows, and still did not survive
a proper entry-date split.

All research paths are off by default and the default report is unchanged:
  BACKTEST_RESEARCH_EXITS=1        take-profit exit rows
  BACKTEST_ATR_TARGET_FALLBACK=k   synthetic k*ATR target when S/R offers none
  BACKTEST_FALLBACK_CLEAR_AIR_ONLY=1  restrict that to genuinely clear air
  BACKTEST_HOLDOUT_SPLIT=YYYY-MM-DD   train/test split by entry date

Also fixes two reproducibility holes found while reconciling our local baseline
against the live report:

- create_backtest_snapshot.py now copies paper_% settings. The production
  monitor row replays the runtime exit policy via get_exit_policy(); without
  those keys a snapshot silently falls back to code defaults, so a live-tuned
  exit would never be reflected.
- Migration 020 drops activation_min_expected_value and
  activation_min_target_probability. Both are orphans of the June EV-gate
  redesign, read by no code path, but prod carries min_target_probability = 50.0
  which implies a probability floor that is not enforced (the real floor is the
  20% constant in qualification.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 15:05:34 +02:00
dennisthiessenandClaude Opus 4.8 fa26ec3ec4 Track backtest reports in git; add a report comparison tool
The reports are the evidence behind the production baseline, so they belong
next to the README that quotes them rather than living only on one machine.
Un-ignores reports/*.json (~2.6 MB compressed for all 11); the snapshot DBs
they run against stay ignored.

Renames the reports to a single dated scheme so they sort chronologically and
say what they measured. Each name is derived from the report's own contents
(the atr_trail_sweep / regime_overlay / blue_sky_projected / sizing_test
sections, and the qualified counts that identify the A/B arms), not from the
ad-hoc slugs they carried before. The run the README quotes is now
backtest-20260711-prod-baseline.json.

reports/compare_reports.py loads every report into one sortable table
(portfolio monitor, entry variants, exit policies, portfolio sim), filters by
report and lookback, and highlights the best row for a chosen metric — max
drawdown correctly ranking lowest-as-best. Stdlib tkinter, no dependencies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:04:13 +02:00
dennisthiessenandClaude Opus 4.8 88527f39b6 Refresh production baseline numbers in the README
The baseline table and promotion evidence still carried pre-primary-target-floor
figures. Re-derived every number from the 2026-07-11 run, the first baseline
measured after the 20% probability floor pruned lottery targets (1,428 -> 1,089
qualified).

The promotion evidence table also claimed the promoted book beat legacy on
"CAGR, Sharpe, and drawdown". That no longer holds: legacy residual 80 + hold
now has the shallowest drawdown (-15.8% vs -21.4%). Production still wins on
Sharpe, so the promotion stands, but the text now says so honestly rather than
implying a clean sweep.

Also documents the primary-target reach-probability floor in the gate
description, which shipped in c7a198b/8f41143 but never reached the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:03:47 +02:00
dennisthiessen cb215e2595 Merge branch 'main' of ssh://git.thiessen.io:2266/dennisthiessen/signal-platform
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m12s
Deploy / deploy (push) Successful in 37s
# Conflicts:
#	app/services/fundamental_service.py
#	app/services/rr_scanner_service.py
#	app/services/scoring_service.py
2026-07-11 17:03:25 +02:00
dennisthiessen 727b147c81 Withhold stale-score trade recommendations 2026-07-11 16:56:52 +02:00
dennisthiessenandClaude Fable 5 292b9934b1 Scan survives score-refresh failures; fix expired-ORM crash after rollback
A scoring error no longer skips setup detection for the ticker: the
rollback already restores a clean transaction, and qualification
re-gates on live scores at alert time, so a stale score is recoverable
but a skipped scan is not.

Iterating symbol strings instead of Ticker instances fixes a latent
crash the new regression test caught: rollback() expires ORM objects
regardless of expire_on_commit, so touching ticker.symbol in the except
handler triggered sync lazy-loading, which raises on an AsyncSession
and killed the whole scan on the first per-ticker error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:29:06 +02:00
dennisthiessen 9450831ef3 Optimize signal read paths and enforce score invariants 2026-07-11 16:04:13 +02:00
dennisthiessen 25364f8e99 Optimize signal read paths and enforce score invariants
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m11s
Deploy / deploy (push) Successful in 37s
2026-07-11 13:36:59 +02:00
dennisthiessenandClaude Fable 5 fdc49d0e28 Setup views: primary-target column, floor-target prune, liveness cutoff
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>
2026-07-11 10:06:34 +02:00
dennisthiessenandClaude Fable 5 8f411435ee Activation gate: primary target probability floor (>= 20%)
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>
2026-07-11 09:31:28 +02:00
dennisthiessenandClaude Fable 5 924c474624 Track Record: Strategy/Lookback selects use the shared Dropdown
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m3s
Deploy / deploy (push) Successful in 34s
The portfolio-monitor filters were native <select> elements with a
hardcoded bg-slate-950 - a look from before the design system. They
now use the shared Dropdown (same as the Setups filters), which also
gives them the dark themed menu and the portal fix from 6464fdd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:23:09 +02:00
dennisthiessenandClaude Fable 5 6464fdd4af Dropdown menus paint under later glass panels - portal them out
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m6s
Deploy / deploy (push) Successful in 35s
The Signals > Setups filter dropdowns (Direction, Recommended Action)
opened UNDER the panels below: glass containers use backdrop-filter,
which creates a stacking context, so the menu's z-50 could never
compete with later sibling panels regardless of value.

The menu now portals to document.body with fixed positioning tracked
against the trigger (re-measured on scroll/resize), and click-outside
accounts for the portaled node. Applies to every Dropdown usage.
Menu background also updated from the old green-tinted #151911 to the
Horizon #14161f.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:20:04 +02:00
dennisthiessenandClaude Fable 5 c3dd80e203 Performance chart under positions; remove obsolete files
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m6s
Deploy / deploy (push) Successful in 35s
- PerfChart moves into the left column under Open Positions
  (half-width), balancing the radar column.

Cleanup:
- Design mockup pages (DesignMockups / DesignHorizon / DesignOrbit)
  and their routes removed - the redesign has shipped; the mockups
  live on in git history. Main bundle shrinks ~97 kB and the
  chunk-size warning goes away.
- frontend/tsconfig.tsbuildinfo untracked and gitignored (incremental
  build artifact that churned in every commit).
- Dead npm test / test:watch scripts removed - vitest was never a
  dependency; CI already skips the step via its vitest guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:07:22 +02:00
dennisthiessenandClaude Fable 5 79010a5dae Overview: restore wide focus card and half-width positions
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m13s
Deploy / deploy (push) Successful in 38s
The focus|radar 3/5-2/5 pairing made the top-pick card cramped and the
full-width positions panel stretched its trade charts. Back to the
layout that worked: focus card full width, account ribbons, then
positions | radar side by side, performance chart at the bottom.

Kept from the rework: radar rows still swap the focus card - and now
reliably, because the ticker symbol inside a row is no longer a
navigation link (clicking anywhere in the row, symbol included,
selects it; the ticker page is reachable via the focus card's Ticker
details button). Below-gate inspection, badges, and the reset link
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:47:40 +02:00
dennisthiessenandClaude Fable 5 06fdd92faa Overview: focus|radar pairing, selectable radar, performance chart
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m9s
Deploy / deploy (push) Successful in 36s
Layout regrouped by relationship, not size: the setup-in-focus card
and the radar sit side by side (they are one decision surface), the
four account ribbons move directly above the open positions they
describe, and a new performance chart closes the page.

- Radar rows are selectable: clicking one swaps the focus card to that
  setup - including below-gate rows, whose card shows a muted
  "rank N / below gate" badge and the disqualify reason in the footer,
  with a "back to top pick" reset. The row currently in focus is
  highlighted; ticker links still deep-link without selecting.
- Performance chart (the mockup's missing piece): new
  GET /paper-trades/equity-curve computes, per benchmark trading day
  since the first paper trade, the book's cumulative P&L (realized +
  mark-to-market from stored OHLCV) vs the same cost basis riding SPY
  over each trade's window (benchmark_prices). Pure curve math in
  paper_trade_service with unit tests; hidden until there are 2+
  points of data. Frontend renders both lines with crosshair readout,
  zero baseline, and direct end labels.

Backend unit suite: 501 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:09:31 +02:00
dennisthiessenandClaude Fable 5 b0b691da9c Overview: regime to the nav bar, watchlist up top
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m5s
Deploy / deploy (push) Successful in 35s
The market regime banner took a full-width row on the dashboard for
ambient context. It is now a subtle status in the top bar (colored dot
+ "neutral regime", full headline and the longs/shorts caution in the
tooltip) linking to /regime - matching the mockup's nav.

The watchlist strip moves from the bottom of the dashboard (where it
got lost) into the freed spot right under the hero: a slim chip row
with today's move per symbol and the full-watchlist link. The empty
state is a one-line hint instead of a callout box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:31:41 +02:00
dennisthiessenandClaude Fable 5 72054d06b0 Radar axes: one canonical order everywhere
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m3s
Deploy / deploy (push) Successful in 35s
The dashboard fingerprints sorted score dimensions alphabetically
while the ticker page rendered them in API order, so the same ticker
drew a differently-shaped polygon on each page.

New shared helper radarAxesFromDimensions() pins the axis order to the
backend's DIMENSIONS list (technical, sr_quality, sentiment,
fundamental, momentum; unknown dimensions append alphabetically) and
centralizes the label truncation. Used by the dashboard fingerprint
strip, the ticker header radar, and ScoreCard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:25:26 +02:00
dennisthiessenandClaude Fable 5 c7a198ba8e Primary target: probability floor stops lottery headlines
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 34s
A setup's primary target could carry a ~3% probability: the picker
chose the most likely target among those with R:R >= 1.5, and after a
run-up that pool can contain only far "lottery" levels (the near,
likely levels fail the R:R floor). The lottery target's inflated R:R
then became the setup's headline and passed the activation gate's
min_rr floor - the gate's probability check only requires a value to
exist.

Fix, no new tuning knobs: the primary must clear BOTH floors
(R:R >= 1.5 AND probability >= 20%). When nothing does, fall back to
the most likely target overall, so the headline carries an honest low
R:R and the gate rejects the setup on real numbers instead of being
gamed by an unreachable target.

Deliberately NOT pure EV-maximization (p*RR): the probability model
adds strength/alignment bonuses as flat percentage points, so EV
arithmetic would scale those bonuses by (RR+1) and systematically
favor far targets on aligned setups - the same lottery bias through
the back door.

Shared by production (enhance_trade_setup) and the backtest simulator,
so backtest comparisons stay apples-to-apples. Unit tests pin the
degenerate case; full unit suite green (497 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:31:35 +02:00
dennisthiessenandClaude Fable 5 6e33897c1a Mark-as-taken opens a modal dialog instead of expanding inline
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m1s
Deploy / deploy (push) Successful in 35s
The inline take form under the price rail pushed the card apart. It is
now a focused overlay dialog (portaled to body - the unified panel's
backdrop-filter/overflow would trap a fixed element):

- symbol + direction header, stop/target/suggested-size summary line
- shares (autofocused), entry, and target selector
- Confirm trade / Cancel; closes on success, Escape, or backdrop click
- the card and panel never move

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:10:13 +02:00
dennisthiessenandClaude Fable 5 16121042cd Target selection syncs to chart; stable rail scale; sizing top-right
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 35s
- The ladder target choice is lifted to the ticker page (per
  direction), so selecting a target updates the candlestick overlay's
  target line and zone too, with the R:R for the tooltip taken from
  the ladder row.
- Price rail: the scale now always spans the entire target ladder, so
  stop / entry / now hold their positions and only the target marker
  moves when a different target is selected. The furthest target sits
  at the right edge.
- Setup card layout: position sizing (shares / value / max loss,
  details in the tooltip) and the Mark-as-taken button move to the top
  right of the card header row, next to the stats they belong with -
  the dangling bottom row is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:52:18 +02:00
dennisthiessenandClaude Fable 5 9f56a25f2d Freshness first row; level clustering; verdict typography;
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m5s
Deploy / deploy (push) Successful in 34s
selectable targets drive the rail

- Data freshness is now the very first row of the ticker panel, above
  the symbol - data age is the first thing to check.
- Indicator cards: long price-level lists (pivot swing highs/lows,
  volume-profile HVN/LVN) no longer clip or overwhelm. Numeric lists
  are clustered into ranges (values within ~0.75% of price merge),
  sorted nearest-to-price first, capped at 4 chips with a "+n more"
  note; scalar values wrap instead of overflowing the card.
- Recommendation verdict: the action ("LONG (high confidence)") is set
  in the display face in accent cyan with the risk level beside it;
  the signal breakdown (technical/momentum/sentiment) drops to a small
  muted subtitle line.
- Target ladder is open by default and selectable: clicking a row
  previews that target on the price rail and updates the R:R and
  probability chips, the low-probability warning, and the prefilled
  target in Mark-as-taken (still overridable there). The scanner's
  primary stays the default; a non-primary choice is chipped as
  "custom target".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:36:39 +02:00
dennisthiessenandClaude Fable 5 03bdcbd745 Ticker panel polish: freshness up top, radar pad, indicator grid,
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 35s
verdict de-dup, target picker on take

Five fixes from prod review:

1. Data freshness row moves from the panel foot to directly under the
   header - data age is a first-look concern.
2. Radar fingerprint labels were clipped left ("Momentum") and right
   (value digits): label padding widened to fit "momentum 100" on both
   anchored sides.
3. Indicators tab: the one-at-a-time dropdown (which also clipped) is
   gone; all indicators load in parallel as quiet outlined cards - EMA
   cross with its colored signal, RSI/ADX with a one-line human read
   (overbought / trending / ...), values formatted to 2 decimals
   instead of 4, normalized score demoted to a corner note.
4. Recommendation verdict line de-duplicated: the backend reasoning
   already starts with the action label, so it now replaces the
   headline instead of repeating under it.
5. Mark-as-taken: the take form gains a target selector listing every
   detected target with price / probability / classification (primary
   preselected), and the card warns when the primary target sits below
   15% probability. Why the scanner promotes such a target to primary
   is a separate (backend) question.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:20:23 +02:00
dennisthiessenandClaude Fable 5 d806ede07e Recommendation module in Horizon language; Standing/Dimensions as tabs
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 37s
RecommendationPanel redesigned to match the rest of the redesign:

- Setup cards use the spatial price rail (stop -> entry -> now ->
  target with live R-multiples) instead of the label/value text grid.
- One quiet chip row per card (direction, preferred marker, confidence,
  R:R, target probability) replaces scattered header text.
- Position sizing is a single mono line (shares / position / max loss)
  instead of a boxed 3-column mini-table; Mark-as-taken flow kept, in
  the standard cyan button style.
- Full target ladder moves behind a disclosure (the primary target is
  already on the rail); warnings (counter-trend, stale/invalidated
  drift, earnings window, conflict flags) all kept.
- Cards are quiet outlined blocks, no glass-in-glass; the module
  renders frameless inside the unified ticker panel's Analysis tab,
  under the chart.

Ticker panel tabs now: Analysis, Standing, Dimensions, Indicators,
S/R Levels, Sentiment, Fundamentals - Standing and Dimensions were the
last below-the-fold sections; the whole page is now the single panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:32:03 +02:00
dennisthiessenandClaude Fable 5 fc0945367a Ticker page: one unified drill-down panel, no card salad
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m7s
Deploy / deploy (push) Successful in 36s
The previous pass kept a stack of independent glass cards (header,
recommendation, chart section, tab pills, per-tab sections), which read
as clutter. Now the page is a single casting, like the mockup:

One glass panel containing:
- header (identity, price, chips | actions + score fingerprint)
- inline hairline tab row (Analysis / Indicators / S/R Levels /
  Sentiment / Fundamentals) with the chart-overlay pills riding on the
  right of the row during Analysis
- tab body: Analysis = the candlestick chart; other tabs render their
  panel inside via .hz-frameless, which strips the nested card chrome
  (no card-in-card)
- panel foot: data freshness row (frameless now) with per-source
  refresh

Below the panel, only on Analysis: the recommendation module and a
two-column Standing | Dimensions grid. Nothing removed; the standalone
Tabs bar, the separate chart card, and the duplicated section stack are
gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:17:05 +02:00
dennisthiessenandClaude Fable 5 f9e12fc78c Ticker page: mockup drill-down layout
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m7s
Deploy / deploy (push) Successful in 37s
Restructures TickerDetailPage to match the Horizon mockup's ticker
drill-down while keeping every existing feature:

- Glass header card: eyebrow + big symbol/company, price with change
  and freshness, chips (composite score, next earnings, top-pick /
  open-trade pills) on the left; watchlist + Fetch All actions and the
  score fingerprint radar on the right (hover a corner for values).
- Data freshness bar (per-source age + refresh buttons) moves to the
  foot of the chart card, like the mockup's panel footer.
- Tabs extended to five: Analysis, Indicators, S/R Levels, Sentiment,
  Fundamentals - sentiment and fundamentals no longer stack below the
  analysis content, they are one tab away.
- ScoreCard gains showRadar prop so the fingerprint is not duplicated
  in the Dimensions section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 07:59:13 +02:00
dennisthiessenandClaude Fable 5 02cf9d1cba Match Horizon mockup structure; restore qualified-first radar
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m8s
Deploy / deploy (push) Successful in 35s
Fixes from first prod review of the redesign:

- Top navigation bar (TopBar) replaces the sidebar, like the mockup:
  wordmark + sections + ticker search + jobs/status/logout; content in
  a centered max-width column. MobileNav unchanged.
- Radar panel: qualified setups lead again (the actionable list, as
  the old Top Setups did); non-qualified sit behind an "N below the
  gate" toggle with per-rule reasons, auto-open only when nothing
  qualifies. Adds mini radar fingerprints for the top 3 qualified
  (dimension scores from the rankings endpoint, fixed axis order).
- Dashboard layout: open positions and radar side by side (xl), as in
  the mockup.
- ScoreCard: the radar fingerprint was inside the showComposite block,
  which the ticker page disables - it never rendered. Now it shows
  whenever >= 3 dimensions exist.
- TradeChart: fresh positions with < 2 bars since entry rendered no
  chart; now pads with ~10 pre-entry context bars (gray) and marks the
  entry point. First open position auto-expands so the chart is
  visible without a click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 07:44:55 +02:00
dennisthiessenandClaude Fable 5 20f6981712 Horizon redesign: space theme, visual dashboard, chart reskin
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m9s
Deploy / deploy (push) Successful in 36s
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>
2026-07-10 07:24:17 +02:00
dennisthiessenandClaude Fable 5 744ea4ddc4 Document July 2026 tuning conclusions in Strategy Status
Deploy / lint (push) Successful in 10s
Deploy / test (push) Successful in 1m22s
Deploy / deploy (push) Successful in 39s
Record every tested-and-confirmed knob (ATR trail, regime overlay, lookback,
cutoff x book, sizing, FIP tie-breaker) so the sweep is not repeated on the
same snapshot, including the inverse-vol mis-attribution warning and the
universe-level fip_id lead. Prune the done items from the next-experiments
list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:59:52 +02:00
dennisthiessenandClaude Fable 5 ae1aeb3c84 Align backtest production sim with live runtime config
The portfolio monitor's Production row now replays the live qualification
flag and the Admin exit policy (mode/ATR multiplier/hold days) instead of a
frozen research-variant gate, so Admin tuning is reflected in the next run.
Single-source the 80/20 strategy_rank weights in momentum_service and pin
every dual-defined constant with a parity test. Behavior-preserving today:
the production sim reproduces the README baseline exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:28:50 +02:00
dennisthiessenandClaude Opus 4.8 65d2dae62a Revert blue-sky projection; keep played-out setup UX
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>
2026-07-08 22:03:25 +02:00
dennisthiessenandClaude Opus 4.8 294d935030 Add blue-sky projected targets and played-out setup UX
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>
2026-07-08 19:30:28 +02:00
dennisthiessen 61156684ff Simplify top setups table
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m17s
Deploy / deploy (push) Successful in 37s
2026-07-04 13:57:42 +02:00
dennisthiessen 65335cf1f3 Tighten qualified signal gate
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m18s
Deploy / deploy (push) Successful in 40s
2026-07-04 13:51:44 +02:00
dennisthiessen 23d1db1f30 Exclude open paper trades from discovery
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m25s
Deploy / deploy (push) Successful in 43s
2026-07-04 13:01:58 +02:00
dennisthiessen edc1a9757b Refine Telegram alert behavior
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m18s
Deploy / deploy (push) Successful in 39s
2026-07-04 12:37:12 +02:00
dennisthiessenandClaude Opus 4.8 ce6035ee3c Fix stale copy, dedupe Exit columns, document local report review
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m26s
Deploy / deploy (push) Successful in 42s
Follow-ups from review of the Track Record slim:

- BacktestPanel: drop the stale "tracking check" sentence from the "How this is
  measured" explainer — that check moved to the maintenance disclosure last
  commit, so it no longer describes anything in this block.
- MyTradesPanel: rename the two identically-labelled "Exit" columns to "Exit Px"
  (exit price) and "Reason" (close_reason) so they're not confusable.
- README: add "Reading a local backtest report" under Local Backtest Snapshots —
  a section->decision map for reports/backtest-*.json. The strategy-tuning tables
  removed from the deployed page (sweep, gate_ablation, time_exit_sweep,
  signal_eval, strategy_variants) now live only in the local report, so this
  keeps "research lives local" from meaning the decision knowledge evaporates.

tsc -b && vite build pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:01:59 +02:00
dennisthiessenandClaude Opus 4.8 2a4bdd16a8 Demote/relabel the setup-outcome check; add exit reason to My Trades
The "tracking/drift" chip compared the live target/stop/expired outcome cohort
against the backtest's target/stop bucket (overall_qualified) — a like-for-like
pipeline check — but sat directly under the portfolio monitor, which shows the
promoted 3x-ATR-trailing book. That juxtaposition (plus "faithfully implementing
it" copy) made a plumbing/QA signal read as validation of the ATR-trail strategy
you actually trade. It validates neither the trailing-stop book nor real trades.

- Move the check out of the monitor block into the "Track-record maintenance"
  disclosure, relabelled "Setup-outcome pipeline check" with copy that says it
  checks the setup-grading pipeline (no look-ahead/config/data drift), NOT the
  ATR-trail production book. The genuine live validation stays My Trades (real
  paper trades, same ATR-trail exits) up top.
- Add a compact "Exit" column to My Trades showing close_reason
  (Stop/Trail/Target/Time/Manual) — the field was already plumbed to the
  frontend PaperTrade type, so this is frontend-only.

tsc -b && vite build pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:51:13 +02:00
dennisthiessenandClaude Opus 4.8 02b28f5ea6 Slim Track Record page to validation + how-to-trade
Strategy research now runs locally against DB snapshots (see README), so the
deployed Track Record page no longer needs the strategy-tuning output. Keep only
what answers "did my trades work / is the strategy working / what do I trade":

- Reshape BacktestPanel into an "Is the strategy working?" block: portfolio
  monitor (unchanged), a deliberate metric set (CAGR, Sharpe, Max DD, Total
  Return vs SPY, per-year returns), plus the folded-in live-vs-backtest verdict
  and the backtest recommendation.
- Fold the standalone portfolio-sim table's unique rows (per-year returns, avg
  hold, best/worst, avg P&L) into the monitor; drop the duplicate table.
- Slim TrackRecordPanel to My Trades -> Is it working? -> maintenance disclosure
  (Evaluate/Reset demoted).
- Cut the local-research tables: percentile sweep, gate ablation, time-exit
  sweep, strategy variants, signal-edge rank-IC, research candidates,
  by-action/by-confidence breakdowns, and the bucket comparison.

Frontend-only; the weekly server backtest still computes the cut tables (they
feed the local report). tsc -b && vite build pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:36:41 +02:00
dennisthiessenandClaude Opus 4.8 ca42e1b28d Precompute ATR series for paper-trade trailing exits
Both _atr_trailing_close (scheduled) and _atr_trailing_level (dashboard
read path) recomputed ATR from scratch on every post-entry bar via
compute_atr(rows[:idx+1]) — O(n*k) per trade. Replace with a single O(n)
Wilder pass, _atr_series_from_rows, that stores round(running, 4) at each
index. compute_atr keeps its running ATR unrounded through the recurrence
and rounds only at return, so this reproduces its per-prefix value exactly
(no behavior change; live-vs-backtest atr_trail3 parity still byte-identical).

Remove the now-unused _atr_from_rows and its compute_atr import. Add a
per-index parity test against compute_atr; existing ATR tests now mock
_atr_series_from_rows (same effect as the old fixed-ATR mock).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:13:19 +02:00
dennisthiessenandClaude Opus 4.8 1155c9ed1b Shorten alembic revision ids to fit version_num VARCHAR(32)
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m29s
Deploy / deploy (push) Successful in 42s
The deploy's `alembic upgrade head` failed at 016->017 with
StringDataRightTruncationError: the revision id
"017_add_trade_setup_strategy_rank" is 33 chars, but Postgres's
alembic_version.version_num is VARCHAR(32). Offline/SQLite checks don't
enforce the length, so this only surfaced against prod Postgres.

Rename the revision ids to the repo's short numeric convention (017, 018);
descriptive filenames are kept. down_revision links updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:17:10 +02:00
dennisthiessenandClaude Opus 4.8 41470c32ec Rename ambiguous loop var l to lb in portfolio monitor
Deploy / lint (push) Successful in 7s
Deploy / test (push) Failing after 18s
Deploy / deploy (push) Has been skipped
Fixes ruff E741 in the lookbacks comprehension of _portfolio_monitor,
which failed the CI lint step and blocked the deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:08:44 +02:00
dennisthiessenandClaude Opus 4.8 cfca59a2d4 Fix migration chain and clear stored paper exit mode
Deploy / lint (push) Failing after 9s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped
Migration 017 set down_revision to "016_add_signal_context_snapshots",
but migration 016's revision id is "016", so `alembic upgrade head` could
not resolve the chain and the deploy's auto-alembic step would fail. Point
017 at the real id "016".

Add migration 018 to delete any persisted `paper_exit_mode` row. The July
2026 promotion changed the paper-trade exit default to `atr_trailing`, but
`get_exit_policy` reads a stored value before the code default, so an
environment that had ever saved the old `time` mode would silently keep it
and never run the promoted 3x ATR trailing exit. Mirrors migration 015's
one-way settings reset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:05:32 +02:00
dennisthiessen 5f2d108227 Promote production portfolio strategy 2026-07-04 07:48:38 +02:00
dennisthiessen 66ef0564c1 Add local backtest snapshot runner
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m22s
Deploy / deploy (push) Successful in 43s
2026-07-03 18:35:07 +02:00
dennisthiessen 14327ab25a Require aligned action for qualified setups
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m7s
Deploy / deploy (push) Successful in 39s
2026-07-03 16:13:27 +02:00
dennisthiessen eaad935a2a Bundle signal alert notifications
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 35s
2026-07-03 13:32:59 +02:00
dennisthiessen d4ccea2d69 Normalize persisted test timestamps
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m12s
Deploy / deploy (push) Successful in 37s
2026-07-03 13:01:45 +02:00
dennisthiessen 8c36cfcef1 Make live signal reads non-mutating
Deploy / lint (push) Successful in 6s
Deploy / test (push) Failing after 48s
Deploy / deploy (push) Has been skipped
2026-07-03 10:09:46 +02:00
dennisthiessenandClaude Fable 5 ac51e23949 Serve live recommendation context on trade setup APIs and alerts
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m0s
Deploy / deploy (push) Successful in 32s
Stored TradeSetup rows are point-in-time snapshots from the RR scan, so
the ticker page could show stale confidence/reasoning/composite (e.g.
sentiment=neutral in the setup card while the sentiment panel showed
bullish). Overlay current score/sentiment context onto the API payload
for GET /trades and GET /trades/{symbol}, gate and format Telegram
qualified-setup alerts on the same live values, and apply the
min_confidence/recommended_action filters after the overlay so they
judge what the caller actually sees. Stored setups stay frozen for
outcome analysis and backtests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:17:27 +02:00
dennisthiessen 2b0068ae08 Add volume pane to ticker chart
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 58s
Deploy / deploy (push) Successful in 32s
2026-07-03 08:09:27 +02:00
dennisthiessen 7fd34d6de8 removed old requirements md file
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m5s
Deploy / deploy (push) Successful in 33s
2026-07-02 21:36:17 +02:00
dennisthiessen 8d5863bac4 document production backtest baseline 2026-07-02 21:31:51 +02:00
dennisthiessen be4d6a05ca clarify documented production strategy
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m7s
Deploy / deploy (push) Successful in 35s
2026-07-02 21:23:05 +02:00
dennisthiessen aadec7d403 promote residual momentum ranking
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m8s
Deploy / deploy (push) Successful in 35s
2026-07-02 21:00:39 +02:00
dennisthiessen 849489a4b5 refine strategy variant lab
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m0s
Deploy / deploy (push) Successful in 33s
2026-07-02 16:47:58 +02:00
dennisthiessenandClaude Fable 5 80b4113280 feat: add strategy variant lab and signal context snapshots
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m1s
Deploy / deploy (push) Successful in 33s
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>
2026-07-02 16:25:04 +02:00
dennisthiessenandClaude Fable 5 13374087db feat: add residual momentum to signal-edge backtest
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 55s
Deploy / deploy (push) Successful in 33s
Adds a research-only 12-1 residual momentum signal to the cross-sectional signal-evaluation harness. The signal estimates benchmark beta over the 12-1 formation window and ranks cumulative stock return minus beta-adjusted benchmark return; it only appears when benchmark closes are available.

No production qualification behavior changes. The Backtest signal table labels the new row as 12-1 residual momentum. Tests cover benchmark-gated emission and beta removal while keeping stock-specific drift.

Verification: 453 backend tests pass, ruff check app/ clean, frontend npm run build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:46:54 +02:00
dennisthiessenandClaude Fable 5 1e82dfad7f feat: adopt Phase 3 gate and paper-trade exit policy
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 56s
Deploy / deploy (push) Successful in 33s
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>
2026-07-02 15:20:34 +02:00
dennisthiessenandClaude Fable 5 29a61cb2ca fix: judge robustness under the recommended exit, not the abandoned one
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 57s
Deploy / deploy (push) Successful in 32s
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>
2026-07-02 12:50:13 +02:00
dennisthiessenandClaude Fable 5 243e369e9a feat: robustness stats + dynamic recommendation; retire settled report sections
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 54s
Deploy / deploy (push) Successful in 32s
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>
2026-07-02 12:33:22 +02:00
dennisthiessenandClaude Fable 5 0f43e755f4 feat: portfolio simulation + per-trade stats (gaps, hold time, best/worst)
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 55s
Deploy / deploy (push) Successful in 38s
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>
2026-07-02 11:56:29 +02:00
dennisthiessenandClaude Fable 5 942a22ce65 feat: grade gate-ablation variants under the hold-to-horizon exit too
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 55s
Deploy / deploy (push) Successful in 33s
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>
2026-07-02 11:34:41 +02:00
dennisthiessenandClaude Fable 5 8750aac6d9 fix: carry action/risk_level onto backtest candidates for the gate ablation
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 57s
Deploy / deploy (push) Successful in 2m24s
_window_setups computed them but _replay_ticker dropped them, so the
ablation's NEUTRAL/tightener checks saw None for every candidate and the
'without confidence floor' / 'without R:R floor' rows collapsed to 0
setups (impossible — removing a floor can only add setups). Regression
test now goes through the real _replay_ticker path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 08:07:27 +02:00
dennisthiessenandClaude Fable 5 29b1a9a28c feat: net-of-cost backtest, gate ablation + time-exit sweeps, longer tails
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 57s
Deploy / deploy (push) Successful in 32s
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>
2026-07-02 07:50:37 +02:00
dennisthiessenandClaude Fable 5 84ce7c5c26 docs: strategy status + maintainer guide in README; document CI/CD deploy
Adds the validated-vs-not verdict table, the iron rule for strategy
changes, ranked next experiments, a maintainer guide (invariants, file
map, verification, roadmap), and corrects the deploy docs: deploys are
automated by Gitea Actions (push to main = deploy), service is
signalplatform.service at /opt/signalplatform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:50:21 +02:00
dennisthiessenandClaude Opus 4.8 da0bb3367e feat: company names for tickers (Alpaca backfill + subtle display)
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 55s
Deploy / deploy (push) Successful in 32s
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>
2026-07-01 10:50:40 +02:00
dennisthiessenandClaude Opus 4.8 a9f4686157 fix: forward sentiment-adjustment fields in the scores API response
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 54s
Deploy / deploy (push) Successful in 31s
get_score computed base_score / sentiment_score / sentiment_adjustment /
max_sentiment_adjustment, but the router's _map_composite_breakdown built the
response model from only the five original keys and silently dropped the rest — so
the API always returned null for them. That's why the ticker page showed neither the
"Composite = Base + Sentiment" caption nor the ± marker on the sentiment row despite
the frontend and scoring service both supporting it. Pass the fields through, with a
guard test so they can't be dropped again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:04:06 +02:00
dennisthiessenandClaude Opus 4.8 94ed3207d7 feat: show composite = base + sentiment caption under the Standing matrix
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 52s
Deploy / deploy (push) Successful in 30s
The ticker page renders the composite via the Standing matrix (ScoreCard runs with
showComposite=false), so the "Base X · sentiment +Y" line in the ScoreCard header
was never visible there. Add a compact caption beneath the matrix — "Composite 83 =
Base 78 + Sentiment 5.0" — shown only when sentiment actually moves the score, so
the composition of the number has a visible home where the number lives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:55:42 +02:00
dennisthiessenandClaude Opus 4.8 5442b62495 fix: decouple the sentiment weight from the base mix in the weights form
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 50s
Deploy / deploy (push) Successful in 31s
Sentiment is now a signed adjustment (± points on top of the base), not part of
the averaged dimensions — but the weights form still squeezed all five sliders to
sum to 100%, so dragging sentiment rebalanced the base and you couldn't set a clean
±N. Now the four base dimensions normalize among themselves (share shown as %),
and sentiment is its own "influence (± points)" control passed through raw
(slider 10 → weight 0.10 → ±10), independent of the base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:43:43 +02:00
dennisthiessenandClaude Opus 4.8 f61e11adea feat: sentiment as a signed adjustment to the composite, not averaged in
Deploy / lint (push) Successful in 23s
Deploy / test (push) Successful in 54s
Deploy / deploy (push) Successful in 31s
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>
2026-07-01 09:34:37 +02:00
dennisthiessenandClaude Opus 4.8 1566b84379 feat: trailing-stop auto-exit for paper trades + close/digest alerts
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 54s
Deploy / deploy (push) Successful in 33s
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>
2026-06-30 18:48:05 +02:00
dennisthiessenandClaude Opus 4.8 ab9ce18809 feat: trailing-stop exit sweep in the backtest
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 55s
Deploy / deploy (push) Successful in 32s
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>
2026-06-30 17:33:17 +02:00
dennisthiessenandClaude Opus 4.8 c5f6b07a3e feat: extend take-profit sweep into the tail + clarify it ignores the target
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 55s
Deploy / deploy (push) Successful in 33s
Avg R was still rising at the previous top level (+15%), so the optimum was off
the table. Extend TP_LEVELS to 20/25/30% to reveal where letting winners run
stops paying (it plateaus toward "just hold to the horizon close").

Also clarify in the panel that the take-profit model deliberately does NOT use
the setup's S/R target — it's a standalone fixed-% exit; exiting at the target is
the target-vs-stop model above. The two are complementary ends, not in conflict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:14:54 +02:00
dennisthiessenandClaude Opus 4.8 c63951ca02 feat: take-profit exit sweep in the backtest (alongside target-vs-stop)
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 59s
Deploy / deploy (push) Successful in 34s
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>
2026-06-30 16:56:32 +02:00
dennisthiessenandClaude Opus 4.8 6511a1020b feat: exclude NEUTRAL setups from the activation gate (default on)
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 56s
Deploy / deploy (push) Successful in 34s
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>
2026-06-30 15:19:07 +02:00
dennisthiessenandClaude Opus 4.8 20a1c143f3 fix: surface empty OHLCV fetch as a warning, not success
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m25s
Deploy / deploy (push) Successful in 46s
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>
2026-06-28 19:27:41 +02:00
dennisthiessenandClaude Opus 4.8 6c2e45377c feat: collapse track record into a live-vs-backtest check
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 57s
Deploy / deploy (push) Successful in 34s
The outcome section measures the same thing as the backtest with the same code
and data — its only unique value is catching when the live system drifts from
the backtest (a bug, config/data drift, or look-ahead). So reframe it as exactly
that: a one-line "Live X R vs Backtest Y R · n matured · tracking ✓ / drift ⚠"
indicator (like-for-like with the qualified toggle), with the stat cards and
By-Action/By-Confidence tables moved into a collapsed "Outcome details"
disclosure. Drop the always-empty By-Direction table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 13:58:15 +02:00
dennisthiessenandClaude Opus 4.8 7e9a6cd7ec fix: only count matured setups in the live track record
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m1s
Deploy / deploy (push) Successful in 36s
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>
2026-06-28 13:41:48 +02:00
dennisthiessenandClaude Opus 4.8 8bcbbfcfd0 fix: show benchmark job in admin; harden + split deploy workflow
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 48s
Deploy / deploy (push) Successful in 28s
- admin_service: register benchmark_collector in VALID_JOB_NAMES, JOB_LABELS and
  PIPELINE_MEMBERS. The Admin → Jobs list is built from these hardcoded sets, not
  the scheduler, so the job was registered but invisible/untriggerable.

- deploy.yml:
  - SSH: verify the host key (StrictHostKeyChecking=yes) now that known_hosts is
    supplied; move private-key cleanup to an `if: always()` step.
  - Add a concurrency guard so deploys serialize.
  - Health-check the service after restart (127.0.0.1:8998/api/v1/health).
  - Align CI Python to 3.12 (matches prod); pip + npm caching.
  - Clarify the Postgres service only validates migrations (tests use SQLite);
    drop the redundant DATABASE_URL from the pytest step.
  - Split the monolithic "Deploy to server" step into named steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 09:01:09 +02:00
dennisthiessenandClaude Opus 4.8 0627787bfc fix(alembic): renumber benchmark migration 011 -> 012
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 46s
Deploy / deploy (push) Successful in 27s
011 collided with the existing 011_add_regime_snapshots (duplicate revision id
and a second head branching off 010), which broke `alembic upgrade head`. Chain
the benchmark_prices migration after regime_snapshots so the history is linear
again (010 -> 011 regime_snapshots -> 012 benchmark_prices, single head).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 08:47:36 +02:00
dennisthiessenandClaude Opus 4.8 30effa89b7 feat: ticker search, watchlist momentum column, alpha vs S&P 500
Deploy / lint (push) Successful in 6s
Deploy / test (push) Failing after 12s
Deploy / deploy (push) Has been skipped
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>
2026-06-28 08:44:40 +02:00
dennisthiessenandClaude Opus 4.8 4a96f85cd9 feat: Standing matrix on the ticker page (quality x momentum verdict)
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 26s
Replace the scattered score readouts with one hero: a quality (composite) x
momentum-percentile scatter that plots this ticker against the whole field and
reads out a verdict by quadrant — Strong Buy / Momentum / Accumulate / Pass. The
dashed divider is the activation gate's momentum percentile, so "above the line =
qualifies" is visible at a glance; peers are clickable. Reuses the regime-quadrant
visual language and is lazy-loaded so recharts stays out of the main ticker chunk.

- New StandingMatrix component (composite x momentum, field cloud, verdict).
- ScoreCard gains showComposite (default true); the ticker page now renders it
  without the composite ring (composite lives in the matrix) under "Dimensions".
- Confidence + target probability stay in the recommendation panel (the trade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:00:47 +02:00
dennisthiessenandClaude Opus 4.8 146dadf06f docs: refresh README and document how it works
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 43s
Deploy / deploy (push) Successful in 25s
- Add "How It Works": daily load (ordered pipeline steps), intraday flow,
  other jobs, and the score -> activation gate -> top pick chain.
- Add "Key Use Cases" (find today's best long setup; track a paper trade).
- Fix stale facts: user-curated (not auto) watchlist, actual routes/pages,
  scheduler description, wrong env defaults (RR 3.0->1.5, fundamentals
  daily->weekly).
- Add missing surface: paper trading, activation gate, market regime, Telegram
  alerts, backtest; API groups (paper-trades, market/regime, jobs); FRED +
  Telegram env vars; note that pipeline timing is admin-cron, not env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:12:59 +02:00
dennisthiessenandClaude Opus 4.8 d15acb8741 feat: top-pick and open-trade status labels on the ticker page
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 25s
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>
2026-06-27 16:04:55 +02:00
dennisthiessenandClaude Opus 4.8 2f21c685e8 feat: always-fresh sentiment for top picks, watchlist & open trades
Tiered, uncapped sentiment scope so the names that matter are never shown
without sentiment.

- Priority (always fully refreshed): top-pick feeders — momentum leaders with a
  tradeable long setup over the R:R floor (the tickers that are, or could become
  with positive sentiment, the dashboard top pick) — plus the curated watchlist
  and open paper trades.
- Filler: top-N by composite, a discovery net, fetched after the priority set so
  a mid-run rate limit lands the important names first.
- Removed the per-run cap (sentiment_max_per_run): the relevant set is naturally
  bounded (watchlist <= 20, composite <= top_composite), so a full refresh stays
  inside the free tier. extra="ignore" keeps a stale env var from breaking startup.
- Refresh window 72h -> 120h (5 days): sentiment shifts slowly, score window is 7d.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:59:58 +02:00
dennisthiessenandClaude Opus 4.8 65dd53baa3 feat: Telegram alert on regime quadrant change (hysteresis + cooldown)
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 41s
Deploy / deploy (push) Successful in 27s
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>
2026-06-26 19:05:01 +02:00
dennisthiessenandClaude Opus 4.8 e683513857 fix: smooth the quadrant trail and fade it by recency
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 25s
The single solid trail line read like a tangle. Make older→newer legible: the
path now fades from muted slate (older) to bright blue (newer) via per-point
colors, the connecting line is faint, and the points are de-noised with a
centered moving average (today kept exact). Easier to see the direction of travel
through the quadrants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:22:30 +02:00
dennisthiessenandClaude Opus 4.8 a07bfee6e6 feat: regime quadrant plot in place of the combined gauge
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 25s
The combined score collapsed two distinct signals into one not-very-meaningful
number. Replace its gauge with a quadrant scatter that shows both axes directly:
x = regime index (coincident), y = early warning (breadth divergence), with a
trail of the last 60 sessions and today highlighted.

The four quadrants make the readings legible — ① hot & brittle (narrow melt-up,
shakeout risk), ② transition, ③ healthy & broad, ④ real downturn — and the trail
surfaces the actual tell: the ①→④ move (early warning rolling over as the regime
index climbs = divergence resolving downward). Combined still shows as a line in
the score-history chart. Frontend-only; reuses the history endpoint. Lazy-loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:14:32 +02:00
dennisthiessenandClaude Opus 4.8 66444af65c feat: score-history chart on the regime tab
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 41s
Deploy / deploy (push) Successful in 25s
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>
2026-06-26 15:48:42 +02:00
dennisthiessenandClaude Opus 4.8 60def1155b fix: coverage-aware event-study headline instead of misleading median delta
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 39s
Deploy / deploy (push) Successful in 24s
The "warned a median -18 days later" line was the median-over-1-event trap: the
coincident baseline's 60d median is a single lucky event, while breadth warned on
7. Replace it with the honest coverage framing (7/11 vs 1/11) and flag that the
median-lead comparison is unreliable when coverage differs this much.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:36:39 +02:00
dennisthiessenandClaude Opus 4.8 02b8df58f0 fix: populate early-warning/combined on the latest snapshot + recent history
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 41s
Deploy / deploy (push) Successful in 24s
The early-warning score showed n/a because it required an exact date match
between the live benchmark (Alpaca, may have today's bar) and the stored
universe breadth (DB, often a day behind), which blanked the newest snapshot —
the one the UI displays.

- Look up the divergence as-of the snapshot date (newest value within a 7-day
  lag) instead of requiring an exact match.
- Backfill early_warning + combined onto recent existing snapshots (the index
  history predates this signal) so the 7/30-day trends populate on the first run
  rather than only filling in over the coming weeks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:31:02 +02:00
dennisthiessenandClaude Opus 4.8 613fc756ec feat: separate live early-warning + combined score on the regime tab
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 40s
Deploy / deploy (push) Successful in 24s
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>
2026-06-26 15:23:37 +02:00
dennisthiessenandClaude Opus 4.8 7c5fb1138d feat: sharpen the event study — more events, fair baseline, per-event view
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 41s
Deploy / deploy (push) Successful in 26s
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>
2026-06-26 14:54:29 +02:00
dennisthiessenandClaude Opus 4.8 f8d62e4074 feat: show current exposure instead of lifetime stats on the overview
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 43s
Deploy / deploy (push) Successful in 25s
The overview's Hit Rate and Expectancy were static lifetime aggregates — they
barely move day to day and aren't actionable at a glance. Replace them with the
current state from open paper trades:

- Open Risk: total $ at risk to stops across open positions.
- Unrealized: summed unrealized R (mark-to-market), with $ P&L and win/loss count.

Computed in the frontend from the already-loaded open trades (tradePnl) — no
backend change. The detailed lifetime stats remain on Signals → Track Record.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 14:08:59 +02:00
dennisthiessenandClaude Opus 4.8 824c15cf69 feat: breadth-divergence early-warning indicator + event study
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>
2026-06-26 14:08:52 +02:00
dennisthiessenandClaude Opus 4.8 ebff19940b feat: add standalone AI/Tech regime-change monitor tab
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 46s
Deploy / deploy (push) Successful in 27s
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>
2026-06-26 11:51:45 +02:00
dennisthiessenandClaude Opus 4.8 5605915d45 fix: scope sentiment collection to the gate's momentum leaders
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 25s
Qualified setups could carry no sentiment because the sentiment job scoped
its relevant-set to watchlist + open trades + top-N composite score, while
the activation gate qualifies on 12-1 momentum percentile — a different axis.
A top-momentum ticker outside the composite top-N never got sentiment, so the
R:R scan enhanced it as neutral.

Add the gate's momentum leaders (percentile >= activation min_momentum_percentile)
to the sentiment relevant-set so scope tracks the gate. Best-effort: a momentum
or config failure falls back to the base set rather than aborting collection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 12:06:52 +02:00
dennisthiessen 437ceacfc1 refactor: dedupe scheduler logging/runtime, centralize SystemSetting access, fix rankings N+1
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 27s
Behavior-preserving cleanup (345 tests pass, ruff clean):

- scheduler: replace 62 inline logger.x(json.dumps({...})) calls with a
  _log_event helper, and collapse 11 identical _job_runtime dicts into an
  _idle_runtime() factory over _JOB_NAMES.
- settings: add app/services/settings_store.py (get_setting/get_value/get_map/
  upsert_setting) and route ~13 hand-rolled SystemSetting queries + two
  identical _settings_map helpers through it.
- scoring.get_rankings: collapse the per-ticker N+1 (3-4 queries + a commit each)
  into 2 bulk reads + a single conditional commit; drop the redundant re-fetch.
  Lazy recompute-on-read is preserved. Adds first tests for get_rankings.

Net ~ -245 lines across the touched modules.
2026-06-24 11:23:39 +02:00
dennisthiessenandClaude Opus 4.8 f48d8705de remove min_target_probability gate + add chart time-range presets
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 39s
Deploy / deploy (push) Successful in 24s
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>
2026-06-24 09:24:35 +02:00
dennisthiessenandClaude Opus 4.8 605f95098c momentum gate: long-only + wire the percentile onto live setups
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 47s
Deploy / deploy (push) Successful in 24s
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>
2026-06-24 07:07:38 +02:00