Selecting a different window or a comparison strategy silently made the tiles
stop matching the recommendation below, which is baked into the report and
cannot follow a dropdown. On load they now agree by construction; moving off
that basis says so.
Also: an absent production row produced no headline and no benchmark, but any
passing gate finding still rendered a green "no warnings" chip — a success badge
for missing data, directly beside "this report predates the portfolio monitor".
Missing baseline now reads "baseline unavailable".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five UI problems, all reported from using the page.
Expanding "How this is measured" shoved every control down, because the
disclosure and the run controls shared one flex row. They no longer do: run
status and the controls that start a new run sit together on one line, and the
explainer is below them where growing it moves nothing.
A long strategy name wrapped the dropdown trigger onto three lines and dragged
the row out of alignment. The trigger now truncates with the full text on hover
— a wrapping dropdown is broken anywhere, so the fix is in the primitive — and
the twelve-character "Production: " prefix is a bullet.
"Sortino 2.72" answered nothing. Each risk-adjusted metric now carries a meter:
a track showing where the value sits, ticks at the band edges, and the band word.
Colour never travels alone. Bands are deliberately stricter than textbook ranges
because this universe is today's survivors replayed backward, which flatters
every ratio — that caveat is stated next to them rather than left implied.
The two tile rows were different sizes, which read as inconsistent rather than
as hierarchy. Every tile is the same size now and grouping carries the ranking:
top row is raw outcome and takes no meters, second row is risk-adjusted ratios
and all take meters. Sharpe moved down to join them — it is one of those ratios,
and leaving it above made it the only metered tile in a row of bare ones.
The recommendation led with a long bold sentence that describes the
configuration, not a verdict, while the actual findings were small grey text.
Findings now come first, each split into label and detail on the colon the
backend strings already carry, and the configuration is a footer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One tab stacked three things that all called themselves a track record:
realized paper P&L, setup-outcome grading under the rejected take-profit model,
and the backtest portfolio simulation. Split into Setups | Paper Trades |
Backtest, one subject each. `track` stays the Paper Trades slug so the legacy
/performance redirect keeps working. The grading diagnostic and its Evaluate /
Reset controls go with Backtest, not Paper Trades — reset_track_record deletes
trade_setups, not paper trades.
BacktestPanel 439 -> 175 lines. Its run settings alone were 106 lines of
hand-rolled sr-only radio cards for two binary choices; they are now two
Dropdowns and a button on one wrapping row, with the per-option prose moved into
the existing explainer. The amber warnings survive as a conditional slot, so a
non-default choice still announces itself but the common path is silent.
The recommendation printed eight findings at equal weight, burying the verdict
in tuning detail. `topic` now splits them: production, benchmark and robustness
stay inline, gate/exit/cutoff collapse behind a disclosure, and any WARNING or
LAGS item is promoted out of the collapsed group regardless of topic. No topic
chips — every backend string already self-prefixes, so a chip would render
"GATE | Gate: ...".
Portfolio metrics are now two tiers: five headline tiles for what the book
returned, then a smaller labelled row for how good that return was (Sortino,
Calmar (MAR), Gain/Pain, Profit Factor $, EV/trade). Reports cached before those
metrics existed hide the second row rather than showing a half-populated line of
dashes.
Extracted EquityCurveChart, PortfolioMonitorPanel and BacktestRecommendationCard,
plus a StatTile primitive and shared formatters for the duplication in the files
this touched. DashboardPage and OpenTradesPanel deliberately keep their own
copies — migrating them is separate scope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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
The portfolio-monitor filters were native <select> elements with a
hardcoded bg-slate-950 - a look from before the design system. They
now use the shared Dropdown (same as the Setups filters), which also
gives them the dark themed menu and the portal fix from 6464fdd.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
The robustness warning was computed on the target-model distribution
while the same panel recommends the hold exit — internally inconsistent.
_robustness_stats (median, profit factor, ex-top-5% expectancy) is now
shared by _bucket_stats and _time_exit_bucket, the time-exit table shows
Median Net R and Ex-Top-5% per hold length, and _build_recommendation
reads the trimmed expectancy from the recommended exit's bucket (falling
back to the target model when no hold is recommended).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Robustness (answers 'is the edge just outliers?'):
- _bucket_stats gains median_net_r, profit_factor, and net_avg_r_ex_top5
(expectancy with the top 5% of winners removed); shown as stat tiles.
- Portfolio sim gains per-calendar-year returns, shown in the sim table.
Dynamic recommendation ('What this backtest recommends' panel):
- _build_recommendation derives advice from the report's own numbers on
every run — exit policy (target vs best hold, with sim CAGRs), which
gate floors earn their keep (ablation Hold column), best momentum
cutoff, book-vs-SPY verdict, and an outlier-dependence warning when
the trimmed expectancy goes non-positive.
Retired (conclusions reached, tables removed from report + UI):
- Take-profit sweep (no interior optimum — fixed TP is the wrong tool
for momentum), trailing sweep (converged to the hold-to-horizon exit),
probability calibration (model is display-only by decision).
- _tp_primitives slimmed to _risk_and_stop_day; trailing machinery gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-trade additions to the report:
- Gap-through-stop fills: stops now fill at the worse of the stop or the
bar's open across every exit model (target, TP, trailing, time), so a
loss can exceed -1R; targets never fill better than their level.
- best_r / worst_r, avg holding days, and net R per day of capital
deployed on the summary buckets and the time-exit sweep.
Portfolio simulation (the stats a per-setup replay cannot give):
- One capital-constrained book over the qualified setups: 10k start, max
10 concurrent positions (one per ticker, best momentum first), 1%
fixed-fractional risk with a 20% no-leverage notional cap, entries at
the detection close, 0.1%/side costs, daily mark-to-market.
- Two exit policies compared: S/R target race vs hold-to-horizon.
- Equity-curve stats: final equity, total return, CAGR, max drawdown,
annualized daily Sharpe, win rate, avg P&L, best/worst trade, avg
hold, entries skipped on a full book, and SPY price return over the
same window (benchmark history refreshed to cover the replay span).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ablation judged floors under the target/stop model, but the exit
sweeps point at replacing that exit with a fixed hold — under which the
R:R floor's rationale (bigger payoff at the target) may not apply. Each
ablation row now also carries hold_avg_r / hold_net_avg_r / hold_total_r
(30d hold, initial stop only), so the Phase 3 gate decision can be read
under the exit policy that would actually be used.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 of the strategy-measurement plan — report-only, no production
trading behavior changes:
- Cost haircut: every bucket/sweep now reports net_avg_r/net_total_r
alongside gross (COST_PER_SIDE=0.1% of notional, converted to R via
each setup's stop distance); params carry cost_per_side_pct.
- Gate ablation table: re-qualifies candidates at the current momentum
cutoff with one floor removed per row (confidence / R:R / NEUTRAL /
momentum-only) to show which floors earn their keep.
- Time-based exit sweep: hold 5/10/21/30 days with the initial ATR stop,
exit at the day-N close — the classic momentum implementation, to
disambiguate the wide-trailing result.
- TP sweep extended to +40/+50%, trailing to 25/30% so the optima are
interior instead of starred at the sweep edge.
- BacktestPanel: Net Avg R columns everywhere, gate-ablation and
time-exit tables, stars now mark best net avg R; stale cached reports
still render (all new fields optional/guarded).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>