Skip to content

vikbht/sensi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Options Sensi — options opportunity scanner

A web app that scans a configurable watchlist on an interval (default 5 min), computes options metrics per ticker, stores snapshots in SQLite, and flags unusual activity in a live signal feed.

Run it

Requires uv.

./run.sh

That syncs the environment from pyproject.toml/uv.lock, picks the first free port from 8000 upward (so it won't collide with anything else you're running), starts the server — which hosts both the API and the dashboard — and opens the browser. PORT=9000 ./run.sh forces a port; NO_OPEN=1 skips the browser; extra args pass through to uvicorn (e.g. ./run.sh --reload).

Add tickers and you're done. The first scan runs at boot and whenever you add a symbol; afterwards on the configured interval.

Real-time data via Schwab (optional)

By default the app uses free, ~15-min-delayed Yahoo data. With a Schwab brokerage account and an approved app on developer.schwab.com you get real-time quotes and chains with broker-computed IV/greeks and live bid/ask:

  1. cp .env.example .env and fill in SCHWAB_APP_KEY, SCHWAB_APP_SECRET, and your registered SCHWAB_CALLBACK_URL.
  2. uv run python -m app.schwab_auth — browser login; the token lands in schwab_token.json (gitignored). Schwab refresh tokens expire every 7 days (their human-in-the-loop security model — this can't be automated away). You won't have to remember it: a header pill counts down (amber ≤2d, red when expired) and clicking it re-runs the flow in your browser; a daily 16:50 ET check also fires a native macOS notification when expiry is near, even if the dashboard isn't open.
  3. Set "provider": "schwab" in config.json and restart.

Earnings dates and short interest still come from yfinance (Schwab's API doesn't serve them); if the token is missing the app falls back to yfinance so it never stops scanning.

The dashboard

  • Index gamma tape — a strip atop the Board tab with SPY/QQQ/IWM dealer-gamma levels on its own 60-second lane (front expirations incl. 0DTE): put wall / call wall on a rail with spot marked (the largest put/call open-interest strikes below/above spot, on the 1–5d chain where OI is real standing positioning), plus two gamma magnets split by horizon — a hollow diamond for the standing pin (1–5d) and a solid diamond for today's 0DTE pin. Their gap is a read: aligned = the book agrees, diverging = same-day flow leaning off the standing pin. A FADE/MOMO regime badge comes from the net GEX sign (pooled across horizons). An inline, phase-aware key under the header decodes every glyph (the close-ring and o/n-drift marks show only pre/post-market, when they're actually on the rail). Runs the options session (to 4:15 ET) live; in pre/post-market it freezes the levels (OI doesn't change overnight) and moves only the spot — a ghost dot marks the close, the solid dot the extended-hours price, so overnight drift toward a wall is the morning-prep read. Configurable via index_gamma_* keys.
  • Watchlist table — symbol, last scanned price with day change vs the previous close (green ▲ / red ▼), IV/HV (highlighted when elevated), put/call ratio, and a 24h signal-count badge (🔥 marks a confluence in the last 24h). IV/HV carries a small Nd horizon tag — the days to the nearest expiry the IV is read from — so different listing cadences (daily ETFs vs monthly small caps) are visible rather than silently conflated. An IV rank column shows where each name's ATM IV sits in its own trailing range (0–100, amber = rich / green = cheap) — comparable across names in a way IV/HV isn't. A Val column shows where the P/S multiple sits in its own band (IV-rank idea applied to valuation; accretes daily, valid between earnings — the fixed denominator makes the multiple band a price band). Click a row to expand full metrics (ATM IV, HV, IV rank + percentile + sparkline, valuation band with P/S/P/E/P/CF and a fair-value price envelope, volumes, peak gamma strike, skew, net GEX) and filter the feed to that name. Negative numbers render finance-style in parentheses.
  • Watchlist board — a Board tab (Dashboard / Board / Performance in the header) applies the index tape's gamma rail to every watchlist name: put wall / pin / call wall on a rail with the spot dot, plus that name's active alert badges (colored by severity) and its IV rank + signal count. A spatial "where's price relative to its walls, and what's firing" read to complement the sortable table — same watchlist, same scan cadence (no 60-second lane). Because single names aren't index ETFs, a chain too thin near spot to anchor trustworthy OI walls renders them muted grey (low-confidence, not firm); the board_wall_min_oi threshold sets the bar. Click a card to open that name filtered in the feed.
  • Signal feed — split into Today and Older sections, with a type filter (dropdown) that composes with the click-a-row symbol filter; a purple daily wrap card lands at 4:15 PM ET summarizing every name's day (POST /api/wrap regenerates it manually); entries younger than ~2 scan intervals get a brighter card and a new pill so the latest information stands out. Click a signal's kind label (e.g. GAMMA FLIP) to jump straight to its explanation in the help glossary.
  • Help glossary — the ? button explains every metric and signal in plain English, including how to read skew, net GEX, and pinning.
  • Settings — collapsible panel for the scan interval and the most-used thresholds; the header shows market open/closed and last/next scan times.
  • Event chart — from any expanded row, "⇱ Event chart" opens a full-width overlay: the price line (5-min snapshots, overnight gaps compressed at session dividers) with curated signal markers overlaid (warning+ severity and setup-class kinds), a 5d/15d/30d range selector, and a synced list showing each signal's forward return — the per-instance answer to "did the signal precede the move?"
  • Performance view — a Dashboard / Performance toggle in the header opens signal outcome tracking: By signal grades each detector by its forward move vs a baseline (trust / promising / weak / noise / collecting), scored by flavor — direction, magnitude, or stillness; By name shows which tickers produce predictive signals plus a name × signal edge heatmap. Each fired signal is stamped with its +1d / +5d return by a daily job; outcomes live in their own table (longer retention than the 5-day feed) so +5d can mature. Early verdicts lean on +1d and upgrade to +5d as data accrues.

Implemented signals

Signal What it detects Why it matters
iv_premium ATM IV ≥ 1.25× 20-day HV Options pricing in a bigger move than the stock has realized — possible event/catalyst, or rich premium to sell
iv_spike ATM IV rising ≥10% vs the average of recent scans Someone is bidding up vol right now — the message says whether the stock was flat (often precedes news), falling (reactive hedging), or rallying (upside chase) alongside
unusual_volume Contract volume on pace for ≥ 2× open interest (min 500 lots at day pace) New positioning today, not closing of old positions
put_call_ratio P/C volume ratio > 2.0 or < 0.4 One-sided directional flow
gamma_build Net GEX up ≥25% scan-over-scan (same sign, ≥ $5M floor) Dealer hedging pressure building — message says whether it stabilizes or destabilizes the tape, and names the driver strikes
gamma_flip Net GEX changed sign since last scan (≥ $5M floor) Regime change: hedging switches between dampening moves (positive) and amplifying them (negative)
gamma_pin Peak gamma strike within 2% of spot Pin/magnet risk into expiry
skew_shift OTM put IV minus OTM call IV moved ≥4 vol pts vs baseline Crash protection getting bid, or upside being chased
setup_read IV rank at an extreme (≤30 cheap / ≥70 rich) + a directional flow lean Synthesizes vol level and flow into one structural read (cheap vol + bullish flow → well-priced upside optionality, etc.)
cc_write IV rank ≥70 AND IV/HV ≥1.25 AND earnings ≥21d out AND short float <10% AND net GEX >0 The covered-call checklist as one signal — fires only on clean premium-selling setups, with the peak-γ strike as a strike-selection hint
strike_concentration ≥40% of the day's option premium piled into one contract (with a $ floor) A single strike drawing that much flow = position-building; the most-active strike is also always in the detail panel + daily wrap
confluence ≥3 distinct signal kinds on one symbol within 4h Independent detectors agreeing — the strongest pattern here; flagged 🔥 in the watchlist
squeeze_setup ≥10% short float + ≥3 of: call-heavy flow, fresh OTM call buying, inverted skew, price+IV rising Short-cover and dealer-hedging feedback loops aligned; warns when the naive GEX sign is likely inverted
vol_compression 10d HV ≤ 0.6× 20d HV with IV not pricing expansion Coiled spring — compressed ranges resolve violently, direction unknown
daily_wrap Generated 4:15 PM ET each market day One card per day: every name's move, IV change, top signal, and what stayed elevated at the close

Gamma signals also carry OpEx context: in the days before monthly expiration they note that OI is concentrated (effects intensify); on the first session after, gamma build/flip are downgraded to info — the change is mostly Friday's expired OI mechanically rolling off, not positioning.

Signals carry catalyst context: earnings within 7 days appends "event premium likely", while IV signals with a known earnings date more than 14 days out get flagged "vol bid without an obvious catalyst" — the interesting case. Unknown dates get no tag (absence of data isn't absence of a catalyst). Windows configurable; earnings dates are fetched once per symbol per day and shown in the detail panel.

All thresholds are editable in the UI (the most-used ones) or via PUT /api/config / config.json (all of them).

Roadmap

The live roadmap is the issue tracker — features are enhancement issues grouped into milestones, bugs carry root cause and resolution, and commits close them with Fixes/Closes #N. The themes below feed that backlog.

Flow-quality signals (need tick/trade-level data — Polygon, CBOE DataShop, or UW):

  • Sweeps vs blocks — multi-exchange sweeps at the ask are aggressive, informed-looking buying; single-exchange blocks at mid are often institutional hedges.
  • Premium-weighted flow — $5M of OTM calls in a quiet mid-cap matters more than $5M in SPY. Rank by premium / average daily premium.
  • Repeated hits — the same strike/expiry getting bought across multiple scans is far more meaningful than one print.

Vol-surface signals (need a fuller chain history):

  • IV rank / IV percentile — where today's IV sits in its own 52-week range; 90th percentile IV means rich premium, 10th means cheap optionality. (The snapshots table already accumulates the history you need for this.)
  • Term-structure inversion — front-month IV above back-month = event premium; quantify the kink around a specific expiry to infer the event date the market expects.
  • Implied earnings move vs history — straddle price around earnings vs the stock's average realized earnings move.

Positioning signals:

  • OI delta day-over-day — volume tells you today's trading; change in OI tells you what stuck. Big new OI at a strike is a footprint.
  • Vanna/charm exposure — beyond gamma: how dealer deltas shift as IV falls or time passes (drives post-event drift and OpEx flows).
  • Max pain drift — strike minimizing option-holder value, and whether spot gravitates toward it into expiry.

Cross-signals:

  • IV up + price flat — vol bid without a move is one of the cleanest "someone knows something" patterns.
  • Catalyst overlay — join signals against an earnings/FDA/macro calendar; an IV spike with no scheduled catalyst is the interesting one.
  • Sector-relative IV — a name's IV jumping while its sector ETF's IV is flat isolates idiosyncratic anticipation.

Architecture

app/
  main.py                 FastAPI routes + APScheduler (interval rescheduled live)
  scanner.py              fetch → compute snapshot → persist → run detectors
  market_clock.py         ET session math: market hours, day-pace projection
  config.py               config.json load/save with defaults
  db.py                   SQLite: watchlist, snapshots, signals
  analytics/
    signals.py            one detector per signal kind
    black_scholes.py      greeks (Yahoo supplies IV, no solver needed)
    historical_vol.py     annualized close-to-close HV
  providers/
    base.py               OptionsDataProvider interface
    yfinance_provider.py  free, ~15-min delayed data
static/                   vanilla-JS dashboard (no build step)

Caveats

  • With provider: schwab, quotes/chains are real-time with broker-computed IV and live bid/ask (the delayed-data caveats below apply to the default yfinance provider only).
  • Yahoo data is ~15 min delayed and per-contract IV can be stale on illiquid strikes. For real edge, swap in Polygon/Tradier/ORATS via providers/base.py — the rest of the app doesn't change.
  • Scheduled scans only run 9:30–16:00 ET Mon–Fri (market_hours_only in config). After hours, the manual Scan now button always works, and adding a symbol triggers an immediate scan of just that symbol so its row fills in right away. Exchange holidays are not modeled — holiday scans see stale data, and the signal cooldown absorbs the repeats.
  • Volume-based signals are projected to full-day pace, and comparison signals (IV spike, gamma build, skew shift) baseline only against the current session, so they stay quiet for the first couple of scans each morning.
  • Snapshots are kept 30 days (they feed future detectors like IV rank) and signals 5 days (alerts get stale fast), both configurable; purged after each sweep, which also drops snapshots for symbols no longer on the watchlist. The DB runs in WAL mode for concurrent scanner/API access.
  • The DB is backed up daily (16:45 ET and on boot) to backups/, keeping the newest 14 (backup_keep) — the snapshots/outcomes corpus is the app's research memory, so it never has just one copy.
  • Only one process scans at a time: instances share a lease in the DB, and the newest start (or a manual Scan now) takes ownership. If a second instance is left running against the same DB it goes passive (its UI shows a ⚠ passive badge) rather than writing conflicting data.
  • Each scan pulls every expiration in the dte_mindte_max window (default 2–21 days, capped at max_expirations), so all names are measured over a comparable forward horizon rather than a fixed count that meant ~2 days for daily-listing ETFs but months for monthly names. The sweep runs symbols in parallel (scan_workers), so it stays fast as the watchlist and the window grow; sweep duration shows next to "last scan". Widening the window or the watchlist still raises Yahoo rate-limit pressure (~1 + expirations-in-window requests per symbol), so keep both sensible.
  • This flags anomalies, not trades. Everything here is a starting point for research, not financial advice.

About

Watchlist scanner for unusual options activity — IV/HV, gamma, skew, squeeze setups, unusual flow — surfaced in a live dashboard with synthesized setup reads, IV rank, and signal outcome tracking. FastAPI + SQLite, free Yahoo data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors