Risk·Developer / quant··7 min read

Monitor portfolio risk with the unusual put options API

A 40-line Python script that polls the OptionsBell API for unusual puts on the stocks you own and pings Slack the moment institutional hedging shows up.

Put waves rarely show up on a daily chart in time. The big institutional hedges that precede a drawdown print on the options tape hours - sometimes days - before the underlying breaks down. If you hold a concentrated equity portfolio, polling the unusual options activity API every few minutes for puts on your tickers turns the OptionsBell feed into a portfolio-grade risk monitor.

This guide walks through a working script you can drop into a Raspberry Pi, a free Render worker or a tiny VPS. The full code is under 50 lines and ships with no dependencies you wouldn't already have.

What we are building

A scheduled job that does three things on every tick: pull the latest unusual options activity for your holdings, keep only the puts that cross a conviction threshold, and post a single Slack message per new contract so you never see the same alert twice.

  • Polls /api/v1/options-flow/unusual every 5 minutes using the ?since= parameter so each call only returns contracts seen after the last poll.
  • Filters server-side for puts on your portfolio symbols, with Vol/OI ≥ 4 and premium ≥ $250k.
  • Deduplicates by contract_id in a local SQLite file so a restart cannot re-fire old alerts.

Step 1 - get an API key

Subscribe to OptionsBell Pro, open Settings, and create an API key. Store it as the OPTIONSBELL_API_KEY environment variable. Every request authenticates via the X-API-Key header; rate limits are returned in X-RateLimit-Remaining, so a 5-minute cadence is comfortably inside the 2,000/day Pro budget.

Step 2 - define your portfolio and risk thresholds

The script reads two pieces of state: the symbols you want monitored, and the Slack webhook to notify. Keep them in a tiny config file so you can change holdings without touching code.

# config.py
PORTFOLIO = ["AAPL", "MSFT", "NVDA", "META", "AMD", "TSM"]

# Conviction floor: vol/oi >= 4 AND premium >= $250k.
# Tighten these to cut noise, loosen to see more.
MIN_VOLOI = 4.0
MIN_PREMIUM = 250_000

SLACK_WEBHOOK = "https://hooks.slack.com/services/T0000/B0000/XXXX"
POLL_SECONDS = 300  # 5 minutes

Step 3 - the polling loop

The endpoint accepts a since ISO timestamp so we only ever pull new contracts. We store the last seen timestamp in a SQLite file and ratchet it forward on every successful response.

# monitor.py
import os, time, json, sqlite3, urllib.request, urllib.parse
from datetime import datetime, timezone
from config import (PORTFOLIO, MIN_VOLOI, MIN_PREMIUM, SLACK_WEBHOOK, POLL_SECONDS)

API_BASE = "https://optionsbell.com/api/v1/options-flow/unusual"
API_KEY  = os.environ["OPTIONSBELL_API_KEY"]
DB       = sqlite3.connect("state.db")
DB.execute("CREATE TABLE IF NOT EXISTS seen (contract_id TEXT PRIMARY KEY)")
DB.execute("CREATE TABLE IF NOT EXISTS cursor (k TEXT PRIMARY KEY, v TEXT)")

def get_cursor() -> str:
    row = DB.execute("SELECT v FROM cursor WHERE k='since'").fetchone()
    return row[0] if row else (datetime.now(timezone.utc).isoformat())

def set_cursor(iso: str) -> None:
    DB.execute("INSERT OR REPLACE INTO cursor(k, v) VALUES('since', ?)", (iso,))
    DB.commit()

def fetch_unusual_puts(since: str) -> list[dict]:
    qs = urllib.parse.urlencode({
        "symbols": ",".join(PORTFOLIO),
        "type": "p",
        "min_voloi": MIN_VOLOI,
        "min_premium": MIN_PREMIUM,
        "since": since,
        "limit": 200,
    })
    req = urllib.request.Request(f"{API_BASE}?{qs}", headers={"X-API-Key": API_KEY})
    with urllib.request.urlopen(req, timeout=15) as r:
        return json.loads(r.read())["data"]

def already_seen(contract_id: str) -> bool:
    return DB.execute(
        "SELECT 1 FROM seen WHERE contract_id=?", (contract_id,)
    ).fetchone() is not None

def remember(contract_id: str) -> None:
    DB.execute("INSERT OR IGNORE INTO seen(contract_id) VALUES(?)", (contract_id,))
    DB.commit()

def alert(c: dict) -> None:
    msg = (
        f":rotating_light: *Unusual PUT on {c['symbol']}* "
        f"- strike ${c['strike']}, exp {c['expiry']}\n"
        f"Vol/OI {c['voloi']:.1f}x, premium ${c['premium']:,.0f}, IV {c['iv']}%"
    )
    body = json.dumps({"text": msg}).encode()
    urllib.request.urlopen(urllib.request.Request(
        SLACK_WEBHOOK, data=body, headers={"Content-Type": "application/json"}
    ), timeout=10).read()

while True:
    since = get_cursor()
    try:
        for c in fetch_unusual_puts(since):
            if already_seen(c["contract_id"]):
                continue
            alert(c)
            remember(c["contract_id"])
        set_cursor(datetime.now(timezone.utc).isoformat())
    except Exception as e:
        print(f"[poll error] {e}")
    time.sleep(POLL_SECONDS)

Step 4 - what a real alert looks like

When the loop fires on something like a 30-day put on a name you own, the Slack message that lands in your channel is enough to act on without leaving the chat:

⚠️ Unusual PUT on NVDA - strike $140, exp Jul 19. Vol/OI 7.4x, premium $1,840,000, IV 52%.

From there, decide: trim the long, roll your existing protective puts forward, or buy a small downside spread to define risk through the same window the institutional hedger picked.

Step 5 - run it as a service

On Linux, a one-line systemd unit keeps the script alive across reboots. On free hosting, a single render.com worker with this file is enough. There is no database to manage and no queue to babysit.

# /etc/systemd/system/optionsbell-monitor.service
[Unit]
Description=OptionsBell portfolio risk monitor
After=network-online.target

[Service]
Environment=OPTIONSBELL_API_KEY=ob_live_xxxxxxxxxxxxxxxx
ExecStart=/usr/bin/python3 /opt/optionsbell/monitor.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Tuning the noise floor

The two knobs that matter are min_voloi and min_premium. Start at 4x and $250k - this typically gets you 1-3 alerts per day across a 10-stock book. If you trim to 6x and $500k you will only see the loudest prints, which is usually what an institutional hedger looks like. Loosening below 3x lets event-driven, single-strike noise back in.

Where to take this next

  • Swap Slack for SMS via Twilio if you treat puts as an off-hours emergency signal.
  • Add a second filter for calls on shorts you carry - the same script with type=c flags institutional buying against your downside thesis.
  • Persist every alert to Postgres and chart the rolling hit rate (how often a flagged put preceded a 3% drop within 10 sessions) so you can defend the workflow with data.

The unusual options activity API was designed for exactly this kind of always-on monitoring. One endpoint, sensible filters, and a stable contract identifier - everything else is composition.