Commit Graph
124 Commits
Author SHA1 Message Date
dennisthiessen 5155d00d9e feat: require gate reset before post-stop reentry 2026-07-17 19:30:40 +02:00
dennisthiessen bbc7383d3a feat: compare legacy and live ranking universes 2026-07-17 17:07:35 +02:00
dennisthiessen 9800114fc4 fix: align daily matrix ranking universe 2026-07-17 16:39:43 +02:00
dennisthiessen 0cd9ee7689 feat: add daily reentry policy matrix 2026-07-17 16:11:18 +02:00
dennisthiessen 65a462271c feat: add selectable daily backtest cadence 2026-07-17 14:41:24 +02:00
dennisthiessen bc50ba9136 fix: harden post-stop reentry lockdown 2026-07-17 14:17:57 +02:00
dennisthiessen 1e9f2dc4fb feat: add five-session post-stop reentry lockdown 2026-07-17 13:21:06 +02:00
dennisthiessen f714782fa4 fix: use categorical regime fundamentals
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m13s
Deploy / deploy (push) Successful in 37s
2026-07-15 10:13:22 +02:00
dennisthiessen 81c6f5fe2f fix: refresh latest regime trading session
Deploy / lint (push) Successful in 10s
Deploy / test (push) Successful in 1m14s
Deploy / deploy (push) Successful in 39s
2026-07-15 09:13:42 +02:00
dennisthiessen 1d5b1489be feat: replace regime monitor with v2 methodology 2026-07-15 09:02:56 +02:00
dennisthiessen fd21067a40 Fix S&P 500 universe parse so renames like BNY are discovered.
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m6s
Deploy / deploy (push) Successful in 37s
Wikipedia no longer uses plain symbol table cells; parse exchange links and NyseSymbol templates, surface the list source in bootstrap results, and keep legacy cell parsing as a fallback.
2026-07-14 14:14:58 +02:00
dennisthiessen f59c0c3484 Add system alerts log with nav badge and Admin Alerts tab.
Deploy / lint (push) Successful in 8s
Deploy / test (push) Failing after 1m4s
Deploy / deploy (push) Has been skipped
Persist job and ingestion warnings/errors for 7 days, surface a dismissible top-nav badge, treat stale OHLCV as a warning (e.g. ticker renames), and show market bar age on the ticker freshness chip.
2026-07-14 13:38:04 +02:00
dennisthiessen 8db535b889 Fix manual refresh dropping qualified ranks and clarify trade UI.
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m20s
Deploy / deploy (push) Successful in 39s
Single-ticker fetch now attaches residual-momentum ranks so setups do not silently fail the activation gate. Exit plan is a timeline, chart labels move left of the price scale, and missing ranks surface explicitly.
2026-07-14 10:10:30 +02:00
dennisthiessen bddaeb9110 Cleanup retired S/R research scaffolding
- Remove unused _gate_eligible_levels filtering logic and its tests (research-only)
- Add prominent RESEARCH/DIAGNOSTIC markers and docs to clear-air/ATR fallback helpers
- Document production vs research BACKTEST_* environment variables in backtest_service
- Minor cleanups: update legacy report text, improve outdated function docstring
2026-07-13 18:52:24 +02:00
dennisthiessen bee5a5ce89 Finalize GTL and retire S/R research harness 2026-07-13 17:58:04 +02:00
dennisthiessen 9d362bd568 Default online backtest to production GTL 2026-07-13 16:51:55 +02:00
dennisthiessen 8e09f239c8 Retire completed GTL tuning harnesses 2026-07-13 16:40:29 +02:00
dennisthiessen 623dc08875 Add GTL cohort composition backtest 2026-07-13 14:22:56 +02:00
dennisthiessen 3999c5efc1 Add single-command GTL tuning matrix 2026-07-13 13:13:18 +02:00
dennisthiessen 8161c352a0 Separate chart S/R from gate target ladder 2026-07-13 11:17:03 +02:00
dennisthiessen f0e6a8fc19 Isolate explicit S/R gate target ladder 2026-07-13 10:49:44 +02:00
dennisthiessen 01e6f7e2c3 Test clean S/R as production rank overlay 2026-07-13 10:33:42 +02:00
dennisthiessen e31d1704a2 Isolate residual S/R target selection 2026-07-13 09:05:28 +02:00
dennisthiessen 1daf762bda Isolate legacy range-expansion factor 2026-07-13 08:37:55 +02:00
dennisthiessen f7c2e35e29 Isolate legacy range-grid features 2026-07-13 07:57:52 +02:00
dennisthiessen b891122936 Fix post-cluster neutral strength ablation 2026-07-12 23:51:28 +02:00
dennisthiessen 93403b4d3a Document S/R findings and isolate legacy gate features 2026-07-12 23:38:09 +02:00
dennisthiessen bd72fa75f9 Separate S/R detector tests from primary R:R policy 2026-07-12 23:18:38 +02:00
dennisthiessen cb64f7bf65 Fix bounded S/R training portfolio calendars 2026-07-12 22:45:05 +02:00
dennisthiessen 19b81c169d Add S/R v2 research and validation harness 2026-07-12 21:15:18 +02:00
dennisthiessenandClaude Opus 4.8 ea11efe3d1 Sweep the R:R floor; fix a holdout metric artifact
Deploy / lint (push) Successful in 10s
Deploy / test (push) Successful in 1m9s
Deploy / deploy (push) Successful in 37s
min_rr = 2.0 was hand-set in Admin (2026-06-24) and never swept — the gate
ablation only tested the floor on-vs-off, never its level. It was the last
un-swept knob in the live gate.

