max-w-md (448px) under-sized the panel vs its real full-width tab placement and
never triggered the desktop two-column layout. Widen to max-w-3xl; note to
resize to ~390px for the mobile (single-column) check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the four-cell quarter tape (too many equally-weighted numbers) with one
comparison rail per metric, so the panel answers "improving? sound? fairly
valued?" instead of asking the reader to decode it.
- Operating trend (revenue/EPS growth, operating/FCF margin, share count): a rail
centered on a truthful reference — prior quarter (growth), prior-period average
(margins), or zero (share count) — with a dot at the current delta and a bar
back to the reference, plus a shaded neutral band (backend's +-2pp / +-1pp /
+-1% rules). Value, read, reference label, and signed delta stay visible;
per-quarter history drops out of the default view.
- Valuation & balance (net debt/EBITDA, P/E, FCF yield): a 0-100 favorable-
percentile rail with the peer median fixed at 50; right is always more
favorable (percentile is polarity-aware). median + peer_count shown.
- Not a progress bar: reference line, not a 100% target.
- Horizon tokens: cyan #6EC9DB favorable / coral #EF9182 adverse / #5D6373 track,
replacing emerald/rose. Two columns on desktop, single column (rows stack) on
mobile. Null -> n/a with no rail; insufficient peers -> "peers n/a", no track.
- Kept: compact earnings line, provenance footer, local-date parsing, aria-labels
on every rail. tsc -b passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the static review + adds a dev-only visual harness:
1. Tape no longer overflows narrow mobile: each row stacks (label + read on one
line, cells below) under sm, keeping the single-line grid on desktop.
2. Date-only strings (earnings, price_date) are parsed as LOCAL calendar dates,
so a viewer west of UTC no longer sees the previous day.
3. Peer context is visible ("med X · Np") on every width and the percentile
strip carries a full aria-label — no longer hover-only / desktop-only.
4. Per-metric provenance + freshness surfaced (SEC filings · latest quarter,
filed date) replacing the removed panel-wide FMP label.
5. Same-day earnings render "today", not "in 0d".
Harness: frontend/harness.html + src/dev/harness.tsx (dev-only, served at
/harness.html by vite, not in the production build) render full /
partial-insufficient-peer / empty fixtures for desktop + ~390px review.
tsc -b passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reshapes FundamentalsPanel to consume the additive API v1, within the app's
existing dark-glass language.
- types.ts updated to the exact v1 shape (metrics/earnings/valuation/reads +
legacy fields preserved).
- The quarter tape is the single distinctive device: per-metric 4-cell tape
(revenue/EPS growth, operating + FCF margin, share count) with the latest cell
toned by the deterministic read; color is always paired with the read text.
- Restrained peer strips for Net debt/EBITDA, P/E, FCF yield: value + a
polarity-aware percentile bar with a median marker + the read; hidden ("peers
n/a") when industry is null (< 5 peers).
- Earnings: next date/session/countdown + last-N beat/miss arrows (▲/▼/·) with
text aria-labels; explicit "no date" state.
- Explicit n/a, insufficient-peer, and no-earnings states; header shows the
deterministic sentence. Removed the hard-coded "FMP" source label.
Frontend tsc -b passes; backend suite 778 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shadow book toggle saved on change and the numeric fields on blur,
inconsistent with every other admin panel (which stages edits behind a
Save button). Stage all four fields in local state and commit them
together on Save, with an unsaved-changes hint. This also makes enabling
the live-trade toggle a deliberate two-step action rather than an
unguarded single click.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The manual paper book only contains trades taken by hand, inside a 20
minute window, on days someone was available. The backtest that validated
this strategy auto-takes the top-ranked qualified setups up to capacity
every session. The forward record was therefore measuring strategy plus
discretion plus availability -- and degrading silently on busy days.
The shadow book closes that gap: it mirrors the backtest's selection rule
(top strategy_rank qualified, up to capacity, 1% fixed-fractional risk)
and shares the manual book's exit policy, so the only difference between
the two books is which setups get taken. Selection ordering reuses the
strategy_rank the scanner already stores rather than recomputing it, so
the two cannot drift apart. It runs as a near-close pipeline step right
after the scan, marking entries at the same prices a human would see.
Gate-reset re-entry state is now scoped per book -- the books diverge as
soon as their entries differ, and each must see only its own stops.
Performance view rewritten around the comparison:
- three series (shadow, manual, SPY) from a new endpoint
- SPY changes from a per-trade cost-basis counterfactual to plain
buy-and-hold %, since one line has to serve two books
- headline stats are R-multiples, not currency: the books size
differently, so only R compares across them
- configurable start date, because the strategy has been revised
repeatedly and pre-cutover trades ran under rules that no longer
exist
Migration 024 also repairs the numeric weekday crons written by 023,
rewriting only rows still holding the broken form so hand-corrected
settings survive. Its literals are inlined because bound parameters
render as NULL under 'alembic upgrade --sql'.
The shadow book is opt-in and writes nothing until enabled. Verify its
first selections match a backtest of that day's cross-section before
trusting any point on the curve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Display-only Da/Gurun/Warachka information discreteness on the ticker
indicator panel. Shared compute with the backtest harness; not wired into
gate or rank.
Regime is computed at 02:00 ET; without a morning alert pass, quadrant
changes waited until 15:30. Dispatcher is change-driven so quiet days stay
quiet. Drop unused alerts_frequency config.
Move the only qualifying R:R scan to 15:30 ET with chained Telegram alerts,
put outcome eval after a final-bar OHLCV fetch, enforce NY trading-day
requalify semantics, stamp paper trades fill_mode=near_close, and migrate
stored schedule_* keys to America/New_York.
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.
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.
Reconstruct the ATR/% trail bar-by-bar so the mini chart shows a ratchet path instead of a flat current level, and de-emphasize screening targets under trailing exit modes.
Keep support/resistance visible even near trade lines, tighten the how-this-exits spine so steps connect, unify step styling, and add space above the panel.
De-clutter setup labels (left roles, right prices; hide Entry near Now; S/R in-plot), and draw green/red arrows for paper trade entry and exit on the matching sessions.
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
The UI told a swing-trade story (entry -> target -> stop) while the engine runs
a momentum portfolio (buy strength, trail out, re-rank). The selection was
honest; everything around it was borrowed from a strategy we don't run.
The target is never an exit under `atr_trailing`: `_atr_trailing_close()` does
not even take it as a parameter. It exists only to compute the R:R and touch
odds that admit a setup through the activation gate. Backtested exit reasons for
the production strategy: 144 initial stop, 98 trailing stop, 78 max hold —
target 0. See docs/research/sr-levels-and-exits.md.
What changed:
- New ExitPlanPanel on every setup card states the rules that actually close the
trade: initial stop (1R), the price at which the 3x ATR trail takes over from
it, the trail width in R, and the max hold. Derived in lib/exitPlan.ts from the
live exit policy, so it follows Admin rather than hardcoding the default.
- New BaseRatesPanel replaces per-target "probability" as the answer to "what
usually happens": win rate, average hold, best/worst R, and how trades actually
ended — measured under the real exit, from the backtest report.
- "Target"/"target probability" relabelled to "level"/"touch odds" and grouped as
gate metrics, with the R:R. On the dashboard focus card, residual momentum
(the actual signal) takes the headline stat those two used to occupy.
- The take-trade dialog no longer offers a target dropdown whose value the exit
ignores; it states the trailing plan instead. The picker returns only when the
live policy is mode='target', where the choice is real. The stored target is
now the setup's own, not whichever row was last clicked while exploring.
- "Played out" is gone. A setup was declared dead once price reached the target —
backwards under a trailing exit, where reaching a level is the good case and
the trade keeps running. Only the stop invalidates a setup now; running past
the entry is an "extended" warning, measured in R (you'd be chasing).
The levels ladder, the price rail and the chart overlay all stay fully
explorable — clicking a level still drives them. It is framed as overhead
structure, which is what it is, rather than a menu of exits.
Adds a parity guard: the UI recovers ATR as |entry - stop| / 1.5, so the test
fails if the scanner's stop width ever moves.
Co-Authored-By: Claude Opus 4.8 <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>
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>
The Signals > Setups filter dropdowns (Direction, Recommended Action)
opened UNDER the panels below: glass containers use backdrop-filter,
which creates a stacking context, so the menu's z-50 could never
compete with later sibling panels regardless of value.
The menu now portals to document.body with fixed positioning tracked
against the trigger (re-measured on scroll/resize), and click-outside
accounts for the portaled node. Applies to every Dropdown usage.
Menu background also updated from the old green-tinted #151911 to the
Horizon #14161f.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PerfChart moves into the left column under Open Positions
(half-width), balancing the radar column.
Cleanup:
- Design mockup pages (DesignMockups / DesignHorizon / DesignOrbit)
and their routes removed - the redesign has shipped; the mockups
live on in git history. Main bundle shrinks ~97 kB and the
chunk-size warning goes away.
- frontend/tsconfig.tsbuildinfo untracked and gitignored (incremental
build artifact that churned in every commit).
- Dead npm test / test:watch scripts removed - vitest was never a
dependency; CI already skips the step via its vitest guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The focus|radar 3/5-2/5 pairing made the top-pick card cramped and the
full-width positions panel stretched its trade charts. Back to the
layout that worked: focus card full width, account ribbons, then
positions | radar side by side, performance chart at the bottom.
Kept from the rework: radar rows still swap the focus card - and now
reliably, because the ticker symbol inside a row is no longer a
navigation link (clicking anywhere in the row, symbol included,
selects it; the ticker page is reachable via the focus card's Ticker
details button). Below-gate inspection, badges, and the reset link
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The market regime banner took a full-width row on the dashboard for
ambient context. It is now a subtle status in the top bar (colored dot
+ "neutral regime", full headline and the longs/shorts caution in the
tooltip) linking to /regime - matching the mockup's nav.
The watchlist strip moves from the bottom of the dashboard (where it
got lost) into the freed spot right under the hero: a slim chip row
with today's move per symbol and the full-watchlist link. The empty
state is a one-line hint instead of a callout box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
- 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>
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>
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>