An autonomous options-wheel bot that runs entirely off your own Robinhood account — one login, one small server, no paid data feeds.
It sells cash-secured puts on stocks you'd be happy to own, manages them to a profit target, and — if assigned — sells covered calls against the shares. The same disciplined rules every day, with no emotion. This is the community edition: all market data comes from the same Robinhood connection it trades through, so you don't need an Alpaca subscription, a TradingView Pro plan, or anything else.
- This bot places real trades with real money. There is no "demo money" once it's armed live.
- Selling puts means you can be assigned — i.e. forced to buy 100 shares per contract at the strike. Only ever put names on your watchlist that you are genuinely willing to own, in sizes you can afford.
- Options carry real, sometimes total, loss risk, including sharp overnight/gap losses. A wheel is not a guaranteed-income machine.
- This is not financial advice, not a solicitation, and comes with no warranty of any kind. You run it entirely at your own risk.
- Not affiliated with, endorsed by, or supported by Robinhood. It uses Robinhood's agentic connection; you are responsible for complying with Robinhood's Terms of Service. Robinhood can change or revoke that access at any time.
- Start in paper mode. Start tiny. Watch it for weeks before you trust it.
If you are not comfortable reading logs, using SSH, and losing every dollar in the account, do not run this.
- What it does
- How it works
- What you need
- Setup overview
- Step 1 — Robinhood account + one-time login
- Step 2 — Spin up a Hostinger VPS
- Step 3 — Install & deploy
- Step 4 — First run, paper → live
- Configuration reference
- Accessing your dashboard securely
- Optional integrations (Alpaca / TradingView)
- Operating the bot
- Safety & risk controls
- Troubleshooting
- Disclaimer & license
The options wheel, automated end to end:
- Sell a cash-secured put on a watchlist name — collect a premium for agreeing to buy the stock at a lower ("strike") price.
- If it expires worthless (stock stayed above the strike) → keep the premium, repeat.
- If you're assigned (stock fell below the strike) → you now own 100 shares per contract at that strike.
- Sell covered calls against those shares — collect more premium — until the shares are called away at/above your cost basis. Then back to step 1.
Every scan, the bot screens each watchlist name's live option chain, filters for the right delta / days-to-expiry / liquidity / yield, ranks by how rich the premium is (IV rank), sizes the position so no single bet dominates, and places the order. It then monitors open positions and buys them back at a profit target, rolls when it makes sense, or lets them ride to assignment.
It is deliberately a slow, patient strategy: 7–14 day expirations, held for days, only on names you'd own.
| Component | Job |
|---|---|
| Scanner | Every few minutes during market hours: screen the watchlist, rank candidates, size, and open new CSPs / CCs. |
| Monitor | Continuously price open positions; take profit, alert on short-DTE, track excursions. |
| Executor | Places and confirms orders through Robinhood; never leaves an order in an unknown state. |
| Reconcile | Keeps the bot's ledger in sync with what Robinhood actually reports (assignments, expiries, fills). |
| Risk | Position/sizing caps, entry gates, a loss circuit breaker, and an instant kill switch. |
| Dashboard | A password-protected, phone-first web page: health, positions, weekly and all-time P&L, the "why" behind every decision, a daily Setups read with measured edge and per-name risk profiles, a Weekly tactical brief archive, and a Tuning tab to change config live from your phone. |
All of it runs as one small Docker container that keeps its state (a local SQLite database + your login token) on a persistent volume. Market data — option chains, quotes, greeks, IV, open interest, and daily price bars — is pulled from Robinhood's own connection (market_data.provider: robinhood), so there are no separate data subscriptions.
- A Robinhood account with options trading enabled and agentic access set up (a primary account + the agentic sub-account — see Step 1). Level 2+ options approval is required to sell cash-secured puts.
- A VPS to run it 24/7. This guide uses Hostinger (Step 2). The bot is lightweight — the smallest KVM plan is plenty.
- A desktop computer for the one-time Robinhood login (the login flow needs a browser + your phone).
- Basic comfort with SSH and the command line.
- ~30–45 minutes for first-time setup.
You do not need: an Alpaca account, a TradingView subscription, a paid market-data feed, or any cloud provider beyond the VPS.
Desktop: run the one-time Robinhood login → produces data/rh_oauth.json
Hostinger: create a VPS (Ubuntu) → install Docker
VPS: clone this repo → add your secrets + config → upload rh_oauth.json → docker compose up
Browser: open the dashboard → verify health → (when ready) arm live
-
Have a Robinhood account with options enabled. You need approval to sell cash-secured puts (typically options Level 2). Set this up in the Robinhood app first.
-
Enable agentic access. Robinhood's automated ("agentic") trading uses a dedicated connection. Follow Robinhood's in-app flow to enable it and create the agentic sub-account you want the bot to trade. Only that account trades — the bot cannot place orders on your other accounts.
-
Run the one-time login on your desktop (not the VPS — the flow opens a browser and pairs with your phone):
# on your desktop, with this repo cloned and Python 3.13 + deps installed: pip install -e ".[all]" python -m agentic.tools.rh_login
This walks through Robinhood's OAuth flow and writes
data/rh_oauth.json(your access + refresh tokens). You'll upload that file to the VPS in Step 3. The bot auto-refreshes the token from then on — you won't have to log in again unless the refresh chain is broken.
Guard
rh_oauth.jsonlike a password. It authorizes trading on your account. Never commit it, never share it, never paste it anywhere. It is git-ignored by default.
💡 Get a Hostinger VPS here → https://www.hostinger.com?REFERRALCODE=LRBKTHIELNOA — using this link supports the project at no extra cost to you.
⚡ Skip the manual install: when creating the VPS, paste this into Hostinger's "Post-install script" field and the server boots with Docker + ThetaBot already set up:
#!/bin/bash curl -fsSL https://raw.githubusercontent.com/SaltyMeat23/ThetaBot/main/scripts/bootstrap.sh | bash(Requires the repo to be public. Then just SSH in, edit
.env+config.yaml, upload your token, and start it.)
-
Go to Hostinger → VPS Hosting and choose a plan. KVM 1 (1 vCPU / 4 GB RAM / ~50 GB) is more than enough — the bot uses a tiny fraction of it. KVM 2 gives comfortable headroom if you want it.
-
Operating system: choose Ubuntu 24.04 (or 22.04). If Hostinger offers an "Ubuntu with Docker" template, pick it — Docker comes pre-installed and you can skip part of Step 3.
-
Set a strong root password (or, better, add your SSH key) when prompted.
-
Choose a datacenter near you (or near US markets — latency isn't critical for this slow strategy).
-
Once it provisions, note the VPS's public IP address from the Hostinger control panel (hPanel → VPS → your server).
-
SSH in from your desktop:
ssh root@YOUR_VPS_IP
On a fresh Ubuntu VPS (or if you used the Hostinger post-install script above, this is already done):
curl -fsSL https://raw.githubusercontent.com/SaltyMeat23/ThetaBot/main/scripts/bootstrap.sh | bashThat installs Docker, clones ThetaBot, and creates your
.env+config.yaml. Then jump to editing them below (steps 3 & 5) — you can skip the manual Docker install and clone.
Manual steps (or to understand what the script did): On the VPS:
-
Install Docker (skip if your template already has it):
curl -fsSL https://get.docker.com | sh docker --version # confirm it's installed
-
Clone this repo and enter it:
git clone https://github.com/SaltyMeat23/ThetaBot.git cd ThetaBot -
Create your secrets file from the template:
cp .env.example .env nano .env
Fill in — at minimum:
# Dashboard login (pick your own — you'll use these to open the web UI) DASHBOARD_USER=admin DASHBOARD_PASSWORD=change-this-to-something-strong # A random token that protects the control endpoints (pause/resume/etc.) CONTROL_TOKEN=another-long-random-string # (Optional) desktop push alerts via Pushover — leave blank to skip PUSHOVER_TOKEN= PUSHOVER_USER= # (Optional) AI trade-review commentary via Claude — leave blank to skip ANTHROPIC_API_KEY=
You do not set any Alpaca or TradingView keys in this edition.
-
Upload your Robinhood token. The easy path is a bind-mounted
data/folder. Create it and copy the file you made in Step 1:mkdir -p data # from your DESKTOP, in a separate terminal: scp data/rh_oauth.json root@YOUR_VPS_IP:/root/ThetaBot/data/rh_oauth.jsonThen make sure
docker-compose.ymlmounts your hostdata/folder (this edition ships that way):volumes: - ./data:/app/data
-
Set your strategy config:
cp config.example.yaml config.yaml nano config.yaml
The most important line to check first:
mode: paper # START HERE. Change to "live" only when you're ready. entry: enabled: true watchlist: [F, SOFI, ...] # names you are happy to own market_data: robinhood # default: all data from your Robinhood login (no Alpaca)
(Full options in the Configuration reference.)
-
Launch:
docker compose up -d --build docker compose logs -f # watch it boot; Ctrl-C to stop watching
- Open the dashboard:
http://YOUR_VPS_IP:8000— log in with theDASHBOARD_USER/DASHBOARD_PASSWORDyou set. - Confirm health: the top banner should show the broker connected and not degraded. Check
http://YOUR_VPS_IP:8000/healthreturnsok. - Let it run in paper mode for a while. Watch it screen, "trade," and manage positions with no real money. Read the decision log — make sure its choices make sense to you.
- Add HTTPS (recommended before going live). Exposing a password over plain
http://is risky. The simplest option is to put Caddy in front for automatic HTTPS with a domain you point at the VPS — see the wiki/docs/. At minimum, restrict port 8000 with a firewall to your own IP. - Go live only when you're ready: set
mode: liveinconfig.yaml, start with a small account and a short watchlist, thendocker compose up -dto apply. The dashboard will showlive_armed: true.
The dashboard runs on your server at port 8000. You do not need a domain or a website. Pick the access method that fits — simplest to most polished:
http://YOUR_VPS_IP:8000. Instant, but it's plain HTTP (password unencrypted) and open to the internet. Fine for a first peek — do not leave it exposed. At minimum, firewall port 8000 to your own IP.
Nothing to install, no open ports. From your computer:
ssh -L 8000:localhost:8000 root@YOUR_VPS_IPThen open http://localhost:8000 while that SSH session is running. The dashboard is never reachable from the public internet — the most private option, and it costs nothing.
A persistent, encrypted URL you can hit from anywhere (including your phone), with no inbound ports open on your VPS. It's built in — just enable it:
- At dash.cloudflare.com → Zero Trust → Networks → Tunnels → Create a tunnel (free).
- Add a public hostname (a subdomain on a domain you've added to Cloudflare) and route it to the service
http://agentic:8000. - Put the tunnel token in
.env:CLOUDFLARE_TUNNEL_TOKEN=... - Start with the tunnel profile:
docker compose --profile tunnel up -d
Now it's at https://yourname.example.com, port 8000 still closed to the world. For a real login wall, add a Cloudflare Access policy (email/Google) to that hostname — highly recommended.
Prefer to self-host HTTPS like a classic web app? Point a domain at the VPS and run Caddy in front for automatic Let's Encrypt certs. More moving parts than the tunnel; only if you specifically want it.
Bottom line: most people should use the SSH tunnel for occasional checks, or a Cloudflare Tunnel for a secure always-on URL. A public website is optional.
config.yaml (hot-reloadable via the dashboard). Key sections:
mode: paper | live
market_data: robinhood # robinhood | alpaca | paper (see "Optional integrations")
entry:
enabled: true
watchlist: [F, SOFI, T, ...] # ONLY names you'd own
feed: opra # ignored for the robinhood provider
prefer_iv_rank: true # sell where premium is richest vs the name's own history
earnings_gate: true # never hold a short put through earnings
criteria: # the CSP screen
delta_min: 0.10
delta_max: 0.20 # ~how likely you are to be assigned; 0.10-0.20 measured best (docs/backtests.md)
dte_min: 7
dte_max: 14
min_annualized_yield: 0.52 # ~1%/week floor on collateral; lower it for more (thinner) trades
min_open_interest: 100
min_volume: 10
max_spread_pct: 0.10
max_pct_below_sma200: 0.20 # skip broken downtrends
require_strike_below_support: true
min_strike_expected_moves: null # OFF. Require the strike >= N option-implied expected-moves OTM
# (scales the cushion to each name's own volatility).
min_iv_rv_ratio: 1.3 # Only sell when IV is >= 1.3x the name's 20-day realized vol: the
# variance-risk-premium edge. Measured: +1.90%/trade vs +1.20% off,
# fewer assignments, ~1/3 fewer entries. null = off.
cc_criteria: # the covered-call screen (post-assignment)
delta_min: 0.20
delta_max: 0.30
min_annualized_yield: 0.20
cc_below_basis_after_days: null # OFF. Assignment clock: after N days under water, allow calls
cc_otm_band: [0.05, 0.10] # BELOW basis inside this band above spot (capital turns over)
sizing:
target_positions: 20 # spread capital across ~N names (scale-invariant)
max_position_size_pct: 0.50 # per-name backstop cap
total_bp_utilization_target: 0.80
buying_power_reserve_pct: 0.15
max_pct_of_oi: 0.10 # never take >10% of a strike's open interest
max_pct_per_underlying: null # OFF by default. Set (e.g. 0.15) to allow MORE THAN ONE CSP per
# name -- laddered strikes/expiries up to this % of account in that
# name; still no averaging-in over later days. null = one per name.
per_ticker: # optional: override any `criteria` field for one name (merged over it)
# NVDA: { delta_max: 0.22, min_iv_rank: 40 }
watchlist_tiers: {} # names to PROPOSE once one contract fits under the per-name cap
# KO: { min_collateral: 7000, per_ticker: { min_annualized_yield: 0.20 } }
macro: # market-regime read (SPY vs its 200-day, VIX term structure)
skip_confirmed_downtrend: false # opt-in: pause NEW puts after 5 straight SPY closes under the 200-day
tax_reserve: # weekly gains sweep -- see "Operating the bot"
enabled: false
pct: 0.20 # of the week's NET realized gains (losses carry forward)
symbol: SGOV # any stock/ETF: T-bills for taxes, or an index fund to reinvest
dry_run: true # first cycle: log + ledger only
risk: # loss circuit breaker (freezes NEW entries; never force-closes)
loss_breaker_enabled: true
lookback_days: 7
max_realized_loss_pct: 0.10 # halt new entries if realized losses over the window exceed 10% of account
max_consecutive_losses: 4
rules: # position management
- name: profit-trail
params: { profit_pct: 0.8, trailing: true, trail_gap: 0.2 } # take profit ~80% of maxTune the yield floor and delta band to your own risk tolerance. Higher min_annualized_yield = fewer, richer, higher-IV trades; lower = more, thinner ones. The defaults above are the ones that survived a real-option-print backtest under both fair and worst-case fills; docs/backtests.md lists every lever tested, what held up, and what did not.
By default, ThetaBot needs nothing but your Robinhood login — market data and trading both ride the same connection. But if you already pay for better data or extra signals, you can plug them in. Neither is required.
Robinhood's data is complete and works well for this slow strategy, but you can use Alpaca instead (e.g. you already have it, or want a separate data source):
- Create an account at alpaca.markets and generate API keys.
- Real-time options data (OPRA) requires Alpaca's paid market-data subscription. The free tier is delayed ("indicative") and is not safe for live entry — only use it for paper testing. Subscribe to their options/Algo data plan for live trading.
- Add the keys to
.env:ALPACA_API_KEY=your_key ALPACA_API_SECRET=your_secret
- Switch the provider (and feed) in
config.yaml:market_data: alpaca entry: feed: opra # real-time (paid). Use "indicative" only for paper/testing.
docker compose up -dto apply. Everything else is identical.
ThetaBot can optionally gate entries on ADX (trend strength) and Bollinger %B (position in the band), fed from TradingView alerts. This is pure enrichment — the bot runs fine without it, and these gates are off by default.
Requires a TradingView plan that supports webhook alerts (Pro+ or higher).
- Choose a webhook token and add it to
.env:TRADINGVIEW_WEBHOOK_TOKEN=some_long_random_string - In TradingView, create alerts on your watchlist symbols with the Webhook URL:
and an alert message that posts the indicator values as JSON (the symbol plus its
https://YOUR_HOST/webhook/tradingview?token=some_long_random_stringadxandbb_percent_b). The webhook handler atPOST /webhook/tradingviewingests those into each name's context. - Turn the gates on in
config.yaml:entry: criteria: min_adx: 20 # skip weak / choppy trends min_bb_percent_b: 20 # skip price pinned to the lower band
If no fresh alert has arrived for a symbol, these gates simply don't apply (fail-open) — they never block a trade for lack of data. To turn them back off, remove the two lines (or set them to null).
- Dashboard (
/) -- tabbed and mobile-friendly:- Overview: health, open positions, realized/unrealized P&L (this week and all-time), win rate, the reason behind each close, the gains-sweep reserve (held, swept to date, this week's net, next sweep) and Ready to add (tier names the account can now afford).
- Watchlist: the daily read on every name you watch. Setups today labels each name from completed daily bars -- washout (oversold), quiet base, support test, coiling (volatility squeeze), breakout / breakdown (confirmed on volume) -- plus a live column for today's unfinished bar breaking a range or support right now. Labels carry a put-seller bias (favorable / avoid / neutral). Below it, Which setups have edge here scores every fire on what actually happened 5 and 10 bars later on your names (hit rate, average move, worst 10-day dip), and Per-name risk profile measures, for each name, how often a put a given expected-move cushion below spot was touched or finished in the money over the last year, and suggests a wider cushion for names that run hot. Every watchlist add is profiled automatically. Endpoints:
/api/setups,/api/setups/accuracy,/api/risk-profile. - Brief: the Weekly tactical brief (
/api/brief) -- market backdrop, this week's catalysts, setups firing, per-name expected-move cushion (strike distance in option-implied moves), earnings-inside-the-contract flags, open-position management (roll window / moneyness), and assignment capacity (collateral if every short put were assigned vs. buying power). Every generated brief is saved: the tab opens on the latest one, a picker lists earlier ones (/api/briefs,/api/briefs/{id}), and Generate new builds a fresh one. Descriptive only -- not advice. - Tuning: change config live from your phone -- no SSH, no restart. Multi-CSP cap, the entry gates, the market regime block (confirmed-downtrend skip), the gains sweep (on/off, percent, symbol, day/time, dry run), setup gates (
avoid_setups/require_setups, a prefer setups ranking tilt, a one-tap put-seller preset that avoids the breakout family, and apply suggested cushions from the risk profile), the delta band, and per-ticker overrides, each schema-validated and persisted to a writable overlay (safety-critical keys stay locked). All setup gates are off by default and only ever tighten. - On a phone the tabs sit in a bottom bar within thumb reach, every card folds on a tap (remembered per device), and wide tables keep their first column pinned while you scroll sideways.
- Gains sweep / tax reserve (
tax_reserve, off by default): every week at the time you set, the bot takes a share of that week's net realized gains (losses carry forward, so a red week is netted against the next green one) and buys that many dollars of a symbol you choose with a small regular-hours market order -- SGOV (T-bills) to set money aside for taxes, or an index fund to reinvest premium. The holding is walled off: it never counts as trading capital, is never written against, and the bot never sells it. One ledger row per week (GET /api/tax-reserve), so a restart can't double-buy. Run it withdry_run: truefor a cycle first; the ledger shows what it would have bought. - Ready to add (
entry.watchlist_tiers, optional): list quality names with the collateral one contract needs; once the account can hold one under the per-name cap, they show up on the Overview and Tuning tabs with a one-tap Add that carries theirper_tickeroverrides. The bot never adds names on its own (GET /api/tiers). - Assignment clock (
cc_below_basis_after_days, opt-in, per ticker): by default calls are never sold below cost basis, which on volatile names can park capital for months. With a clock set, shares still under water that many days after assignment may be written against below basis inside a 5-10% OTM band, so the capital turns over. The holdings table shows each name's days held and whether below-basis calls are allowed. - Open interest:
GET /api/option-oi?symbol=SMRreturns the full call+put chain with OI, volume, IV and greeks (Robinhood-sourced; Alpaca's snapshots carry no OI). - Pause / resume: the kill switch halts all new orders instantly; use it any time you want to stop trading without touching positions.
- Loss circuit breaker: trips automatically on a losing streak (see below) and shows in
/api/ops— it freezes new entries but keeps managing what's open. - Logs:
docker compose logs -fon the VPS. - Updating:
git pull && docker compose up -d --build. - Backups: your whole state is
data/(the SQLite DB +rh_oauth.json). Back that folder up.
The bot is built to survive, not to gamble:
- Only sells names you list — you curate the universe of what it can be assigned.
- Position + concentration caps so no single trade dominates.
- Entry gates — skips earnings, broken downtrends, illiquid contracts, and (opt-in) thin cushion or thin variance-risk-premium (the expected-move and IV/realized gates). An opt-in confirmed-downtrend skip pauses new puts market-wide once SPY has spent five straight sessions under its 200-day.
- Roll safety — a roll that closes the old put but cannot open the new one is audited as an error and pushed to you as "Roll INCOMPLETE", never silently dropped.
- Loss circuit breaker — freezes new entries after a bad realized run (default: −10% of account in 7 days, or 4 straight losers). It never force-liquidates — it stops digging, it doesn't panic-sell.
- Instant kill switch — halts on broker errors or on your command.
- No hidden leverage — cash-secured puts are fully collateralized.
None of this removes market risk. A sharp gap down on a held name is still a real loss.
| Symptom | Check |
|---|---|
| Dashboard won't load | docker compose ps (is it running?), firewall allows port 8000, correct IP. |
| Banner shows "degraded"/broker down | rh_oauth.json present in data/? Token still valid? Re-run rh_login on desktop and re-upload. |
| No trades firing | Normal if nothing clears your criteria. Lower min_annualized_yield or check the decision log for the gate that's rejecting. |
| "paused" and won't trade | Kill switch or loss breaker engaged — check /api/ops for the reason; resume from the dashboard. |
| Token stopped refreshing | Re-run the desktop rh_login and re-upload rh_oauth.json. |
This software is provided "as is", without warranty of any kind, express or implied. The authors are not liable for any losses, damages, or account actions arising from its use. It is not financial, investment, tax, or legal advice, and nothing here is a recommendation to buy or sell any security.
Trading options involves substantial risk of loss. Cash-secured puts can result in forced purchase of shares; covered calls can cap gains and result in shares being sold. Only trade with money you can afford to lose.
This project is not affiliated with, endorsed by, or sponsored by Robinhood Markets, Inc. "Robinhood" is a trademark of its owner. You are solely responsible for complying with Robinhood's Terms of Service and all applicable laws and regulations in your jurisdiction.
By running this software you accept full responsibility for its behavior and any resulting trades.
License: MIT (see LICENSE).