Commit Graph
100 Commits
Author SHA1 Message Date
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
dennisthiessenandClaude Opus 4.8 7060b9a019 parallelize the backtest across worker processes (true multi-core)
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 38s
Deploy / deploy (push) Successful in 25s
The replay was CPU-bound and single-core: the earlier asyncio.to_thread offload
kept the API responsive but, because of the GIL, ran on one core. Per-ticker
replay is independent, so fan it out across worker processes (which sidestep the
GIL) for real multi-core speedup.

- New `settings.backtest_workers` (default 4), capped to cpu_count-1 so a core
  stays free for the web server.
- Uses a `forkserver` context (workers forked from a clean single-threaded
  server — avoids the fork-with-threads deadlock); falls back to `fork`. On
  spawn-only platforms (Windows) and for 1-ticker runs it uses the thread path,
  so dev/tests are unaffected.
- Worker takes primitive column arrays (cheap to pickle), rebuilds bars, and
  returns (candidates, plain-dict signal series) — both picklable across the
  process boundary. Bars are still fetched in the event loop (ORM-safe).
- Pool creation is guarded: if the pool can't start, the job falls back to the
  sequential thread path instead of failing.

334 backend tests pass (parallel path is POSIX/server-only, so it's covered by
construction + the picklability/worker-count tests; the thread fallback is
exercised by the run_backtest smoke test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:20:20 +02:00
dennisthiessenandClaude Opus 4.8 e71c07e554 fix: blank Track Record page when the cached backtest report is pre-momentum
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 41s
Deploy / deploy (push) Successful in 24s
The momentum-sweep table read row.min_momentum_percentile.toFixed(), but a report
cached before the EV->momentum change only has min_expected_value rows. undefined
.toFixed() threw during render and — with no error boundary — blanked the whole
Track Record tab. Guard the sweep block on the new field so a stale report just
hides the sweep; re-running the backtest repopulates it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 22:47:36 +02:00
dennisthiessenandClaude Opus 4.8 ef523474ad replace EV activation gate with cross-sectional 12-1 momentum ranking
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 41s
Deploy / deploy (push) Successful in 26s
The 5-year backtest confirmed the EV gate adds negative value (high threshold =
worst expectancy) and that 12-1 month momentum is the one price signal with a
plausible, right-signed cross-sectional IC (~0.05). So "qualified" now means:
clears the R:R + confidence floors AND the ticker ranks in the top
`min_momentum_percentile` of the universe by 12-1 momentum that week.

- qualification.py: drop expected_value_r / the EV gate; add a momentum-percentile
  gate (duck-typed `momentum_percentile`, only enforced when attached + threshold
  set, else defers to floors). Mirrored in frontend qualification.ts.
- activation config/schema: min_expected_value -> min_momentum_percentile
  (default 80 = top quintile). ActivationSettings, DashboardPage (ranks/【shows】
  momentum instead of EV), and the BacktestPanel sweep follow.
- backtest: rank each ISO week's universe by 12-1 momentum, assign a percentile,
  and qualify the top slice; the sweep now sweeps the percentile cutoff.

Also offload the backtest's per-ticker compute to a worker thread so the heavy
~5y run no longer blocks the API event loop (the "backend offline" flicker).

Production setups don't carry momentum_percentile yet — wiring the scanner to
attach it (a universe momentum-rank step) is the next step; until then the live
gate defers to floors while the backtest measures the momentum selection. 330
backend tests pass; frontend build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 22:42:24 +02:00
dennisthiessenandClaude Opus 4.8 099846513b deepen OHLCV history + make the factor-IC pass honest about overlap/regime
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 39s
Deploy / deploy (push) Successful in 25s
Two changes so the cross-sectional signal results can actually be trusted.

(a) History depth — the binding constraint. Ingestion defaulted to 365 days, so
long-lookback factors (12-month momentum, 52-week high) were only computable on a
handful of weeks at the tail, and every IC reflected a single market regime.
- New `settings.ohlcv_history_days` (default 1825 ≈ 5y); new tickers backfill this
  far instead of 1 year.
- New manual "data_backfill" job (Admin → Jobs) re-fetches the full window for
  every ticker, ignoring incremental resume — run once to deepen existing
  1-year histories. Idempotent (upsert); resumes after rate limits.

(b) Factor-IC honesty. The IC was averaged over weekly rebalances whose 30-day
forward windows overlap, inflating the t-stat ~sqrt(6)x.
- IC now measured on NON-OVERLAPPING windows (weeks thinned to ~HORIZON apart).
- Each signal carries a `reliable` flag (>= 12 independent windows); BacktestPanel
  greys out and de-stars thin signals so a lucky 9-week IC of 0.3 can't masquerade
  as an edge.

332 backend tests pass; frontend build clean. No migration (config + job + an
added JSON field on the cached backtest report).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:20:59 +02:00
dennisthiessenandClaude Opus 4.8 402025692a add cross-sectional signal evaluation (factor rank-IC) to the backtest
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 40s
Deploy / deploy (push) Successful in 26s
The per-setup hit-rate report can't tell whether a signal predicts returns —
only how a target/stop structure built on one performs. This adds a
cross-sectional factor-IC pass: each week the universe is ranked by a price-only
signal and graded by its rank correlation (Spearman IC) and top-minus-bottom-
quintile spread against the forward 30-day return.

Candidate signals (point-in-time from price; sentiment/fundamentals have no
history in the replay): 12-1/6-1/3-1 month momentum, 1-month reversal,
price-vs-200d SMA, proximity to the 52-week high (George/Hwang), and 126-day
realized volatility (low-vol anomaly).

Reuses the existing per-ticker replay loop (no new data, no second DB pass);
results land in the cached backtest_report as `signal_eval` and render as a
"Signal edge" table in BacktestPanel beside the calibration curve.

330 backend tests pass (10 new in test_signal_eval); frontend build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:58:40 +02:00
dennisthiessenandClaude Opus 4.8 c34f3cb1a4 redesign activation gate to expected value + make pipelines cron-configurable
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 46s
Deploy / deploy (push) Successful in 28s
Diagnosing "no qualified signals for 5 days": setups were generated but none
qualified. The gate required BOTH a high min_rr (2.0) AND a high
min_target_probability (60), which became contradictory after the Jun-15
probability recalibration — probability already embeds R:R via the 1/(rr+1) ruin
term, so high-R:R targets are inherently low-probability and nothing cleared both.

Gate is now expected value (R): p*rr - (1-p) from the primary target's
probability. R:R and confidence stay as floors; high-conviction / exclude-conflicts
/ min-target-probability become optional tighteners (default off). Defaults:
min_expected_value=0.15, min_rr=1.2, min_confidence=55. EV is only enforced when
computable. Migration 009 clears stored activation_* rows so the new defaults
apply. Backtest sweeps min_expected_value instead of target probability.

Scheduling: pipelines are now cron-configurable in Admin -> Jobs. daily_pipeline
(full, default 0 7 * * *) plus a new light intraday_pipeline (OHLCV + outcome eval,
default hourly US session) that keeps prices/live-R:R current without setup churn.
Fundamentals on its own early weekly cron. Timezone configurable (default
Europe/Berlin). Moving interval->CronTrigger also fixes the restart-deferral bug
where an interval job's countdown resets on every process restart.

319 backend unit tests pass; frontend tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:46:38 +02:00
dennisthiessenandClaude Opus 4.8 d53b4ffb57 fix admin password reset: send new_password (was password)
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 36s
Deploy / deploy (push) Successful in 24s
The reset endpoint's schema expects new_password; the client sent password,
causing "body.new_password: Field required".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:46:06 +02:00
dennisthiessenandClaude Opus 4.8 9008865d75 run fundamentals weekly, not daily — it's quarterly-ish data
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 36s
Deploy / deploy (push) Successful in 24s
Pulled the fundamental collector out of the daily pipeline (where it re-fetched
near-identical numbers every day and burned free-tier API quota) and made it an
independent weekly job. P/E/market-cap drift with price but the score buckets
them coarsely; revenue growth and earnings surprise only change at quarterly
earnings. Added "weekly" to the frequency map; fundamental_fetch_frequency now
defaults to weekly (configurable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:23:16 +02:00
dennisthiessenandClaude Opus 4.8 e982487abd coordinate jobs: daily pipeline orchestrator runs the flow in order
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 39s
Deploy / deploy (push) Successful in 25s
Jobs were independent 24h timers with no ordering, so the scanner could run on
stale OHLCV, and manual runs desynced the offsets. New daily_pipeline job runs
the data→signal flow in dependency order: OHLCV → fundamentals → sentiment →
R:R scan → outcome eval (+paper close) → market regime. Each step keeps its own
enable flag and runtime status; a failing step is logged and the pipeline
continues.

The member jobs are registered PAUSED (no auto-fire) so they only run via the
pipeline — but stay manually triggerable from Admin → Jobs (shown as "runs in
daily pipeline"). Alerts (hourly), ticker universe sync, and backtest keep their
own independent cadence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:16:41 +02:00
dennisthiessenandClaude Opus 4.8 fb3b8d18d7 complete paper trading: auto-close on stop/target + My Trades realized record
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 38s
Deploy / deploy (push) Successful in 25s
resolve_open_trades walks the daily bars after each open trade and closes it at
the target (target hit) or stop (stop/ambiguous), leaving undecided trades open.
Runs nightly inside the outcome evaluator (so it's coordinated with fresh OHLCV)
and on its manual trigger. New "My Trades" section at the top of Signals → Track
Record shows realized hit-rate, expectancy (avg R), total R, total P&L, and a
closed-trades table — your actual results, separate from the theoretical signal
record below it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:49:28 +02:00
dennisthiessenandClaude Opus 4.8 7e87a15a12 admin Jobs tab: show job controls above the pipeline readiness table
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 22s
The 500+ row readiness table forced scrolling past it to reach the actual job
controls. Put JobControls first, readiness below.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:41:56 +02:00
dennisthiessenandClaude Opus 4.8 e5166ed668 sentiment: LLM buy/hold/avoid + full analysis, and search-budget scoping
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 34s
Deploy / deploy (push) Successful in 21s
Richer LLM output (same grounded call, ~no extra cost):
- All providers now also return a recommendation (buy/hold/avoid) and a thorough
  reasoning paragraph; Gemini now actually captures reasoning + grounding
  citations (it was dropping them). Stored on sentiment_scores (migration 008),
  exposed in the API; display-only — NOT fed into the composite/EV.
- Ticker Sentiment panel shows an "LLM view" badge and a "Full analysis & sources"
  expander with the complete reasoning + citations.

Search-budget scoping (Gemini grounding free tier = 5000/mo):
- collect_sentiment now targets only watchlist + open paper trades + top-N by
  composite, skips tickers refreshed within sentiment_fresh_hours (72h), and caps
  per run (sentiment_max_per_run). Once the relevant set is fresh, runs spend 0
  searches until it ages out — bounding monthly usage well under the free tier.
- Widened sentiment lookback to 7d (scoring + display) so sparser collection
  still feeds the dimension score.

Deploy: alembic upgrade (sentiment_scores.recommendation). Switch provider to
Gemini Flash in Admin for the cost win (grounded, cheapest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:34:19 +02:00
dennisthiessenandClaude Opus 4.8 a69557f5d8 add paper trading: mark a setup as taken, track open P&L, sell
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 24s
New paper_trades table (migration 007) + service/router. "Mark as taken" on each
setup card (shares prefilled from position sizing, entry from current price, both
editable) records a simulated trade. Overview gains an Open Trades table that
marks each position to the latest close — P&L in $, %, and R-multiples — with a
total unrealized P&L footer and a Sell button to close at the current price.
Closed trades are retained for future realized-P&L reporting.

Deploy: alembic upgrade (new paper_trades table).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 06:33:56 +02:00
dennisthiessenandClaude Opus 4.8 050abc6f71 backtest: add min target-probability sweep
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 40s
Deploy / deploy (push) Successful in 26s
Re-applies the activation gate at several min_target_probability thresholds
(60→30, other conditions fixed) over the already-replayed candidates, so the
trade-off between how many setups qualify and their expectancy is visible in one
table — the cheap "optimize" half of Phase 2. Candidates now carry meets_core +
best_prob so the sweep needs no re-replay. New sweep table in BacktestPanel with
the current threshold starred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 06:13:30 +02:00
dennisthiessenandClaude Opus 4.8 9d2e1e74bf fix probability over-confidence: model target-before-stop, not just touch
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 24s
Backtest (32k setups) showed the touch-only probability model was ~2x
over-confident — predicted 70% hit 39%, predicted 88% hit 46% — because it
ignored the competing stop. estimate_probability now multiplies the reach
probability (touch within horizon) by the two-barrier gambler's-ruin ratio
1/(R:R+1) = P(target before stop). A 3:1 setup now reads ~25% base, not ~70%,
which lines up with realized rates. Strength/alignment modulation unchanged.

Recalibrates every probability and the EV ranking; the min_target_probability
gate threshold now means roughly what it says. Re-run the backtest to confirm
the calibration table flattens toward the diagonal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:52:09 +02:00
dennisthiessenandClaude Opus 4.8 b00e482258 add backtest report UI to the Track Record tab
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 36s
Deploy / deploy (push) Successful in 23s
New BacktestPanel: shows qualified hit-rate/expectancy vs the all-setups baseline,
a by-direction breakdown, and the probability calibration table (predicted vs
realized, over-confident buckets flagged amber). Includes a "Run backtest" button
that triggers the job and a plain explanation of the method and its limits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:16:12 +02:00