What this is. The options-flow trading agent I run, and the infrastructure it stands on: orchestration through the Claude Agent SDK, a deterministic 14-check safety gate, kill switch and circuit breakers, broker-as-source-of-truth reconciliation, and VIX-based market-regime context. The model plans and scores; deterministic code decides whether a trade is allowed to happen, and nothing in the prompt can override it.
Status. Research code that runs for real (paper or live, per
.env). Not a framework, not a package, not advice — interfaces change when I need them to.Related. reward_hacking_research — a documented reward hack in a self-optimizing trading-research agent (a second exploit found 18 minutes after the first was patched), with the full trajectory and re-runnable ablations. The safety-gate design here is the same instinct applied to execution instead of evaluation: trust the model's reasoning, never its authority.
AI-native options flow trading system powered by the Anthropic Claude Agent SDK. Monitors institutional options flow via the Unusual Whales API, scores signals using deterministic rules, and executes trades through Alpaca with a multi-layered safety architecture. Trades calls only, ASK-side only — following institutional buyers with no directional interpretation needed.
Monitor Loop (30-180s adaptive polling)
|
v
Orchestrator (Claude Sonnet 4 - lead agent)
|
+-- Flow Scanner ------> Unusual Whales API (/option-trades/flow-alerts)
| - newer_than high-watermark (only new alerts)
| - Calls only, ASK-side only, opening positions
| - min_premium, DTE, vol/OI filters
|
+-- Position Manager ---> SQLite DB (positions, P&L, Greeks)
|
+-- Risk Manager -------> Portfolio risk scoring (0-100)
|
+-- Executor -----------> Alpaca broker (limit orders, fill polling)
|
Safety Gate (14 hard checks, non-overridable)
The orchestrator delegates work to four specialized subagents. Each subagent has its own tools and prompt (see config/prompts/). All trade execution passes through a deterministic safety gate that cannot be overridden by the AI.
- Python 3.11+
- API keys: Anthropic, Unusual Whales, Alpaca (paper or live)
- Optional: Telegram bot token for notifications
cd /home/ubuntu/momentum-agent-v2
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"Copy .env.example to .env and fill in your API keys:
ANTHROPIC_API_KEY=sk-ant-...
UW_API_KEY=...
ALPACA_API_KEY=...
ALPACA_SECRET_KEY=...
ALPACA_BASE_URL=https://paper-api.alpaca.markets
TELEGRAM_BOT_TOKEN=... # optional
TELEGRAM_ADMIN_ID=... # optional
PAPER_TRADING=true
SHADOW_MODE=falseAll configuration lives in config/settings.py as a single Pydantic Settings class. Environment variables and .env are validated at startup.
# Continuous monitoring loop (production)
python -m scripts.run
# Single scan cycle (testing)
python -m scripts.run --once
# Risk assessment only
python -m scripts.run --risk
# Kill switch
python -m scripts.run --kill # halt all trading
python -m scripts.run --unkill # resumesudo cp momentum-agent.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now momentum-agent
journalctl -u momentum-agent -fconfig/
settings.py # Unified Pydantic config (single source of truth)
prompts/ # External markdown prompts for each agent
orchestrator.md
flow_scanner.md
position_manager.md
risk_manager.md
executor.md
core/
safety.py # 13-point deterministic safety gate
circuit_breaker.py # Loss-based trading halts (daily/weekly/consecutive)
killswitch.py # File-based emergency halt
health.py # Dependency health checks (APIs, DB, disk)
reconciler.py # Alpaca <-> DB position sync
logger.py # Structlog with correlation IDs + JSON output
utils.py # OCC symbol parsing, DTE calculation
data/
models.py # SQLAlchemy ORM + Pydantic schemas
services/
unusual_whales.py # UW flow-alerts client with newer_than watermark
alpaca_broker.py # Alpaca order submission + account queries
alpaca_options_data.py # Alpaca options data client (Greeks, IV, bid/ask snapshots)
telegram.py # Telegram notification service
tools/
flow_tools.py # scan_flow, score_signal, save_signal, send_scan_report
position_tools.py # get_open_positions, check_exit_triggers
risk_tools.py # calculate_portfolio_risk, pre_trade_check
execution_tools.py # execute_entry, execute_exit, position sizing
agents/
orchestrator.py # Lead agent + AgentRunner (agentic loop)
definitions.py # Prompt loading from external files
monitor/
loop.py # Main event loop with market hours + circuit breakers
analytics/
performance.py # Win rate, profit factor, Sharpe, max drawdown
bot/
commands.py # Telegram bot (15 commands, auth-gated)
scripts/
run.py # CLI entry point
tests/
unit/ # Unit tests
e2e/ # End-to-end tests (requires live APIs)
scenarios/ # Scenario-based integration tests
Signals from Unusual Whales are scored on a 0-10 scale. Minimum to pass: 7.
| Indicator | Points |
|---|---|
| Sweep order | +2 |
| Floor trade | +2 |
| Opening position | +2 |
| Vol/OI >= 1.5 | +1 |
| Vol/OI >= 3.0 | +1 |
| Premium >= $250K | +1 |
| Premium >= $500K | +2 |
| Directional >= 75% | +1 |
| Directional >= 90% | +2 |
| Single-leg (no multi) | +1 |
| Block trade (<10) | +1 |
| Indicator | Points |
|---|---|
| IV rank > 70% | -3 |
| DTE < 6 | -2 |
| DTE 6-13 | -1 |
| Earnings within 7 days | -2 |
| Non-CALL option | blocked |
is_call: true,is_put: false (calls only)is_ask_side: true (institutional buyers only)all_opening: true (opening positions only)min_premium: $100,000min_volume_oi_ratio: 1.5min_dte: 6 (no upper limit — LEAPs allowed)issue_types: Common Stock (excludes ETFs)newer_than: Unix timestamp of last scan (high-watermark)
13 deterministic checks executed before every order submission. These cannot be overridden by the AI agent:
- Calls-only enforcement (non-CALL types hard-blocked)
- Excluded ticker blocklist
- Max positions (3)
- Max total exposure (25% of equity)
- Max position value ($1,000)
- Max executions per day (2)
- Daily loss limit (5% of equity)
- Weekly loss limit (10% of equity)
- IV rank cap (70%)
- Minimum DTE (6)
- Max bid-ask spread (15%)
- Earnings blackout (2 days)
- Market timing (15 min buffer before market close; no open delay by default)
Note: Consecutive losses are handled exclusively by the circuit breaker (120-min cooldown), NOT the safety gate. A duplicate check here previously caused a permanent deadlock — the gate blocked all entries, which meant no new trades could clear the loss counter, freezing the system indefinitely.
Three independent loss-based breakers:
- Daily: Realized losses >= 5% equity -> halt rest of day
- Weekly: Realized losses >= 10% equity -> halt until Monday
- Consecutive: 2 consecutive losing trades -> halt 120 minutes
File-based emergency halt. If the KILLSWITCH file exists in the project root, all trading stops immediately. Controlled via CLI or Telegram /killswitch command.
Exits are evaluated every monitoring cycle:
| Trigger | Condition | Priority |
|---|---|---|
| DTE mandatory | DTE <= 5 | Highest |
| Hard stop loss | P&L <= -35% | High |
| Adaptive profit target | P&L >= 15-40% by DTE | High |
| Gamma risk | DTE <= 5 and P&L < 20% | Medium |
| Conviction drop | Conviction < 50% | Medium |
Adaptive profit targets by DTE:
| DTE Range | Target |
|---|---|
| > 14 | 40% |
| 7-14 | 35% |
| 3-7 | 25% |
| < 3 | 15% |
Uses the /option-trades/flow-alerts endpoint with a high-watermark pattern:
- First call after startup: fetch latest alerts (no time filter)
- Record
time.time()as_last_scan_ts - Subsequent calls: pass
newer_than=<_last_scan_ts>so the API only returns alerts created after the last poll - Client-side dedup set in
flow_tools.pyprovides a second layer of protection against rescoring
This prevents the same contract from being scored repeatedly across poll cycles.
Blocked from trading (hedging noise, low signal-to-noise):
- Index ETFs: SPY, QQQ, IWM, DIA
- Sector ETFs: XLF, XLE, XLK, XLV, XLI, XLU, XLB, XLC, XLY, XLP, XLRE
- Commodities/Bonds/Vol: GLD, SLV, TLT, HYG, EEM, EFA, UNG, VXX, UVXY, SVXY
- Leveraged: SQQQ, TQQQ, SPXU, SPXL, UPRO
- Meme/Low-quality: AMC, GME, BBBY, MULN, HYMC, MMAT, ATER, DWAC, WISH, PLTR
- Index options: SPXW, SPX, NDX, XSP
Runs every 5th monitor cycle (~60 seconds). Compares Alpaca broker positions against the local SQLite database and fixes discrepancies:
- Phantoms (in DB but not in broker): Position closed at broker without DB knowing (manual close, stop fill, etc.)
- Orphans (in broker but not in DB): Position exists at broker with no DB record (manual trade, restart data loss, etc.)
- Price drift: >10% discrepancy between broker and DB prices
Safety guards before phantom closure:
- TradeLog check — if
execute_exit()already closed this position (TradeLog exists), just flip status without creating a duplicate trade record - Pending exit intent — if an exit order is in flight (
OrderIntentwith status=PENDING), don't phantom-close; the order reconciler will handle the fill - Working sell order — if a SELL order is submitted/pending at the broker (
BrokerOrder), the position may briefly vanish fromget_positions()during fill; wait for it
Safety guards before orphan adoption:
- Same-cycle block — symbols phantom-closed in this cycle are not re-adopted as orphans
- Recently-closed block — symbols closed within the last 30 minutes are not re-adopted (prevents phantom→orphan→phantom death spiral)
Broker-aware position queries:
get_open_positions() fetches broker data every cycle and flags positions missing from the broker (broker_missing: true). The monitor loop skips exit trigger evaluation and Claude decisions for broker-missing positions — only the reconciler handles their cleanup.
Three output streams via structlog:
- Console: Human-readable (journalctl compatible)
- File:
logs/momentum.log - JSON:
logs/momentum.jsonl(machine-parseable)
Every event is tagged with session_id and cycle_id for tracing.
Run every 30 minutes, alert on 3+ consecutive failures:
- Alpaca API connectivity
- Unusual Whales API connectivity
- Database accessibility
- Disk space (> 500MB)
15 interactive commands (auth-gated to admin only):
/health - Run health checks
/status - Mode, uptime, scan count, circuit breaker state
/positions - Open positions with P&L, DTE
/orders - Pending broker orders
/expirations - DTE alerts for open positions
/risk - Portfolio risk score breakdown
/performance - 30-day metrics (win rate, Sharpe, drawdown)
/weekly - 7-day performance report
/history - Last 10 trades with P&L
/flow - Manual scan trigger
/close <id> - Close position by ID or ticker
/reconcile - Sync positions with broker
/killswitch - Toggle kill switch
/errors - Last 10 error log entries
/help - List available commands
Alpaca broker is the source of truth for all position state. The database is a journal for logging, trade history, and metadata enrichment (Greeks, conviction, thesis).
Every decision-making code path — safety gate, risk scoring, position sizing, exit triggers, Claude context — queries the broker first. The DB is only consulted for metadata the broker doesn't track (Greeks, entry thesis, conviction scores).
| Decision | Source | DB Role |
|---|---|---|
| Position count (max_positions gate) | Broker get_positions() |
Not used |
| Exposure calculation (max_exposure gate) | Broker get_positions() |
Not used |
| Position sizing (remaining capacity) | Broker get_positions() |
Not used |
| Risk scoring (delta/gamma/theta) | Broker positions | Greeks enrichment |
| Exit trigger evaluation | Broker positions | Greeks/conviction enrichment |
| Claude entry/exit context | Broker positions | Thesis enrichment |
| Trade history / P&L ledger | Not applicable | DB is primary |
| Circuit breakers (loss limits) | Not applicable | DB TradeLog is primary |
SQLite with SQLAlchemy ORM. Tables:
- signals: Scored flow signals (accepted/rejected)
- positions: Open/closed positions with Greeks and P&L
- order_intents: Idempotency layer (prevents duplicate orders on restart)
- broker_orders: Actual Alpaca order tracking
- trade_log: Completed trade P&L ledger (used by analytics and circuit breakers)
Pure SQL aggregates on the trade_log table:
- Win rate, profit factor, max drawdown
- Sharpe ratio (annualized, 5% risk-free rate)
- Average hold duration
- Daily P&L series
Available via analytics/performance.py and the Telegram /performance command.
# Run tests
pytest tests/ -v
# Lint
ruff check .
# Type check
mypy .| Package | Purpose |
|---|---|
| anthropic | Claude API (agent SDK) |
| alpaca-py | Broker (orders, account, data) |
| pydantic-settings | Validated configuration |
| sqlalchemy | ORM + database |
| alembic | Database migrations |
| httpx | Async HTTP client |
| structlog | Structured logging |
| pytz | Timezone handling |
| aiohttp | Telegram bot long-polling |
| websockets | WebSocket support |
| python-dotenv | .env file loading |
| yfinance | Earnings date lookups (fallback) |