Skip to content

Fix wheel options chain#7

Open
rmalhotra25 wants to merge 141 commits into
ryananicholson:i01from
rmalhotra25:fix-wheel-options-chain
Open

Fix wheel options chain#7
rmalhotra25 wants to merge 141 commits into
ryananicholson:i01from
rmalhotra25:fix-wheel-options-chain

Conversation

@rmalhotra25

Copy link
Copy Markdown

No description provided.

rmalhotra25 and others added 30 commits August 9, 2021 20:16
Replaces the trivial PHP app with a full Python+React trading recommendation
platform featuring three analysis tabs, AI-powered recommendations via Claude,
and scheduled runs at 9:00, 9:45 AM, 12:00, 3:00, and 6:00 PM Eastern.

Backend (FastAPI + SQLite):
- Options tab: top 5 options trades with entry/exit/stop, score (0-100), grade (A-F)
- Wheel Strategy tab: top 5 put-selling candidates; accept → track position state
  machine (put_active → assigned → call_active → closed); weekly Claude-generated
  covered call suggestions for assigned positions
- Long-Term tab: top 5 growth/income picks (12+ month horizon) with target price
- APScheduler cron jobs at 5 daily times (EST) + Monday call suggestion refresh
- News scraping: Reuters/CNBC/MarketWatch/SeekingAlpha RSS, Yahoo Finance, Stocktwits
- Stock data via yfinance (free, no key required)
- AI analysis via claude-sonnet-4-6 (ANTHROPIC_API_KEY env var)

Frontend (React + Vite):
- Three-tab layout with TradingView chart embeds per recommendation
- ScoreBar (0-100) and GradeChip (A-F) conviction indicators
- Wheel tracker: Accept modal → live position cards with status transitions,
  inline call entry form, history log, P&L tracking
- Manual "Run Analysis" refresh button on each tab

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
yfinance and feedparser fail to build on this server due to C extension
issues with multitasking/sgmllib3k. Both are now optional:
- stock_data.py falls back to Yahoo Finance JSON API directly
- news_scraper.py falls back to requests+BeautifulSoup XML parser

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…y proxy

- News scraper: short 8s timeout so blocked sources fail fast instead of hanging
- All three engines: when no news items are scraped, pass Claude a context note
  about today's date and current macro themes so it still produces quality output
- Stock data: already has HTTP fallback from previous commit

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
- Added full NYSE holiday calendar computed dynamically each year:
  New Year's, MLK Day, Presidents' Day, Good Friday, Memorial Day,
  Juneteenth, Independence Day, Labor Day, Thanksgiving, Christmas
- Observed dates handled correctly (Sat→Fri, Sun→Mon)
- Cron triggers fire Mon-Fri; is_trading_day() check inside each job
  skips execution on market holidays
- Call suggestion refresh moved to Tuesday to avoid Monday holidays
- Verified: today Good Friday Apr 3 2026 correctly marked CLOSED

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
Mounts the frontend/dist directory as static files so the entire app
is served from a single port (8000), making deployment simpler.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…app-eTOFH

Add TradeIQ trading recommendation application
Stock data (stock_data.py):
- RSI (14-day) — overbought/oversold timing
- MACD with crossover detection — momentum shifts
- Bollinger Bands with %B and squeeze flag — volatility context
- 20/50/200-day moving averages with golden/death cross — trend
- ATR (14-day) — daily expected move for sizing entries/exits
- VWAP — intraday mean reversion level
- Fibonacci retracements (23.6%, 38.2%, 50%, 61.8%, 78.6%) — support/resistance
- Pivot-based support/resistance levels (S1, S2, R1, R2)
- Volume trend (5d vs 20d avg) — accumulation/distribution

All computed from raw OHLCV price history via pandas/numpy — no extra APIs.

Claude prompts updated:
- Options: cite RSI, MACD crossover, Fib levels, BB squeeze in explanations;
  set strikes near key technical levels; use ATR to size targets
- Wheel: require uptrend (above MA50+200), place puts below Fib support,
  penalise death-cross stocks; explain trend + support in every recommendation
- Long-term: require above MA200 for all picks; favour golden cross;
  cite 52w range position and volume accumulation
- Call suggestions: use Fib resistance + BB upper to select strike; cite ATR

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…app-eTOFH

