Document Phase A (max-hold/vol/corr closed; next-open as decision baseline).
Add stale_close and next_open gap-cap fill modes plus a small matrix to test
whether near-close scheduling recovers overnight momentum drift.
Ship shared Sharpe SE/PSR diagnostics, next-open fill and equity-curve vol targeting in the portfolio simulator, re-derived fip_id, and a checkpointed offline matrix runner for Mac-side validation sweeps.
Honor custom S/R tolerance as a transient detect, refresh levels after OHLCV
mutations without failing committed price writes, report per-ticker S/R
rebuild failures from admin cleanup, and warn in the admin UI when refresh is partial.
Ship greenfield min_rr=2.0 and conf=0, read-only Structural S/R, indicator
cache invalidation, and UI/gate language that treats GTL as screening not exit.
Align strategy_rank missing-vol fallback live vs backtest, single-source
PRIMARY_TARGET_MIN_RR, expand prod parity tests, and drop dead FE clients.
Finnhub profile2 reports marketCapitalization in millions; storing it
as dollars made mega-caps like SPCX show as micro (e.g. 1.8M). Normalize
on ingest, add unit tests, and include a one-shot SQL backfill script.
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.
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.
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.
- 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
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>
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>
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>
Three follow-ups to the gate probability floor (8f41143):
- Signals table shows the starred primary target (shared primaryTarget
helper) instead of an independently computed max-probability best,
so Overview, Signals and ticker details agree by construction.
- Targets pinned at the 3% probability clamp floor collapse to the
nearest one (enhance_trade_setup + backtest candidates in parity):
floor-pinned levels are indistinguishable to the model, so farther
ones were duplicate 3% rows inviting lottery headlines.
- get_trade_setups only returns setups re-emitted within
LIVE_SETUP_MAX_AGE_DAYS (3): an older latest row means the daily
scan no longer confirms the setup, and such rows otherwise surface
forever on Overview/Signals/ticker/alerts. History endpoints keep
full history.
Backtest on the Jul-3 snapshot is metric-identical to the gate-floor
run on all qualified stats (1089 qualified, Sharpe 2.02, CAGR +49.6%,
DD -15.8%): the prune only removes noise the gate already rejected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A qualified setup's primary target must now clear MIN_TARGET_PROBABILITY
(20%), shared with the primary-selection floor in recommendation_service
and mirrored in the frontend gate. Closes the read-time hole where a
stale pre-c7a198b row starring a far lottery target (probability pinned
at the 3% clamp floor, R:R inflated by the same distance) qualified
forever: the scanner emits no replacement row and live R:R never decays.
A/B backtest vs c7a198b baseline (same July-3 snapshot): 7 of 1096
qualified setups removed; qualified net avg R 0.202 -> 0.207, hold
Sharpe 2.00 -> 2.02, CAGR +48.8% -> +49.6%, max DD unchanged at -15.8%.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
A local backtest (offline prod snapshot, 506 tickers) evaluated blue-sky
projected targets under the PRODUCTION exit (3x ATR trailing + 30d max hold,
paper_trade_service DEFAULT_EXIT_MODE="atr_trailing"). Blue-sky setups are
dilutive: the qualified book scored 328% return / Sharpe 1.84 / DD -21.0%
WITHOUT them vs 300% / 1.58 / -18.7% WITH them. They rank high on momentum by
construction, so they grab slots from S/R setups that catch bigger runs under
a trailing-stop exit (only ~2pp worse drawdown doesn't justify the lost return
and Sharpe).
Reverts the scanner/TargetGenerator measured-move projection, the stricter
projected activation gate, the frontend qualification mirror, the `projected`
type field, and the projected tests -- all backend files are now byte-identical
to the pre-blue-sky commit.
Keeps the played-out "No current setup" UX (RecommendationPanel): when price
has run past the target (played out) or through the stop (invalidated), the
panel shows a plain no-setup state instead of a stale actionable card. This is
frontend-only (reads last close + existing setup fields) and is what actually
fixes the reported stale-below-price bug -- no backend change or rescan needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes stale below-price setups showing as current recommendations. Three
distinct causes share the symptom (get_trade_setups returns the latest stored
setup per direction and never expires it):
- Genuine blue-sky (no overhead S/R): scanner + TargetGenerator now project a
measured-move target (entry +/- 3*ATR, ~2:1 R:R), flagged projected with a
low sr_strength probability haircut. Overhead check keys on level tag OR price
so it never projects through a straddling resistance cluster.
- Projected targets clear a stricter activation bar (long-only, momentum >= 90,
confidence >= min+10), independent of the general momentum gate. Mirrored in
frontend qualification.ts.
- Played-out UX (fixes the reported TTWO case, which is R:R-starved under a
resistance cluster, not blue-sky): when price is at/past target or through the
stop, RecommendationPanel shows a "No current setup" state and softens the
stale ticker-level header/reasoning, instead of a stale actionable card.
No migration: the projected flag rides in existing targets_json. 504 backend
unit tests pass; frontend typechecks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>