Fully Autonomous Options Income Bot on TradeStation API v3 with Deep Market Intelligence
The v3.0 blueprint relied on IV Rank, EMA/RSI, and VIX as the only market signals. v3.1 adds five additional intelligence layers that transform the bot from a mechanical rules engine into a context-aware system that understands why the market is moving, not just that it is moving.
| Track | Strategy | Symbols | DTE | Key Rules |
|---|---|---|---|---|
| A — Wheel | Cash-Secured Put + Covered Call | AAPL, MSFT, NVDA, AMZN, GOOGL | 30 DTE | 0.30 delta, 50% profit target, skip 7d before earnings |
| B — Strangle | Short Strangle (SPY/QQQ only) | SPY, QQQ | 45 DTE | 0.30 delta, 3x stop, close at 21 DTE (non-negotiable) |
| C — 0DTE | Iron Butterfly | SPX | 0 DTE | 10:15 AM entry, noon hard exit, Mon/Wed/Fri only |
| Fallback | Iron Condor / Vertical Spread | Individual stocks | 30-45 DTE | Defined-risk when IV Rank qualifies but Wheel is full |
Each symbol receives a composite Intelligence Score (0-100) before every trade decision. The bot will not trade without consulting the intelligence service first.
| Intelligence Layer | What It Detects | Data Source |
|---|---|---|
| News Sentiment | Positive/negative tone in headlines via FinBERT NLP | RSS feeds (Reuters, Finviz, SEC EDGAR, Fed Reserve, Yahoo Finance) |
| Options Flow | Unusual block orders, dark pool prints, sweep activity, skew | TradeStation API option chains |
| Macro Calendar | FOMC, CPI, NFP, GDP, Fed speeches, earnings dates | FRED API, BLS/BEA RSS, Federal Reserve RSS |
| Historical Backtester | Whether current parameters worked on last 90 days | PostgreSQL trade history + TradeStation bars |
| Market Breadth | VIX regime, put/call ratio, sector rotation, advance/decline | TradeStation API + market breadth data |
| Score | Label | Bot Behavior |
|---|---|---|
| 85-100 | STRONG GO | All tracks active. 1.2x position sizing. |
| 65-84 | GO | Normal operation. All tracks at standard sizing. |
| 45-64 | CAUTIOUS | 0.5x size. Skip 0DTE. Require IV Rank > 40. |
| 25-44 | HOLD | No new entries. Monitor and manage existing only. |
| 0-24 | DANGER | Close positions near expiry. Send CRITICAL alert. |
| Layer | Weight |
|---|---|
| News Sentiment | 20% |
| Options Flow | 25% |
| Macro Calendar | 20% |
| Market Breadth | 20% |
| Backtester | 15% |
┌──────────────────────────────────────────┐
│ Nginx (HTTPS) │
│ TLS · Rate Limiting · CSP Headers │
└────┬──────────┬──────────┬───────────────┘
│ │ │
┌──────────▼──┐ ┌───▼───┐ ┌──▼──────────────┐
│ Trading Bot │ │ Front- │ │ Intelligence │
│ Node.js │ │ end │ │ Python │
│ :3001 │ │ :80 │ │ :5050/:8050 │
└──────┬──────┘ └───────┘ └────────┬────────┘
│ │
┌──────▼──────────────────────────────▼──────┐
│ PostgreSQL 16 │
│ Shared Database (tsbot) │
└────────────────────────────────────────────┘
│ │ │
┌──────▼──┐ ┌──────▼──┐ ┌──────▼──┐
│ OpenBao │ │Promethe-│ │ Grafana │
│ Secrets │ │ us │ │ :3000 │
│ :8200 │ │ :9091 │ └─────────┘
└──────────┘ └─────────┘
| Service | Port | Purpose |
|---|---|---|
| nginx | 80, 443 | HTTPS termination, JWT gate, rate limiting |
| trading-bot | 3001, 9090 | Node.js bot API + Prometheus metrics |
| intelligence | 5050, 8050 | Python FastAPI intelligence + Dash dashboard |
| frontend | 80 (internal) | React SPA dashboard |
| grafana | 3000 | Metrics dashboards |
| prometheus | 9091 | Metrics scraper (90-day retention) |
| postgres | 5432 (internal) | PostgreSQL 16 database |
| openbao | 8200 (internal) | Secrets management (HashiCorp Vault fork) |
| mailserver | 25, 587 | SMTP for email alerts |
| watchtower | — | Auto-update trading-bot from Docker Hub |
- Runtime: Node.js 22 LTS, TypeScript 5.x (strict mode)
- API: Express 4.x
- ORM: Drizzle ORM
- Auth: JWT (jsonwebtoken) + Zod validation
- Scheduling: node-cron (America/New_York timezone)
- Metrics: prom-client (Prometheus)
- Secrets: OpenBao (HashiCorp Vault fork) + Docker Secrets fallback
- Alerts: Nodemailer (12 event types, 4 severity levels)
- Runtime: Python 3.12
- API: FastAPI + Uvicorn
- NLP: FinBERT (ProsusAI/finbert) via Hugging Face Transformers + PyTorch
- Dashboard: Plotly Dash with Bootstrap
- Reports: WeasyPrint (PDF) + Jinja2 templates
- ORM: SQLAlchemy 2.x (async with asyncpg)
- Scheduling: APScheduler
- Framework: React 18 + TypeScript
- Build: Vite
- Styling: TailwindCSS (dark trading-terminal theme)
- State: Zustand (JWT in memory only — not localStorage)
- Data: TanStack Query (polling at 15-60s intervals)
- Charts: Recharts
All interactions use https://api.tradestation.com/v3 (live) or https://sim-api.tradestation.com/v3 (simulation).
| Endpoint | Method | Rate Limit | Purpose |
|---|---|---|---|
/marketdata/quotes/{symbols} |
GET | 240/min | Real-time quotes |
/marketdata/barcharts/{symbol} |
GET | 120/min | OHLCV historical bars |
/marketdata/symbollists/optionexpirations/{symbol} |
GET | 60/min | Option expiration dates |
/marketdata/options/chains/{symbol} |
GET | 60/min | Full option chain with Greeks |
/accounts/{accountID}/orders |
POST | Monitored | Place orders (single or multi-leg) |
/accounts/{accountID}/positions |
GET | 240/min | Open positions |
/accounts/{accountID}/balances |
GET | 240/min | Buying power, equity, margin |
| Symbol | API Format | Notes |
|---|---|---|
| SPX Index | $SPX.X |
Dollar sign prefix, .X suffix |
| SPX Options | SPX |
No prefix for option chains |
| VIX Index | $VIX.X |
Dollar sign prefix, .X suffix |
| OCC Option | SPY 250321C00500000 |
Symbol + space + YYMMDD + C/P + 8-digit strike |
Every trading cycle passes through a 9-layer portfolio risk check before any trade routing occurs:
- Intelligence Score Gate — Block if score < 45 (HOLD/DANGER)
- Daily Loss Limit — Block if daily loss exceeds 5% of equity
- Macro Block — Block if intelligence reports macro event within window
- Max Drawdown — Block if portfolio drawdown exceeds 10%
- VIX Ceiling — Block if VIX > 35
- Open Position Limit — Block if max concurrent positions reached
- Per-Track Allocation — Block if track exceeds allocation limit (Wheel 35%, Strangle 45%, 0DTE 15%)
- Portfolio Delta — Block if absolute portfolio delta > 0.50
- Buying Power Reserve — Ensure minimum reserve after trade
| Limit | Value | Rationale |
|---|---|---|
| 21 DTE universal close | All multi-day positions | Gamma explosion protection |
| 0DTE noon hard exit | 12:00 PM ET | Scheduled at startup, never skipped |
| Strangles on SPY/QQQ only | Never individual stocks | Index-only for undefined risk |
| Order retry safety | Never retry possibly-filled | Prevents double fills |
| Event | Level | Throttle |
|---|---|---|
| Bot started | INFO | 1/hr |
| Bot shutdown | WARNING | 1/hr |
| Daily loss limit hit | CRITICAL | 1/hr |
| Max drawdown exceeded | CRITICAL | 1/hr |
| VIX > 35 — halted | WARNING | 1/hr |
| Strangle stop 3x | CRITICAL | Per position |
| TS API auth failing | CRITICAL | 1/hr |
| OpenBao unreachable | CRITICAL | 1/hr |
| Token renewal failed | CRITICAL | 1/hr |
| 0DTE noon force-close | INFO | Per position |
| Health check degraded | DOWNTIME | 1/hr |
| End-of-day P&L summary | INFO | Unlimited |
- Linux server (Ubuntu 22.04+ or Debian 12+ recommended)
- Docker Engine 24+ and Docker Compose v2
- A registered domain with DNS pointing to your server
- TLS certificate (Let's Encrypt or similar)
- TradeStation API v3 credentials (Client ID, Client Secret, Refresh Token)
git clone https://github.com/YOUR_USERNAME/ts-options-bot.git
cd ts-options-botEvery secret must be created via Docker Secrets. The bot reads credentials from /run/secrets/* at runtime. Never put secrets in environment variables or config files.
# Initialize Docker Swarm (required for Docker Secrets)
docker swarm init
# TradeStation API credentials
echo "YOUR_TS_CLIENT_ID" | docker secret create ts_client_id -
echo "YOUR_TS_CLIENT_SECRET" | docker secret create ts_client_secret -
echo "YOUR_TS_REFRESH_TOKEN" | docker secret create ts_refresh_token -
# Database password (use a strong random password)
openssl rand -base64 32 | docker secret create db_password -
# JWT secret for dashboard authentication
openssl rand -hex 32 | docker secret create jwt_secret -
# OpenBao token (generated during OpenBao init)
echo "YOUR_OPENBAO_BOT_TOKEN" | docker secret create openbao_token -
# SMTP password for email alerts
echo "YOUR_SMTP_PASSWORD" | docker secret create smtp_password -Place your TLS certificate and private key in the Nginx certs directory:
mkdir -p docker/nginx/certs
cp /path/to/fullchain.pem docker/nginx/certs/fullchain.pem
cp /path/to/privkey.pem docker/nginx/certs/privkey.pem
chmod 600 docker/nginx/certs/privkey.pemUsing Let's Encrypt (recommended):
# Install certbot
sudo apt install certbot
# Generate certificate
sudo certbot certonly --standalone -d yourdomain.com
# Copy certs
sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem docker/nginx/certs/
sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem docker/nginx/certs/Edit docker-compose.yml and update the non-secret environment variables:
trading-bot:
environment:
- TS_SIM=true # KEEP true until all paper trading phases pass
- DASHBOARD_URL=https://yourdomain.com
- SMTP_HOST=mailserver # Or smtp.gmail.com for Gmail
- SMTP_PORT=587
- ALERT_EMAIL=your@email.com
mailserver:
domainname: yourdomain.com # Your actual domain
intelligence:
environment:
- INTELLIGENCE_API_KEY=<generate-a-strong-random-key>Generate the intelligence API key:
openssl rand -hex 32# Start PostgreSQL first
docker compose up -d postgres
# Wait for it to be healthy
docker compose exec postgres pg_isready -U botuser -d tsbot
# Run Drizzle migrations to create tables
docker compose run --rm trading-bot npm run db:push# Start OpenBao
docker compose up -d openbao
# Initialize the vault
docker compose exec openbao bao operator init \
-key-shares=5 \
-key-threshold=3
# IMPORTANT: Save the unseal keys and root token securely!
# You need 3 of 5 keys to unseal after every restart.
# Unseal (repeat 3 times with different keys)
docker compose exec openbao bao operator unseal <KEY_1>
docker compose exec openbao bao operator unseal <KEY_2>
docker compose exec openbao bao operator unseal <KEY_3>
# Enable KV secrets engine
docker compose exec openbao bao secrets enable -path=trading-bot kv-v2
# Store the TradeStation credentials in OpenBao
docker compose exec openbao bao kv put trading-bot/credentials \
ts_client_id=YOUR_CLIENT_ID \
ts_client_secret=YOUR_CLIENT_SECRET \
ts_refresh_token=YOUR_REFRESH_TOKEN
# Create a policy for the bot
docker compose exec openbao bao policy write trading-bot - <<'EOF'
path "trading-bot/data/*" {
capabilities = ["read", "create", "update", "delete"]
}
EOF
# Create a token for the bot service
docker compose exec openbao bao token create \
-policy=trading-bot \
-ttl=24h \
-renewable=true
# → Use this token as the openbao_token Docker Secret# Build all images
docker compose build
# Start the full stack
docker compose up -d
# Verify all services are healthy
docker compose ps
# Check trading bot logs
docker compose logs -f trading-bot# Health check
curl https://yourdomain.com/health
# Expected response:
# { "status": "healthy", "uptime": 120, "checks": { "db": true, "openbao": true, "intelligence": true } }
# Test email alerts
docker compose exec trading-bot node dist/scripts/test-alert.js| URL | Service |
|---|---|
https://yourdomain.com/ |
React Trading Dashboard |
https://yourdomain.com/intel/ |
Intelligence Plotly Dash Dashboard |
https://yourdomain.com/grafana/ |
Grafana Metrics Dashboard |
https://yourdomain.com/health |
Health Check (public) |
Dashboard Login: The password is the first 16 characters of your JWT secret.
Do not skip phases. Each phase validates a critical subsystem.
TS_SIM=true
- All unit tests pass (
npm test— 65+ tests) - Frontend builds successfully (
cd frontend && npm run build) - Docker Compose starts without errors
- Health check returns healthy for all dependencies
- Email alerts work (
npm run test:alertsfires all 4 levels) - Intelligence score returns for SPY, QQQ, AAPL
- 30+ 0DTE butterfly fills logged
- Noon hard exit fires every Mon/Wed/Fri at 12:00 PM ET
- P&L tracking matches TradeStation account
- Wing width and entry time are sensible
- 20+ strangles opened and closed
- 21 DTE rule closes them automatically
- 3x stop loss triggers correctly
- Only SPY/QQQ (never individual stocks)
- Full CSP → assignment → CC → called away cycle on 2+ symbols
- Wheel state machine transitions correctly
- Cost basis tracking is accurate
- Earnings skip (7-day window) works
TS_SIM=false # LIVE TRADING — read carefully before changing
Before flipping to live:
- All Phase 2 paper trading criteria met
- Reviewed every trade in the audit log
- 1-contract maximum enforced
- All email alerts verified
- Monitoring dashboards showing correct data
- Kill switch tested (bot stops immediately)
- Daily backup procedure established
# To go live, change in docker-compose.yml:
# TS_SIM=false
# Then rebuild and restart:
docker compose build trading-bot
docker compose up -d trading-bot- Increase position size only after positive months
- Add new Wheel symbols one at a time
- Review intelligence score accuracy monthly
- Never increase risk during drawdowns
| Metric | Type | Description |
|---|---|---|
bot_trades_total |
Counter | Total trades by track, symbol, result |
bot_portfolio_value |
Gauge | Current portfolio equity |
bot_daily_pnl |
Gauge | Today's realized P&L |
bot_iv_rank |
Gauge | Current IV Rank per symbol |
bot_portfolio_delta |
Gauge | Net portfolio delta |
bot_intel_score |
Gauge | Intelligence score per symbol |
bot_api_latency |
Histogram | TradeStation API response time |
Pre-provisioned dashboard at /grafana/ shows:
- P&L by track (stacked bar chart)
- Win rate by strategy
- Portfolio Greeks (delta, theta, vega)
- Open positions count
- Intelligence score timeline
- API latency percentiles
Four PostgreSQL tables managed by Drizzle ORM:
positions — All open/closed positions across all tracks
wheel_states — Wheel strategy state machine per symbol
daily_pnl — Daily P&L aggregation by track
audit_log — Full audit trail of every bot event
# Generate migration from schema changes
npm run db:generate
# Apply migrations
npm run db:migrate
# Push schema directly (development only)
npm run db:push# Prerequisites
# - Node.js 22+, Python 3.12+, PostgreSQL 16
# Bot
npm install
npm run dev # Runs with tsx (hot reload)
# Intelligence service
cd intelligence
pip install -r requirements.txt
uvicorn app.main:app --port 5050 --reload
# Frontend
cd frontend
npm install
npm run dev # Vite dev server at localhost:5173 (proxies to :3001)# TypeScript unit tests
npm test
# TypeScript type checking
npm run typecheck
# Frontend type checking
cd frontend && npx tsc --noEmit
# Python (from intelligence/)
cd intelligence
pytest -v --asyncio-mode=autoGitHub Actions runs on every PR:
- TypeScript type check + unit tests (65+ tests)
- Frontend build verification
- Python lint + tests
- Docker Compose config validation
On git tag v*.*.*:
- Multi-stage Docker build for all 3 services (bot, intelligence, frontend)
- Push to Docker Hub (private repositories)
- Watchtower auto-updates production (labeled services only)
| Image | Source | Size |
|---|---|---|
youruser/ts-bot |
Dockerfile (Node.js multi-stage) |
~50 MB |
youruser/ts-intel |
intelligence/Dockerfile (Python + FinBERT) |
~3-4 GB |
youruser/ts-frontend |
frontend/Dockerfile (Vite → nginx:alpine) |
~50 MB |
For production, you deploy pre-built images from Docker Hub — no source code needed on the server.
-
Create 3 private repositories on hub.docker.com:
youruser/ts-botyouruser/ts-intelyouruser/ts-frontend
-
Set GitHub Secrets (Settings → Secrets → Actions):
DOCKERHUB_USERNAME— your Docker Hub usernameDOCKERHUB_TOKEN— Docker Hub Access Token (not password)
Generate a token at: hub.docker.com/settings/security
# Tag and push — GitHub Actions builds all 3 images automatically
git tag v1.0.0
git push origin v1.0.0GitHub Actions will:
- Build
ts-bot,ts-intel,ts-frontendimages - Push each with
:v1.0.0and:latesttags - Watchtower (on production server) detects new
:latestwithin 5 minutes
# 1. Install Docker Engine + Compose
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 2. Initialize Docker Swarm (required for Docker Secrets)
docker swarm init
# 3. Login to Docker Hub (for private repos)
docker login -u youruser
# 4. Create Docker Secrets (see Step 2 in Production Deployment Guide above)
# 5. Copy only the compose files + config to the server (NO source code needed)
# Required files:
# docker-compose.yml
# docker-compose.prod.yml
# docker/nginx/nginx.conf
# docker/nginx/certs/fullchain.pem
# docker/nginx/certs/privkey.pem
# docker/openbao/config.hcl
# docker/prometheus/prometheus.yml
# docker/grafana/provisioning/
# 6. Create .env with your Docker Hub user
cat > .env << 'EOF'
DOCKERHUB_USER=youruser
TAG=v1.0.0
INTELLIGENCE_API_KEY=your_generated_key_here
EOF
# 7. Pull and start all services
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# 8. Verify
docker compose ps
curl https://yourdomain.com/healthAutomatic (Watchtower):
After pushing a new git tag, Watchtower polls Docker Hub every 5 minutes and auto-updates labeled services (trading-bot, intelligence, frontend).
Manual:
# Pull specific version
export TAG=v1.1.0
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d# Roll back to previous version
export TAG=v1.0.0
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dThe docker-compose.prod.yml file overrides the base docker-compose.yml:
- Removes
build:directives (images pulled from Docker Hub, not built locally) - Sets
TS_SIM=falsefor live trading - All other config (secrets, networks, depends_on) inherited from base
# Development (builds locally):
docker compose up -d
# Production (pulls from Docker Hub):
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
| Job | Schedule | Description |
|---|---|---|
| Main Cycle | */15 9-15 * * 1-5 |
Every 15 min during market hours |
| 0DTE Entry | 15 10 * * 1,3,5 |
10:15 AM Mon/Wed/Fri |
| 0DTE Monitor | */5 10,11 * * 1,3,5 |
Every 5 min 10-11 AM |
| 0DTE Noon Exit | 0 12 * * 1,3,5 |
12:00 PM HARD EXIT |
| Greeks Refresh | */10 9-15 * * 1-5 |
Every 10 min market hours |
| EOD Report | 5 16 * * 1-5 |
4:05 PM daily |
| TS Token Refresh | */10 * * * * |
Every 10 min (tokens expire in 20 min) |
| OpenBao Renewal | 0 0 * * * |
Midnight daily |
- Secrets: Docker Secrets (
/run/secrets/*) with OpenBao (HashiCorp Vault) as primary store - Auth: JWT with timing-safe password comparison, Zod-validated login requests
- Network: Two Docker networks —
botnet(internal) andwebnet(public-facing via Nginx only) - Headers: CSP, HSTS, X-Frame-Options DENY, X-Content-Type-Options nosniff
- Rate Limiting: 5 req/min on login, 10 req/sec on API (via Nginx)
- Container: Non-root user (
botuser) in all application containers - Frontend: JWT stored in memory only (not localStorage) — re-login required on page refresh
- Intelligence API: API key validated with
secrets.compare_digest()(timing-safe)
Ensure all Docker Secrets are created. Run docker secret ls to verify all 7 secrets exist.
Access tokens expire every 20 minutes. The bot refreshes them proactively via cron. If you see persistent 401s, your refresh token may have expired (365-day lifetime). Generate a new one via TradeStation developer portal.
The FinBERT model takes 30-60 seconds to load on first request. If it persists, check docker compose logs intelligence for memory issues (FinBERT requires ~2GB RAM).
The noon exit is scheduled via node-cron with America/New_York timezone. Verify your server timezone: timedatectl. The cron fires at exactly 12:00 PM ET regardless of server timezone.
Ensure PostgreSQL is healthy: docker compose exec postgres pg_isready -U botuser -d tsbot. Check that db_password Docker Secret matches what PostgreSQL was initialized with.
OpenBao seals on every container restart. You must unseal with 3 of 5 keys: docker compose exec openbao bao operator unseal <KEY>.
Private — All rights reserved.
Built with TypeScript, Python, and the TradeStation API v3. Intelligence Engine v3.1.