Add technical indicators (RSI, MACD, Fibonacci, Bollinger Bands, MAs, ATR, VWAP)
…edictions (#3)

- New quant_scorer.py: pure math scoring engine with component scorers for
  RSI, MACD, Bollinger, MAs, Fibonacci proximity, volume, IV rank, and R/R;
  composite scores for options (8 components), wheel (6), and long-term (5)
- Entry/exit calculators using ATR x 2 targets and Fibonacci support levels
- DB models updated: quant_score, qual_score, combined_score, quant_components
  plus underlying_entry/target/stop (options), buy_zone/invalidation (longterm),
  pct_otm/breakeven (wheel)
- Claude prompts updated to return qual_score and entry/exit fields; quantitative
  baseline passed as context so Claude adjusts from an anchored starting point
- Combined score = 40% quant (math) + 60% AI qualitative judgment
- New DualScorePanel component: purple QUANT bar + orange AI bar + bold COMBINED
- All three tab cards updated to show dual scores and entry/exit/stop tables

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
- recommend_strategy_type(): picks best structure from technicals —
  range-bound + high IV → iron_condor; mild bull/bear + high IV →
  credit spreads; strong directional → single_leg; breakout squeeze → single_leg
- compute_entry_exit_multi_leg(): calculates all four strikes using ATR
  wing sizing and Fib levels; computes max profit/loss and breakeven range
- DB/schema: strategy_type, short/long call/put strikes, net_credit,
  max_profit, max_loss, breakeven_low, breakeven_high
- Claude prompt updated: selects best structure for each setup and returns
  multi-leg JSON fields; quant baseline now includes strategy suggestion
- OptionsTab: color-coded strategy badge (yellow=condor, green=bull put,
  red=bear call, blue=bull call, purple=bear put); dedicated payoff tables
  per strategy type with strikes, credit/debit, profit zone

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
Previously the frontend polled only once after 30s. On the free Render
tier, analysis takes 60-120s (Claude API + data fetching), so results
were never displayed. Now polls repeatedly until a newer batch appears
or 3 minutes elapse. Button shows 'Analyzing...' while polling is active.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
anthropic==0.34.2 passes a 'proxies' kwarg to httpx which was removed
in httpx 0.28+. Upgrading to >=0.40.0 fixes the incompatibility.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
- database.py now reads DATABASE_URL env var first (PostgreSQL on Neon/
  Supabase), falling back to SQLite for local dev
- config.py: added database_url setting
- render.yaml: replaced DB_PATH with DATABASE_URL (sync: false — entered
  manually in dashboard)
- Added psycopg2-binary for PostgreSQL driver
- pool_pre_ping=True so connections auto-reconnect after idle periods

To complete setup: create a free Neon database at neon.tech and paste
the connection string as DATABASE_URL in Render's environment variables.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
- New run_status.py: tracks state (idle/running/ok/error) + error message
  per engine; all three engines update it on start/success/failure
- New /api/status endpoint returns current state for all three engines
- useRefreshPoller now checks /api/status on each poll — if the backend
  reports an error it surfaces it immediately as a red banner instead of
  silently timing out after 3 minutes
- onDone callback ensures the Analyzing... button resets on both success
  and failure

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…unding

- Added _round_strike(): snaps strikes to real option increments
  ($0.50 / $1 / $2.50 / $5 / $10 based on stock price)
- Added estimate_atm_premium() / estimate_otm_premium(): simplified
  Black-Scholes using ATR-derived annualised vol (ATR / price * sqrt(252))
- compute_entry_exit_options(): now returns rounded strike + BS-estimated
  entry/target/stop premiums for 7-day expiry
- compute_entry_exit_multi_leg(): all 4 strikes on standard increments;
  wing premiums computed via BS for each leg; credits = sum of spreads
- Claude prompt now anchors to BS estimates with label
  'use these as realistic anchors'
- UI: premiums labelled 'Est.' with disclaimer 'verify with your broker'

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…tions

Single-leg options: show 4 rows at +25%/+50%/+75%/+100% premium gain,
each labelled "Sell 25% of position", with per-contract and total P&L.
Credit spreads (iron condor, bull/bear spreads): show early-close targets
at 25%/50%/75% profit capture with buy-back prices.
Debit spreads (bull/bear call/put): show scale-out at same 4 levels.
Removes the unrealistic single 2× (100%) target display.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…examples

Add _valid_expiry_dates() helper that computes the next 6 upcoming Fridays
(at least 5 calendar days away) from today's actual date. Pass the list
explicitly into every Claude prompt for options, wheel, and covered call
analysis with a hard instruction to use only those dates.

Replace all hardcoded "2025-04-18" example dates in JSON schema strings
with dynamically generated dates so Claude never anchors on stale year/month.

Fixes: app showing May 16 (Saturday) when broker shows May 15 (Friday).

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
Ticker universe (stock_data.py):
- Expanded DEFAULT_UNIVERSE from 40 to ~90 liquid S&P/NASDAQ names
- Added mid/large-cap tech (AVGO, QCOM, MU, LRCX, MRVL, TXN, AMAT)
- Added cybersecurity/cloud (CRWD, PANW, NET, SNOW, DDOG, ZS, OKTA, PLTR)
- Added consumer/fintech (UBER, ABNB, COIN, RBLX, AXP, BLK)
- Added healthcare (LLY, MRK, DHR, TMO, ISRG), energy (SLB, OXY),
  industrials (LMT, RTX, GE, ETN), and more retail/staples (SBUX, NKE, T)

Stock Lookup tab (new):
- routers/lookup.py: POST /api/lookup/analyze — fetches technicals,
  fundamentals, and news for any ticker then calls Claude for analysis
- claude_analyst.py: analyze_stock() — returns rating (BUY/SELL/HOLD),
  conviction level, short-term and long-term price targets with timeframes,
  support/resistance levels, upside catalysts, risks, technical summary
- StockLookupTab.jsx: search input + quick-pick chips (AAPL, NVDA, etc.),
  results card with color-coded rating badge, dual outlook panels,
  key levels grid, catalysts/risks, technical summary, TradingView chart

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
1. lookup.py: get_price_and_technicals() takes a single str, not a list —
   removed erroneous [ticker] list wrapper; added regex ^[A-Z]{1,5}$ input
   validation to reject symbols with digits or special characters

2. claude_analyst.py analyze_stock(): two wrong dict key names —
   fib.get('fib_50') → fib.get('fib_500') (matches stock_data.py output)
   technicals.get('volume_vs_avg') → technicals.get('volume_trend',{}).get('ratio')
   (volume_vs_avg key doesn't exist; ratio lives inside the volume_trend dict)

3. options_engine.py _store(): single-leg strike had no fallback if Claude
   omitted it — now falls back to quant-computed ee['suggested_strike']

4. quant_scorer.py Black-Scholes: d1 was missing the risk-free rate term.
   Added r=0.045 (4.5%) to d1/d2 and discounted the strike in put/call formula
   (strike × e^{-rT}). Makes OTM premium estimates materially more accurate.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
stock_data.py:
- Added _bs_put_delta() and _bs_put_theta_daily() using full Black-Scholes
  with per-strike implied volatility (yfinance returns IV as fraction, used correctly)
- Added get_put_tiers(): fetches real options chain, targets 14-45 day expiry,
  computes delta/theta for every liquid put, groups into 3 tiers by assignment
  probability (45%/30%/16% delta targets)
- Added get_chain_context(): returns compact real bid/ask strings for nearby
  strikes on each ticker — used by options engine to give Claude live prices

claude_analyst.py:
- Added analyze_wheel_custom(): prompts Claude with real market data + all 3
  put tiers; instructs plain English throughout (no jargon). Greeks translated:
  delta→assignment chance, theta→daily income, IV→options expensive/cheap,
  breakeven→protected down to $X

options_engine.py:
- _format_technicals() now accepts chain_context dict; appends real bid/ask
  chain per ticker so Claude uses live prices not BS estimates for entry/exit
- run() calls get_chain_context() on top 20 tickers and passes to formatter

routers/wheel.py:
- Added POST /api/wheel/custom-analyze: fetches technicals, real put tiers,
  fundamentals, news for any ticker; returns full plain-English AI analysis

frontend:
- WheelCustomAnalysis.jsx: ticker input + 10 popular wheel stocks as chips;
  results show rating (GOOD/NEUTRAL/AVOID), company assessment, 3 color-coded
  tier cards with all plain-English descriptions, overall verdict
- WheelTab.jsx: WheelCustomAnalysis placed above scheduled recommendations
- api.js: added wheel.customAnalyze()

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…hours use

Key bug fixes:
- get_put_tiers: fall back to lastPrice when bid=0 (markets closed/after-hours)
- get_put_tiers: fix atm_row selection using idxmin() instead of argsort().iloc[0]
- get_chain_context: fix _nearby() index bug using idxmin() for non-contiguous indexes
- _best_near_delta: use pre-computed _mid (with lastPrice fallback) instead of raw bid/ask
- Expose data_source ('live' vs 'last_trade') in API response and frontend notice

New features synced from master:
- Stock Lookup tab: on-demand buy/sell/hold analysis for any ticker
- Wheel custom analysis: any ticker with 3 put-selling tiers (plain English)
- Expanded stock universe from 40 to ~90 liquid S&P/NASDAQ tickers
- Black-Scholes put delta/theta with correct r=0.045 risk-free rate
- Valid Friday expiry dates injected into all Claude prompts (fixes wrong dates)
- Real yfinance options chain context passed to Claude for all tabs

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
Root cause: DB_PATH pointed to /tmp/trading.db on Render, which is wiped
on every redeploy — all tracked positions were lost silently.

Changes:
- database.py: use DATABASE_URL env var (PostgreSQL) when set, fall back
  to SQLite for local dev. Handles postgres:// → postgresql:// rewrite
  for Render's connection string format.
- requirements.txt: add psycopg2-binary for PostgreSQL driver
- render.yaml: document DATABASE_URL env var with setup instructions
- .env.example: document DATABASE_URL option
- wheel router: add GET /api/wheel/positions/export endpoint returning
  all positions + full history as JSON
- WheelTab: add "Backup Positions" button that downloads positions.json
- api.js: add wheel.exportPositions()

To enable persistence: set DATABASE_URL in Render dashboard to a
PostgreSQL connection string (free tier at neon.tech works well).

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
- database.py: support DATABASE_URL for PostgreSQL (Neon/Render) so trade
  history survives redeploys; falls back to SQLite for local dev
- requirements.txt: add psycopg2-binary for PostgreSQL driver
- stock_data.py: use lastPrice fallback when bid=0 (after market hours),
  fix idxmin() index bugs for atm_row and _nearby selection
- claude_analyst.py: propagate data_source (live vs last_trade) into prompt
- routers/wheel.py: expose data_source in custom-analyze response,
  add GET /api/wheel/positions/export for JSON backup
- api.js: add exportPositions helper
- WheelTab.jsx: add Backup Positions download button
- WheelCustomAnalysis.jsx: show data-source note in footer
- render.yaml: add DATABASE_URL env var placeholder with instructions

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
Market Context Banner (all tabs):
- Fetches VIX + SPY from yfinance, classifies regime (Bull/Bear/Sideways/Volatile)
- Green/Yellow/Red trade verdict with per-strategy guidance
- Cached 30 min in memory, manual refresh button
- Expandable panel shows guidance for options, wheel, and long-term

Account Manager (Wheel tab):
- Stores capital ($25k default) in database, persists across deploys
- Add funds / set balance / withdraw with transaction log
- Shows: total capital, capital at risk, available, max per-trade (10% rule)
- Warns when >50% of account is at risk across open positions

Position Sizing on Wheel Cards:
- Each recommendation shows capital required and % of account
- Red warning if it exceeds your 10% per-trade limit

Performance Tracker (new tab):
- Win rate, total P&L, avg return per trade, W/L record
- Open positions table with status + capital at risk
- Closed trades table with P&L and return %

Watchlist (new tab):
- Add any ticker, score it across all 3 strategies simultaneously
- Claude provides wheel/options/long-term score + grade + summary
- Earnings warning if earnings within 14 days
- Remove and refresh scoring on demand

Backend:
- models/account.py, models/watchlist.py
- routers/market.py, account.py, performance.py, watchlist.py
- services/market_context.py (VIX/SPY regime classification)
- claude_analyst.py: score_watchlist_ticker() method
- stock_data.py: get_stock_info() convenience helper

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
Replaces horizontal tab bar with a left-side nav panel (200px).
Prevents tab overflow as more sections are added. Layout is now
header → market banner → sidebar + content side by side.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
Backend:
- models/champion.py: stores wheel/options/longterm champions
- services/champions_engine.py: pre-screens ~50 stocks with Python
  (price, volume, RSI, earnings filter) then sends ONE batched Claude
  call to pick the best stock per strategy — ~$0.02-0.05 per run
- services/claude_analyst.py: pick_champions() batch method
- routers/champions.py: GET /api/champions, POST /api/champions/refresh
- scheduler.py: daily 9:10am Eastern job (Mon-Fri, trading days only)
- database.py, main.py: register new model and router

Frontend:
- WatchlistTab.jsx: ChampionsSection at top with three champion cards
  (wheel / options / longterm), each showing ticker, grade, score bar,
  plain-English reason, and "Add to Watchlist" button
- api.js: champions.get() and champions.refresh()

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
…lling

champions_engine.py:
- Removed ~100 individual yfinance calls (IV rank + earnings + info per ticker)
  that were causing timeouts. Now uses a single bulk yf.download() only.
- Fixed MultiIndex column access for newer yfinance versions
- Only deletes old champions AFTER Claude successfully returns results
- Loosened pre-screen filters (RSI 20-80, volume 300k) to ensure enough survivors

routers/champions.py:
- Replaced BackgroundTasks with threading.Thread + in-process job tracker
- Added GET /api/champions/status to expose running state
- Fixed champion retrieval to not rely on exact datetime equality
- Returns scan_running flag so frontend knows when to stop polling

WatchlistTab.jsx:
- "Run Scan" now polls /api/champions every 8s until scan_running=false
- Shows live elapsed seconds during scan instead of static message
- Auto-updates champion cards when scan completes

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
rmalhotra25 and others added 29 commits May 15, 2026 00:18
Stock Lookup was fully superseded by DCF Valuation + Stock Triggers — removed
frontend tab and backend router registration (services remain as they are shared).

Day Scanner and Options Flow merged into a single '⚡ Market Pulse' tab:
- Three toolbar buttons: Day Plays / Options Flow / Run Both (parallel fetch)
- Market status bar now shows both SPY change and flow sentiment in one row
- Day trade plays section (PlayCard grid) above options flow section (FlowCard grid)
- Each section loads and errors independently — one failure doesn't block the other
- Updated data source note from 'yfinance options chains' to 'Polygon options chain'
  (reflecting the actual backend we already run)
- Default tab changed from daytrade to pulse

Net: 12 tabs → 10 tabs, ~800 lines deleted across StockLookupTab + DayTradeTab +
OptionsFlowTab (their component logic lives on in MarketPulseTab).

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
…→BUY

* Remove Stock Lookup tab; merge Day Scanner + Options Flow into Market Pulse

Stock Lookup was fully superseded by DCF Valuation + Stock Triggers — removed
frontend tab and backend router registration (services remain as they are shared).

Day Scanner and Options Flow merged into a single '⚡ Market Pulse' tab:
- Three toolbar buttons: Day Plays / Options Flow / Run Both (parallel fetch)
- Market status bar now shows both SPY change and flow sentiment in one row
- Day trade plays section (PlayCard grid) above options flow section (FlowCard grid)
- Each section loads and errors independently — one failure doesn't block the other
- Updated data source note from 'yfinance options chains' to 'Polygon options chain'
  (reflecting the actual backend we already run)
- Default tab changed from daytrade to pulse

Net: 12 tabs → 10 tabs, ~800 lines deleted across StockLookupTab + DayTradeTab +
OptionsFlowTab (their component logic lives on in MarketPulseTab).

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add trigger watchlist: auto-refresh on open, upgrade alerts for WATCH→BUY

- Save WATCH-rated stocks to localStorage trigger_watchlist with score/date
- Auto-refresh stale watchlist items on app open (different day or >6h)
- Prominent upgrade alerts when any item moves WATCH → SMALL BUY / STRONG BUY
- Per-upgrade dismissal, manual refresh button, loading state per item
- useRef guard prevents double-fire in React StrictMode

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
…e + Big Movers)

- data/dividend-achievers.json: Nasdaq Dividend Achievers seed list (~270 tickers,
  update quarterly per index reconstitution)
- dcf_service.analyze_quant(): mechanical DCF without Claude, safe for bulk scan loops
- advanced_scanner_service.py: two-stage funnel (Finnhub/Polygon pre-filter then quant
  trigger scoring); 24h disk cache; in-memory progress tracking; ThreadPoolExecutor
- advanced_scanner router: GET /dividend, /movers, /status/{type}; POST /refresh/{type}
- Scheduler: daily 6AM Eastern job runs both scans so results are fresh at market open
- GrowthScannerTab.jsx: mode toggle, 2s-poll progress bar, result cards with yield/
  growth/base/bear/MA/earnings pills, SPECULATIVE label on mover results, watchlist btn
- App.jsx: longterm tab replaced by scanner tab (backend longterm code retained)
- .gitignore: tighten data/ rule so static seed JSON files are tracked

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
- Cache moved from data/ (read-only on Render) to /tmp/ — was silently failing
  on every save, causing infinite re-scan loops
- Finnhub workers reduced 6→2 to avoid rate limiting on free/starter tier
- _get_fundamentals: drop get_company_profile call — halves API calls per stock
- Dividend scanner: remove minimum trigger score 5 — mature dividend stocks score
  3-4 on a growth-focused trigger system; show top 10 by score instead
- Dividend pre-filter: add per-criterion rejection log to diagnose future issues
- Movers scanner: remove P/S <= 8 filter (mutually exclusive with rev growth >= 20%;
  valuation handled by Monte Carlo × upside ranking instead)
- Movers scanner: lower revenue growth from 20% to 15%
- Movers scanner: volume filter skipped when Finnhub field missing (returns 0)
- _load_dividend_achievers: try multiple paths for resilience on Render deployment

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
…isation

- Dividend yield threshold: 3.5% → 2.0% (market yields have compressed; many
  quality dividend achievers now yield 2-3.5%)
- Revenue growth threshold: 5% → 2% (mature dividend companies typically grow 2-4%)
- Market cap threshold: $5B → $2B
- Gross margin: 20% → 15%
- Payout ratio: 65% → 70%
- Add decimal-vs-% normalisation for dividendYieldIndicatedAnnual and payoutRatioTTM
  (Finnhub API version differences can return either form)
- Surface rejection_stats in API response so empty-state shows why stocks failed
- Add /api/advanced-scanner/debug/{ticker} endpoint to inspect raw Finnhub fields
- Fix React '0' rendering bug: use != null instead of truthy check for survivor count
- Update empty state message to show pre-filter rejection breakdown

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
…try backoff

Root cause: ThreadPoolExecutor with 2 concurrent workers fired ~240 Finnhub
calls/minute — far exceeding the 60/min free tier. Rate-limited responses
returned {} → dividendYieldIndicatedAnnual missing → yield defaulted to 0 →
all stocks rejected on yield filter.

Fixes:
- _batch_fundamentals: replace thread pool with sequential loop + 1.2s sleep
  between calls (~50/min, safe for free tier)
- _get_fundamentals: add retry with exponential backoff (2s, 4s) for empty
  Finnhub responses before giving up
- Dividend pre-filter: skip yield/payout filters when Finnhub returned 0
  (missing data ≠ no dividend; Achievers list already guarantees dividend history)

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
* Fix Finnhub rate limiting causing 0 survivors in dividend scanner

Root cause: ThreadPoolExecutor with 2 concurrent workers fired ~240 Finnhub
calls/minute — far exceeding the 60/min free tier. Rate-limited responses
returned {} → dividendYieldIndicatedAnnual missing → yield defaulted to 0 →
all stocks rejected on yield filter.

Fixes:
- _batch_fundamentals: replace thread pool with sequential loop + 1.2s sleep
  between calls (~50/min, safe for free tier)
- _get_fundamentals: add retry with exponential backoff (2s, 4s) for empty
  Finnhub responses before giving up
- Dividend pre-filter: skip yield/payout filters when Finnhub returned 0
  (missing data ≠ no dividend; Achievers list already guarantees dividend history)

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add Top Rated Market Scanner with 3-tab Stock Triggers restructure

Scans S&P 500 + Nasdaq 100 (~560 tickers) using a two-stage funnel to find
stocks scoring 7/8 on the trigger system. Zero Claude calls in scan loop.

Backend:
- top_rated_scanner_service.py: Stage 1a Polygon batch snapshot (price/vol
  filter), Stage 1b sequential Finnhub fundamentals (24h disk cache at
  /tmp/tr_cache/), Stage 2 DCF+MC scoring with skip-MC logic (skip when
  non_MC_earned + 2 < 7), near-trigger message generation for 6/8 stocks
- top_rated_scanner.py: GET /results, GET /status, POST /refresh
- main.py: register top_rated_scanner router
- scheduler.py: daily 10 AM Eastern job (trading days only)
- data/sp500.json, data/nasdaq100.json: hardcoded universe (update quarterly)

Frontend:
- StockTriggersTab: restructured into 3 sub-tabs (Analysis / Top Rated /
  Watchlist), cross-tab navigation via CustomEvent, upgrade alerts and
  watchlist moved to Watchlist tab, TopRatedTab with scan progress bar,
  TopRatedCard (7/8) and NearTriggerCard (6/8) with "what it needs" message,
  scan efficiency stats panel

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
- _near_trigger_message: check mc_bd.earned < 2 (not == 0) so near-trigger
  stocks at 6/8 (mc_earned=1) correctly show "Needs MC >=85%" message
- _score_ticker: remove dead shareOutstanding fallback; snapshot_price from
  Polygon batch is always available and correct for current_price display

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
* Fix near_trigger_message and current_price in top rated scanner

- _near_trigger_message: check mc_bd.earned < 2 (not == 0) so near-trigger
  stocks at 6/8 (mc_earned=1) correctly show "Needs MC >=85%" message
- _score_ticker: remove dead shareOutstanding fallback; snapshot_price from
  Polygon batch is always available and correct for current_price display

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix Big Movers scanner: no results + slow performance

Two root causes:
1. Market cap ceiling $50B eliminated most high-growth companies that have
   scaled past that threshold (NVDA, META, MSFT, etc.) — removed ceiling
2. Revenue growth threshold 15% was too strict for S&P 500 — lowered to 10%
3. Polygon volume pre-filter was 200k which passed ~490/500 tickers, causing
   ~10 min Finnhub batch — raised to 1M to cut universe to ~150 before
   the 1.2s-sleep Finnhub loop

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Fix near_trigger_message and current_price in top rated scanner

- _near_trigger_message: check mc_bd.earned < 2 (not == 0) so near-trigger
  stocks at 6/8 (mc_earned=1) correctly show "Needs MC >=85%" message
- _score_ticker: remove dead shareOutstanding fallback; snapshot_price from
  Polygon batch is always available and correct for current_price display

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix Big Movers scanner: no results + slow performance

Two root causes:
1. Market cap ceiling $50B eliminated most high-growth companies that have
   scaled past that threshold (NVDA, META, MSFT, etc.) — removed ceiling
2. Revenue growth threshold 15% was too strict for S&P 500 — lowered to 10%
3. Polygon volume pre-filter was 200k which passed ~490/500 tickers, causing
   ~10 min Finnhub batch — raised to 1M to cut universe to ~150 before
   the 1.2s-sleep Finnhub loop

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix DCF valuation: gross margin data quality flag + debt-adjusted WACC

- _build_fundamentals: add gross_margin_note reliability flag (reported vs
  unreliable) based on Finnhub percentag scale check (> 0 and < 95)
- _claude_dcf_params: pass gross_margin_data_quality to Claude prompt so it
  ignores unreliable gross margin and derives FCF from operating/net margin
- _wacc_from_beta: debt-adjusted WACC for non-financial companies with D/E > 0.5
  using weighted avg of CAPM equity cost + 4% after-tax debt cost; financial
  sector excluded; clamped to [5.5%, 15%]
- analyze() and analyze_quant(): pass debt_equity_ratio + sector to _wacc_from_beta,
  include gross_margin_note in API response
- DcfTab + StockTriggersTab: show N/A ⚠ with amber border and hover tooltip
  when gross_margin_note != "reported"

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Fix near_trigger_message and current_price in top rated scanner

- _near_trigger_message: check mc_bd.earned < 2 (not == 0) so near-trigger
  stocks at 6/8 (mc_earned=1) correctly show "Needs MC >=85%" message
- _score_ticker: remove dead shareOutstanding fallback; snapshot_price from
  Polygon batch is always available and correct for current_price display

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix Big Movers scanner: no results + slow performance

Two root causes:
1. Market cap ceiling $50B eliminated most high-growth companies that have
   scaled past that threshold (NVDA, META, MSFT, etc.) — removed ceiling
2. Revenue growth threshold 15% was too strict for S&P 500 — lowered to 10%
3. Polygon volume pre-filter was 200k which passed ~490/500 tickers, causing
   ~10 min Finnhub batch — raised to 1M to cut universe to ~150 before
   the 1.2s-sleep Finnhub loop

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix DCF valuation: gross margin data quality flag + debt-adjusted WACC

- _build_fundamentals: add gross_margin_note reliability flag (reported vs
  unreliable) based on Finnhub percentag scale check (> 0 and < 95)
- _claude_dcf_params: pass gross_margin_data_quality to Claude prompt so it
  ignores unreliable gross margin and derives FCF from operating/net margin
- _wacc_from_beta: debt-adjusted WACC for non-financial companies with D/E > 0.5
  using weighted avg of CAPM equity cost + 4% after-tax debt cost; financial
  sector excluded; clamped to [5.5%, 15%]
- analyze() and analyze_quant(): pass debt_equity_ratio + sector to _wacc_from_beta,
  include gross_margin_note in API response
- DcfTab + StockTriggersTab: show N/A ⚠ with amber border and hover tooltip
  when gross_margin_note != "reported"

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Debug: widen D/E field lookup + log WACC inputs for NEE diagnosis

Try three Finnhub field names for debt/equity in _build_fundamentals:
debtToEquityAnnual, totalDebt/totalEquityAnnual, longTermDebt/totalEquityAnnual.
Log beta, debt_equity, sector, and computed WACC on every analyze() call so
the Render logs show exactly which field resolved and whether the debt
adjustment fired.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
Prints debt_equity resolved value, sector string, and all Finnhub metric
keys containing debt/equity/leverage to stdout (visible in Render logs).
Remove this block after confirming which key resolves correctly.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
Confirmed Finnhub keys from NEE output: totalDebt/totalEquityAnnual (primary)
and longTermDebt/equityAnnual (fallback). Removed debtToEquityAnnual (doesn't
exist) and longTermDebt/totalEquityAnnual (wrong name). Removed print block and
logger.info WACC debug statements.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
* Fix D/E field name + remove debug blocks

Confirmed Finnhub keys from NEE output: totalDebt/totalEquityAnnual (primary)
and longTermDebt/equityAnnual (fallback). Removed debtToEquityAnnual (doesn't
exist) and longTermDebt/totalEquityAnnual (wrong name). Removed print block and
logger.info WACC debug statements.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix MA trigger: re-anchor above_ma to Finnhub current price

Polygon daily closes lag intraday moves, causing above_ma to be True
even when the displayed Finnhub price is below the 50-day MA. In
analyze_trigger(), override above_ma using dcf current_price (Finnhub)
before scoring so the MA badge and points always match what the user sees.
Also cancels crossover_5d if the price has since fallen back below MA.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Fix D/E field name + remove debug blocks

Confirmed Finnhub keys from NEE output: totalDebt/totalEquityAnnual (primary)
and longTermDebt/equityAnnual (fallback). Removed debtToEquityAnnual (doesn't
exist) and longTermDebt/totalEquityAnnual (wrong name). Removed print block and
logger.info WACC debug statements.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix MA trigger: re-anchor above_ma to Finnhub current price

Polygon daily closes lag intraday moves, causing above_ma to be True
even when the displayed Finnhub price is below the 50-day MA. In
analyze_trigger(), override above_ma using dcf current_price (Finnhub)
before scoring so the MA badge and points always match what the user sees.
Also cancels crossover_5d if the price has since fallen back below MA.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add Put Selling Recommender to Stock Triggers analysis

Shows below trigger score breakdown when score >= 4, price >= $10,
and earnings not within 14 days. Entirely frontend — uses data already
returned by the trigger API.

- computePutRec(): strike selection (10% OTM, 10% below bear case, DCF p10
  floor — highest valid candidate), 3rd-Friday monthly expiry selection
  targeting 35 DTE with earnings hard rule, BS assignment probability and
  put premium estimate from beta-derived IV tier
- PutSellingCard: 5-section layout — trade summary, income metrics,
  probability progress bar, if-assigned return table, 4-item checklist
- Black-Scholes helpers: normCdf, bsAssignmentProb, bsPutPremium (pure JS,
  no external deps)
- Earnings-blocked state shown as warning instead of hiding card entirely

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
#35)

StockTriggersTab already renders the full DCF view (Monte Carlo, bull/base/bear
scenarios, Claude reasoning, market context, reverse DCF) below the trigger score
card. The standalone DCF tab called the same backend engine and showed duplicate
content, adding an extra API call with no new information.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
* Add Public.com fallback for wheel options chain when Polygon + yfinance fail

Polygon options data requires a paid Options plan; yfinance has been unreliable.
Public.com already has a working get_option_chain integration with real Greeks —
wire it in as a third fallback in get_put_tiers so the wheel custom-analyze
endpoint no longer errors on valid tickers like AAPL.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix wheel options chain failing when market is closed (bid/ask = 0)

When market is closed, Polygon snapshots have last_quote.bid/ask = 0
causing every contract to be filtered out at mid <= 0. Add fallbacks:
fair_market_value (Polygon's model price) then day.close (prior session).
Also upgrades Polygon path error log from debug to warning for visibility.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add diagnostic logging to Polygon options chain to surface filter failures

Logs a warning showing how many contracts were kept vs dropped (by DTE,
mid=0, delta=None, or exception) so Render logs reveal exactly where
AAPL options are being filtered out.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix Polygon client routing to api.polygon.io instead of api.massive.com

The massive library v2.7.0 (formerly polygon-api-client) now defaults its
base URL to api.massive.com, but the API key is for api.polygon.io.
This caused NOT_AUTHORIZED errors on all snapshot and aggs endpoints.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add dual-lens analysis: Valuation score + Paradigm score + combined recommendation

Every Stock Triggers analysis now produces two scores:
- Valuation (existing 0-8 trigger score, unchanged)
- Paradigm (new 0-5): revenue acceleration, platform lock-in, TAM expansion,
  winner-take-most dynamics, network effects/data moat

The three qualitative factors (lock-in, TAM, network effects) are answered
by Claude inside the existing DCF parameter call — zero additional API calls.
The two quantitative factors use Finnhub data already in the response.

Combined 3x3 recommendation matrix produces labels like:
  DEEP VALUE BUY, QUALITY BUY, EXCEPTIONAL OPPORTUNITY,
  PARADIGM HOLD, PARADIGM WATCH, VALUATION RISK — MONITOR, etc.

Frontend shows DualLensCard at the top of results and ParadigmBreakdown
after the trigger score breakdown. RecoChip replaced by the combined label.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix revenue_growth_annual_pct None→0 bug; bump DCF Claude max_tokens

Preserving None when Finnhub has no annual revenue growth data prevents
Factor 1 (revenue acceleration) from incorrectly scoring +1 for any
stock with >30% TTM growth compared to a fake 0% annual baseline.

Also bump _claude_dcf_params max_tokens 800→1000 to accommodate the
three new paradigm fields in the JSON response schema.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
…Polygon routing fix

* Add Public.com fallback for wheel options chain when Polygon + yfinance fail

Polygon options data requires a paid Options plan; yfinance has been unreliable.
Public.com already has a working get_option_chain integration with real Greeks —
wire it in as a third fallback in get_put_tiers so the wheel custom-analyze
endpoint no longer errors on valid tickers like AAPL.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix wheel options chain failing when market is closed (bid/ask = 0)

When market is closed, Polygon snapshots have last_quote.bid/ask = 0
causing every contract to be filtered out at mid <= 0. Add fallbacks:
fair_market_value (Polygon's model price) then day.close (prior session).
Also upgrades Polygon path error log from debug to warning for visibility.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add diagnostic logging to Polygon options chain to surface filter failures

Logs a warning showing how many contracts were kept vs dropped (by DTE,
mid=0, delta=None, or exception) so Render logs reveal exactly where
AAPL options are being filtered out.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix Polygon client routing to api.polygon.io instead of api.massive.com

The massive library v2.7.0 (formerly polygon-api-client) now defaults its
base URL to api.massive.com, but the API key is for api.polygon.io.
This caused NOT_AUTHORIZED errors on all snapshot and aggs endpoints.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add dual-lens analysis: Valuation score + Paradigm score + combined recommendation

Every Stock Triggers analysis now produces two scores:
- Valuation (existing 0-8 trigger score, unchanged)
- Paradigm (new 0-5): revenue acceleration, platform lock-in, TAM expansion,
  winner-take-most dynamics, network effects/data moat

The three qualitative factors (lock-in, TAM, network effects) are answered
by Claude inside the existing DCF parameter call — zero additional API calls.
The two quantitative factors use Finnhub data already in the response.

Combined 3x3 recommendation matrix produces labels like:
  DEEP VALUE BUY, QUALITY BUY, EXCEPTIONAL OPPORTUNITY,
  PARADIGM HOLD, PARADIGM WATCH, VALUATION RISK — MONITOR, etc.

Frontend shows DualLensCard at the top of results and ParadigmBreakdown
after the trigger score breakdown. RecoChip replaced by the combined label.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix revenue_growth_annual_pct None→0 bug; bump DCF Claude max_tokens

Preserving None when Finnhub has no annual revenue growth data prevents
Factor 1 (revenue acceleration) from incorrectly scoring +1 for any
stock with >30% TTM growth compared to a fake 0% annual baseline.

Also bump _claude_dcf_params max_tokens 800→1000 to accommodate the
three new paradigm fields in the JSON response schema.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix PutSellingCard: use real Polygon options data instead of Black-Scholes estimates

- Add GET /api/wheel/put-tiers/{ticker} endpoint — lightweight wrapper around StockDataService.get_put_tiers()
- Rewrite PutSellingCard to fetch real options chain via useEffect/useState
- Shows actual Polygon strike (holiday-aware expiry), real bid/ask/mid premium
- Displays delta, IV%, volume, OI from live chain instead of beta-estimated IV
- Removes all Black-Scholes helpers (normCdf, bsPutPremium, bsAssignmentProb, getThirdFriday, computePutRec)
- Uses delta_abs from Polygon for assignment probability instead of BS

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Add event risk discount, sector WACC floors, and Monte Carlo 97%+ cap

FIX 1 — Event risk discount:
- Claude DCF prompt now returns event_risk_discount (0.0–0.60) and event_risk_reason
- Applied multiplicatively to FCF margins: bull/base get full discount, bear gets 50%
  unless discount >= 0.50 (then all three get full discount)
- Re-clamps bear <= base <= bull ordering after asymmetric discounting
- If discount >= 0.40, WACC raised by +1.5pp (high event risk = higher required return)
- EventRiskBanner shown in trigger card whenever discount > 0 with color-coded severity

FIX 2 — Sector-specific WACC floors:
- Replaces universal 5.5% floor with Finnhub-string-keyed sector floors
- Utilities 5.5%, Real Estate 6.0%, Energy/Consumer Defensive 6.5%,
  Healthcare/default 7.0%, Industrials 7.5%, Tech/Software/Semiconductors 8.0%,
  Banks/Insurance/Financial Services 8.5%

FIX 3 — Monte Carlo 97%+ confidence cap:
- Probability display capped at '97%+' in both MonteCarloSection and summary pill
- Yellow warning badge: 'Model at maximum confidence — verify sector assumptions before acting'
- Score calculation unchanged (97%+ still awards +2 trigger points)

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix: pass sector to _wacc_from_beta in discovery engine; fix banner text

- discovery_engine._dcf_scenarios: pass sector=d.get('sector') to _wacc_from_beta
  for consistency (sector may be 'Unknown' at this stage but correct for future enrichment)
- EventRiskBanner: change 'ALL SCENARIOS' to 'DCF SCENARIOS' — bear gets half the
  discount (not full), so 'ALL' was misleading

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
…e, ⓘ tooltip

Monte Carlo section rewrite:
- Replace '97% chance stock is undervalued' with tiered signal labels:
  Very High (90%+) / High (75-90%) / Moderate (55-75%) / Mixed (40-55%) / Weak (<40%)
- New subtext: 'X% of modeled valuation scenarios exceed current price'
- ⓘ toggle tooltip: 'Based on Monte Carlo simulation of valuation assumptions
  (growth, margins, discount rates, event adjustments). This is not a probability
  of positive investment returns.'
- 97%+ cap and warning badge retained

WACC sensitivity table:
- Backend: _wacc_sensitivity() runs _run_dcf at 5.5%, 7.0%, 8.5% using same
  Claude-set scenarios, returns bear/base/bull price + upside% at each rate
- Added to both analyze() and analyze_quant() return dicts
- Frontend: table below MC histogram with highlighted row for current company WACC
  Shows how valuations shift across discount rate assumptions

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
* Institutional MC wording, tiered signal labels, WACC sensitivity table, ⓘ tooltip

Monte Carlo section rewrite:
- Replace '97% chance stock is undervalued' with tiered signal labels:
  Very High (90%+) / High (75-90%) / Moderate (55-75%) / Mixed (40-55%) / Weak (<40%)
- New subtext: 'X% of modeled valuation scenarios exceed current price'
- ⓘ toggle tooltip: 'Based on Monte Carlo simulation of valuation assumptions
  (growth, margins, discount rates, event adjustments). This is not a probability
  of positive investment returns.'
- 97%+ cap and warning badge retained

WACC sensitivity table:
- Backend: _wacc_sensitivity() runs _run_dcf at 5.5%, 7.0%, 8.5% using same
  Claude-set scenarios, returns bear/base/bull price + upside% at each rate
- Added to both analyze() and analyze_quant() return dicts
- Frontend: table below MC histogram with highlighted row for current company WACC
  Shows how valuations shift across discount rate assumptions

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix $0 premium display: round mid before filter and hide zero bid/ask

Two fixes for PutSellingCard showing $0 premiums:
1. backend/services/stock_data.py: Move mid rounding before the <= 0 filter check
   so deep-OTM options with mid=0.001 are correctly skipped rather than passing
   the filter then storing as 0.0 after rounding.
2. frontend/src/tabs/StockTriggersTab.jsx: Show '—' for Bid/Ask when both are 0
   (market closed, mid comes from fair_market_value/day-close fallback).

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
…42)

* Institutional MC wording, tiered signal labels, WACC sensitivity table, ⓘ tooltip

Monte Carlo section rewrite:
- Replace '97% chance stock is undervalued' with tiered signal labels:
  Very High (90%+) / High (75-90%) / Moderate (55-75%) / Mixed (40-55%) / Weak (<40%)
- New subtext: 'X% of modeled valuation scenarios exceed current price'
- ⓘ toggle tooltip: 'Based on Monte Carlo simulation of valuation assumptions
  (growth, margins, discount rates, event adjustments). This is not a probability
  of positive investment returns.'
- 97%+ cap and warning badge retained

WACC sensitivity table:
- Backend: _wacc_sensitivity() runs _run_dcf at 5.5%, 7.0%, 8.5% using same
  Claude-set scenarios, returns bear/base/bull price + upside% at each rate
- Added to both analyze() and analyze_quant() return dicts
- Frontend: table below MC histogram with highlighted row for current company WACC
  Shows how valuations shift across discount rate assumptions

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix $0 premium display: round mid before filter and hide zero bid/ask

Two fixes for PutSellingCard showing $0 premiums:
1. backend/services/stock_data.py: Move mid rounding before the <= 0 filter check
   so deep-OTM options with mid=0.001 are correctly skipped rather than passing
   the filter then storing as 0.0 after rounding.
2. frontend/src/tabs/StockTriggersTab.jsx: Show '—' for Bid/Ask when both are 0
   (market closed, mid comes from fair_market_value/day-close fallback).

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix dividend scanner zero results: P/E fallback when P/S unavailable

Banks, utilities, and REITs often don't have psTTM from Finnhub, causing
analyze_quant (and analyze) to raise ValueError for all 51 dividend
scanner survivors — returning zero results.

Add fallback: when P/S is missing, derive revenue_0 from P/E ÷ net margin
(mathematically equivalent). Only hard-fail if neither P/S nor P/E+net_margin
are available.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
_mechanial_scenarios was called in analyze_quant (used by all scanner
paths) and as the Claude fallback in analyze(), but was never defined
anywhere. Every _quant_trigger call threw NameError, returning None,
producing zero scanner results.

Implements bull/base/bear scenario generation using conservative sleeper
caps for mature stocks (rev_growth ≤ 20%) and compounder ceilings for
high-growth stocks (rev_growth > 20%), matching the logic in
discovery_engine._dcf_scenarios.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
* Define missing _mechanial_scenarios — fixes scanner zero results

_mechanial_scenarios was called in analyze_quant (used by all scanner
paths) and as the Claude fallback in analyze(), but was never defined
anywhere. Every _quant_trigger call threw NameError, returning None,
producing zero scanner results.

Implements bull/base/bear scenario generation using conservative sleeper
caps for mature stocks (rev_growth ≤ 20%) and compounder ceilings for
high-growth stocks (rev_growth > 20%), matching the logic in
discovery_engine._dcf_scenarios.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Lower Top Rated threshold from 7/8 to 6/8

7/8 requires both an MA crossover (2pts) AND MC ≥85% (2pts) simultaneously,
which is extremely rare mechanically. 6/8 is still a high bar — requires
most criteria — while actually producing results.

Also lowers skip-MC cutoff from non_mc_earned ≥ 5 to ≥ 4, and shifts
Near Trigger from 6/8 to 5/8 accordingly.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Define missing _mechanial_scenarios — fixes scanner zero results

_mechanial_scenarios was called in analyze_quant (used by all scanner
paths) and as the Claude fallback in analyze(), but was never defined
anywhere. Every _quant_trigger call threw NameError, returning None,
producing zero scanner results.

Implements bull/base/bear scenario generation using conservative sleeper
caps for mature stocks (rev_growth ≤ 20%) and compounder ceilings for
high-growth stocks (rev_growth > 20%), matching the logic in
discovery_engine._dcf_scenarios.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Lower Top Rated threshold from 7/8 to 6/8

7/8 requires both an MA crossover (2pts) AND MC ≥85% (2pts) simultaneously,
which is extremely rare mechanically. 6/8 is still a high bar — requires
most criteria — while actually producing results.

Also lowers skip-MC cutoff from non_mc_earned ≥ 5 to ≥ 4, and shifts
Near Trigger from 6/8 to 5/8 accordingly.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix Top Rated scanner: use prev_close when market is closed

When the market is closed, snap.day.close = 0, so all 483 tickers fail
the price >= 15 filter and nothing reaches Stage 1b. Fall back to
prev_close (always populated by Polygon) for both the price filter and
the displayed snapshot price.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
When market is closed, get_snapshots_batch returns {} — every ticker
then hits the no_data path and is dropped before Stage 1b runs.

Now: if Polygon returns empty, pass all tickers directly to Stage 1b
so Finnhub's market_cap/rev_growth/gross_margin/PE filter acts as the
sole quality gate. Stage 1a only applies when Polygon data is available.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

Co-authored-by: Claude <noreply@anthropic.com>
* Fix Stage 1a: bypass price/vol filter when Polygon returns no data

When market is closed, get_snapshots_batch returns {} — every ticker
then hits the no_data path and is dropped before Stage 1b runs.

Now: if Polygon returns empty, pass all tickers directly to Stage 1b
so Finnhub's market_cap/rev_growth/gross_margin/PE filter acts as the
sole quality gate. Stage 1a only applies when Polygon data is available.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add Momentum Alerts backend: scanner service, router, finnhub days param

- momentum_scanner_service.py: 3-signal convergence scanner (options flow,
  insider cluster buy, pre-earnings drift) with 6h cache, top-10 results
- routers/momentum.py: GET /alerts, POST /refresh, GET /status, GET /debug/{ticker}
- main.py: register momentum router
- finnhub_client.py: add optional days param to get_insider_sentiment (default 90)

Frontend tab coming in next commit.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Add Momentum Alerts convergence scanner tab

Three-signal convergence scanner for S&P 500 + Nasdaq 100:
- Signal 1: Unusual options flow (vol/OI >= 2x on short-dated contracts)
- Signal 2: Insider cluster buying (2+ buyers in 21-day window via Finnhub)
- Signal 3: Pre-earnings drift (price alpha vs SPY 14-42 days before earnings)

Threshold: 3/6 signals. Cached 6 hours. Top 10 results.

Backend: momentum_scanner_service.py + routers/momentum.py
Frontend: MomentumTab.jsx registered as ⚡ Momentum tab in App.jsx

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

* Fix two momentum scanner bugs from review

1. _score_pre_earnings_drift: sort earnings_list by date before iterating
   so nearest upcoming earnings is checked first, not arbitrary API order.
   Reject signal if nearest earnings is < 14 days away (imminent).

2. _score_options_flow: return 0 early when price=0.0 (market closed /
   Polygon returned no data). Prevents unfiltered options chain fetches
   that produce spurious flow scores across the whole universe.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch

---------

Co-authored-by: Claude <noreply@anthropic.com>
Universe was sorted alphabetically; with ~1.4s/ticker the full 500+
ticker scan takes ~11 min and gets killed by Render before completing,
so only A-stocks appeared in results.

Fix 1: shuffle universe before processing so partial runs give diverse
results across the alphabet rather than only A stocks.

Fix 2: when Polygon has no data (market closed), cap to 150 randomly-
sampled tickers instead of the full universe to keep runtime under 5 min.

https://claude.ai/code/session_0118U48tD3eV3sNjsCUvrmch
@rmalhotra25 rmalhotra25 force-pushed the fix-wheel-options-chain branch from 58b71f5 to a2d35bc Compare May 29, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants