Autonomous AI trading agent using Pydantic AI for type-safe agents, Temporal for durable execution, and the Unusual Whales MCP server for market data.
⚠️ Disclaimer: This is engineering scaffolding, not investment advice. Trading real money with autonomous AI agents is risky. Run paper-only until you have validated end-to-end behavior, and even then, treat live deployment with extreme caution.
┌─────────────────────────┐
│ Unusual Whales MCP │
│ api.unusualwhales.com │
└────────────▲────────────┘
│ tool calls (as activities)
│
┌──────────┐ start/signal ┌─────────────┴─────────────┐
│ FastAPI │ ───────────────────▶│ TradingSessionWorkflow │
│ control │ ◀── query ──────────│ (long-running, signals) │
│ plane │ └────┬─────────┬────────────┘
└──────────┘ │ │
▲ child wf │ │ child wf
│ ▼ ▼
│ ┌──────────────────┐ ┌─────────────────────┐
│ │ MarketScan │ │ PortfolioMonitor │
│ │ Workflow │ │ Workflow (poll loop)│
│ └────────┬─────────┘ └─────────┬───────────┘
│ │ │
│ │ child wf per ticker │ stop-loss orders
│ ▼ ▼
│ ┌────────────────────────────┐
│ │ TradeExecutionWorkflow │
│ │ scanner → researcher → │
│ │ trader → risk → submit │
│ └────────────────────────────┘
│ │
│ ▼
│ ┌────────────────────────────┐
│ │ Activities (I/O layer) │
└─────────────│ • get_portfolio (Alpaca) │
│ • evaluate_risk (HARD) │
│ • submit_trade (Alpaca) │
│ • record_* (Supabase) │
└────────────────────────────┘
- Pydantic AI — structured I/O between agents (every agent has a typed
output_type), MCP support, native Temporal wrapper. - Temporal — durable execution: model timeouts, broker hiccups, and worker restarts don't lose state. Each workflow restarts exactly where it stopped.
- Unusual Whales MCP — 100+ market data endpoints (options flow, dark pool, congress trades, Greek exposure) exposed as MCP tools. Agents discover and call them autonomously.
- Hard risk guardrails —
activities/risk.pyis deterministic Python, not LLM reasoning. The LLM cannot bypass position sizing, DTE limits, naked option bans, or kill switches.
| Agent | Model | Job | Output |
|---|---|---|---|
market_scanner |
Opus | Wide market scan via UW MCP | ScanResult (8 candidates max) |
ticker_researcher |
Opus | Deep dive on one ticker | ResearchBrief |
trader |
Opus | Construct trade structure | TradeProposal |
Each agent has a stable name because Temporal derives activity names from it. Once deployed, names cannot change without breaking in-flight workflows.
| Workflow | Lifetime | Pattern |
|---|---|---|
TradingSessionWorkflow |
Per session/day | Signal-driven, queryable state |
MarketScanWorkflow |
Minutes | Scan → fan-out child workflows |
TradeExecutionWorkflow |
Minutes | Research → propose → risk → submit |
PortfolioMonitorWorkflow |
Continuous | Poll loop with continue_as_new |
These run as deterministic Python in activities, after the agent has reasoned. The agent sees them in its prompt but cannot override them.
- Total loss kill switch — halt ALL trading if cumulative dollar loss > $1,000 (default)
- Total loss % kill switch — halt ALL trading if loss > 15% of starting equity (default)
- Per-trade notional cap — no single trade may cost more than $500 (default)
- Daily loss kill switch (-2% default)
- Per-position size cap (5% equity default)
- Open position count cap (15)
- No duplicate positions (blocks entering a ticker you already hold)
- Naked short option block (defined-risk only, hardened qty-aware check)
- DTE limits (2-45 days, no 0DTE)
- Penny stock filter ($5 min)
- Buying-power check
- Pattern day trader awareness
- Idempotent order IDs (deterministic from workflow ID, prevents double-fills on retry)
See tests/test_risk_guardrails.py — edit these in lockstep with activities/risk.py.
All limits are configurable via RISK_* environment variables. See Risk Settings Reference below.
The system uses a Python DSL for defining trading strategies. Each strategy is a typed StrategyPlaybook object composed from reusable primitives (leg templates, entry conditions, exit rules, sizing presets).
src/trading_agent/strategies/
├── __init__.py # public API: get_strategy, list_strategies
├── primitives.py # reusable building blocks (import these in playbooks)
├── registry.py # auto-discovers playbook modules, validates defined-risk
└── playbooks/
├── long_call_momentum.py
├── bull_put_credit_spread.py
├── iron_condor_range.py
└── equity_breakout.py # add your own .py files here
| Name | Category | Description |
|---|---|---|
long_call_momentum |
directional_long | Buy near-ATM call on low-IV bullish breakout |
bull_put_credit_spread |
credit_spread | Sell OTM put spread on elevated-IV bullish name |
iron_condor_range |
iron_condor | Sell put + call spread on high-IV range-bound name |
equity_breakout |
equity | Buy shares on technical breakout with volume confirmation |
Pass a strategies list when starting a session. Empty list = agent-discretion mode (uses all strategies):
# Use a specific strategy
curl -X POST localhost:8000/sessions \
-H "Content-Type: application/json" \
-d '{"auto_scan_minutes": 30, "strategies": ["equity_breakout"]}'
# Multiple strategies (agent picks the best fit per ticker)
curl -X POST localhost:8000/sessions \
-H "Content-Type: application/json" \
-d '{"auto_scan_minutes": 30, "strategies": ["bull_put_credit_spread", "long_call_momentum"]}'
# Agent-discretion mode (all strategies available)
curl -X POST localhost:8000/sessions \
-H "Content-Type: application/json" \
-d '{"auto_scan_minutes": 30}'- Create a new file in
src/trading_agent/strategies/playbooks/my_strategy.py - Import primitives and define a
STRATEGYconstant:
from trading_agent.models.strategy import ManagementRule, RiskOverrides, StrategyPlaybook
from trading_agent.strategies.primitives import (
BULLISH_TREND, CONSERVATIVE_SIZING, LONG_CALL_ATM,
LONG_PREMIUM_EXITS, NO_EXISTING_POSITION, NO_NEAR_EARNINGS, IV_LOW,
)
STRATEGY = StrategyPlaybook(
name="my_strategy",
description="Short description of what this strategy does.",
category="directional_long", # see StrategyCategory for options
legs=[LONG_CALL_ATM],
entry_conditions=[IV_LOW, BULLISH_TREND, NO_NEAR_EARNINGS, NO_EXISTING_POSITION],
exit_rules=LONG_PREMIUM_EXITS,
sizing=CONSERVATIVE_SIZING,
risk_overrides=RiskOverrides(max_dte_days=45, min_dte_days=21),
)- The registry auto-discovers it on next worker start — no registration step needed.
- Important: Any strategy with short option legs must include a covering long leg of the same type. The registry validates this at load time and will refuse to start if the strategy has unbounded risk.
Open src/trading_agent/strategies/primitives.py and add a new constant. For example, a new entry condition:
HIGH_CONVICTION = EntryCondition(
name="high_conviction",
description="Only trade when researcher conviction is above 0.8",
parameter="conviction",
operator="gte",
value=0.8,
)Then import it in any playbook that needs it.
A playbook can tighten (but never loosen) global risk limits via risk_overrides:
risk_overrides=RiskOverrides(
max_dte_days=30, # stricter than global 45-day max
min_dte_days=21, # stricter than global 2-day min
max_position_pct=0.03 # stricter than global 5% cap
)A strategy with allow_add_to_position=True will skip the duplicate-position blocker, enabling scaling into existing positions.
All risk limits are configurable via environment variables with the RISK_ prefix. Set them in your .env file or shell environment. Strategy risk_overrides can only make these limits stricter, never looser.
| Variable | Default | Description |
|---|---|---|
RISK_MAX_TOTAL_LOSS |
1000.0 |
Kill switch. Halt all trading if cumulative dollar loss exceeds this amount |
RISK_MAX_TOTAL_LOSS_PCT |
0.15 |
Kill switch. Halt all trading if loss exceeds this fraction of starting equity |
RISK_MAX_TRADE_NOTIONAL |
500.0 |
Max dollar cost of any single trade (all legs combined) |
RISK_MAX_DAILY_LOSS_PCT |
0.02 |
Halt for the day if daily P&L drops below -2% |
RISK_MAX_POSITION_PCT |
0.05 |
Max single equity position as fraction of total equity |
RISK_MAX_OPTIONS_PREMIUM_PCT |
0.02 |
Max single options trade as fraction of total equity |
RISK_MAX_OPEN_POSITIONS |
15 |
Hard cap on concurrent open positions |
RISK_MAX_DTE_DAYS |
45 |
No options with more than 45 days to expiry |
RISK_MIN_DTE_DAYS |
2 |
No 0DTE or same-day options |
RISK_FORBID_NAKED_OPTIONS |
true |
Block any short option not covered by a long (do not change) |
RISK_MIN_PRICE |
5.0 |
Penny stock filter — no tickers below this price |
RISK_MIN_AVG_VOLUME |
500000 |
Minimum average daily volume for liquidity |
RISK_MAX_PORTFOLIO_CONCENTRATION_PCT |
0.40 |
Max equity in a single sector |
Example: Conservative settings for a small account:
RISK_MAX_TOTAL_LOSS=500
RISK_MAX_TOTAL_LOSS_PCT=0.10
RISK_MAX_TRADE_NOTIONAL=250
RISK_MAX_OPEN_POSITIONS=5You need:
- Anthropic (or OpenAI) API key
- Unusual Whales API key — https://unusualwhales.com/settings/api-dashboard
- Alpaca paper keys — https://app.alpaca.markets/paper/dashboard/overview
cp .env.example .env
# Fill in keysmake up # Temporal + worker + API
make logs # Tail worker + API logsTemporal UI: http://localhost:8233 API: http://localhost:8000/docs
make install
docker compose up -d temporal
make worker # Terminal 1
make api # Terminal 2make test# Agent-discretion mode (all strategies available)
curl -X POST localhost:8000/sessions \
-H "Content-Type: application/json" \
-d '{"auto_scan_minutes": 30, "enable_monitor": true}'
# → {"session_id": "session-20260506-100000", ...}
# Restrict to specific strategies
curl -X POST localhost:8000/sessions \
-H "Content-Type: application/json" \
-d '{"auto_scan_minutes": 30, "enable_monitor": true, "strategies": ["equity_breakout", "bull_put_credit_spread"]}'curl -X POST localhost:8000/sessions/session-20260506-100000/scan \
-H "Content-Type: application/json" \
-d '{"context": "Focus on tech mega-caps with positive flow"}'curl localhost:8000/sessions/session-20260506-100000curl -X POST localhost:8000/sessions/session-20260506-100000/pause
curl -X POST localhost:8000/sessions/session-20260506-100000/resume
curl -X POST localhost:8000/sessions/session-20260506-100000/endcurl -X POST localhost:8000/sessions/scan/adhoc \
-H "Content-Type: application/json" \
-d '{"context": "look at AI infrastructure names"}'curl localhost:8000/portfolioA few changes you'll want before pointing this at real money:
- Replace Temporal dev server with
temporalio/auto-setup+ Postgres, or use Temporal Cloud. The dev server is ephemeral. - Add stop/target persistence. Currently
PortfolioMonitorWorkflowuses ±25%/+50% — should pulltarget_pct/stop_pctfrom the persisted decision row. - Tighten risk limits in
RISK_*env vars to match your account size and tolerance. Defaults are conservative but generic. - Logfire — set
LOGFIRE_TOKENto get full agent traces, including every MCP tool call and model response. - Schedule the session. Use a Temporal Schedule to start a session at market open and signal
end_sessionat close. Example:await client.create_schedule( "trading-session-daily", Schedule( action=ScheduleActionStartWorkflow(TradingSessionWorkflow.run, ...), spec=ScheduleSpec(cron_expressions=["30 9 * * 1-5"]), # 9:30 ET weekdays ), )
- Authenticate the API. Add JWT middleware (your fastapi-backend skill has the pattern).
- Monitor everything. PnL, win rate, slippage, MCP latency, model token spend.
Build the api target and deploy the worker as an ECS Fargate task (workers need to stay alive; AppRunner is request-driven). Both pull config from env.
docker build --target api -t trading-agent-api .
docker build --target worker -t trading-agent-worker .Educational/personal use. Not financial advice. Past performance, etc. You're responsible for your own trades. Paper-trade first.