Swept against portfolio Sharpe under the real exit, with a parity self-check
(reproduces_production_gate: the row at the live floor must rebuild production's
exact 1,089-setup qualified set — it does).

  min_rr   qualified   in-sample Sh/CAGR   OOS Sh/CAGR (entries >= 2024-07)
  0.0        6636      1.98 / 58.5%        2.02 / 66.2%
  1.2        3897      1.34 / 33.9%        1.12 / 28.8%
  1.5        3127      1.20 / 29.6%        1.12 / 28.8%
  1.75       1974      1.64 / 44.5%        1.15 / 27.4%
  2.0 (live) 1089      2.04 / 50.4%        2.78 / 73.3%
  2.25        577      1.64 / 31.8%        1.71 / 31.9%
  2.5         286      1.67 / 29.0%        0.68 /  8.7%

KEEP 2.0. It is the optimum in both windows, and a peak that reproduces in data
it was never fitted to is real evidence. But treat it as fragile: unlike the ATR
trail (a plateau), this is a spike with a trough beside it — +/-0.25 costs ~0.4
Sharpe in-sample and ~1.6 out-of-sample — and the curve is bimodal (floor-off is
good, 1.2-1.75 is bad, 2.0 is good). The hand-set value landed on the peak by
luck, not by tuning. Do not nudge it.

Worth knowing: turning the floor OFF entirely is the second-best row in both
windows, with substantially higher CAGR (58.5% / 66.2%) and more trades. If CAGR
ever outranks Sharpe here, "no R:R floor" is a live option — and it would sever
the gate's last dependency on the weak S/R detector.

Also fixes a metric artifact in the holdout harness. The train book's equity curve
ran to the end of the data while its entries stopped at the split, so it sat in
flat cash for two years and deflated its own CAGR/Sharpe (reported 0.95 / 14.6%;
actually 1.31 / 29.6%). _simulate_portfolio now truncates the calendar to
hold_days after the last entry when end_date is set — it only triggers on the
holdout train window, so no other number moves. The clear-air OOS verdict is
unaffected: it rests on the test row, whose entries and curve both start at the
split and were always clean. Both holdout reports regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:45:59 +02:00
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
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
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 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 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 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 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 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 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
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