Skip to content

Repository files navigation

Trading Agent

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.

Architecture

                                     ┌─────────────────────────┐
                                     │  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)     │
                      └────────────────────────────┘

Why each piece

  • 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 guardrailsactivities/risk.py is deterministic Python, not LLM reasoning. The LLM cannot bypass position sizing, DTE limits, naked option bans, or kill switches.

Agents

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.

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

Risk guardrails (the safety layer)

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.


Strategy Playbooks

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).

File layout

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

Included strategies

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

Selecting strategies per session

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}'

Creating a new strategy

  1. Create a new file in src/trading_agent/strategies/playbooks/my_strategy.py
  2. Import primitives and define a STRATEGY constant:
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),
)
  1. The registry auto-discovers it on next worker start — no registration step needed.
  2. 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.

Adding new primitives

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.

Strategy risk overrides

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.


Risk Settings Reference

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=5

Setup

1. API keys

You need:

2. Configure

cp .env.example .env
# Fill in keys

3. Run with Docker

make up        # Temporal + worker + API
make logs      # Tail worker + API logs

Temporal UI: http://localhost:8233 API: http://localhost:8000/docs

4. Run without Docker (talks to dockerized Temporal)

make install
docker compose up -d temporal
make worker    # Terminal 1
make api       # Terminal 2

5. Run the test suite

make test

Usage

Start a trading session

# 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"]}'

Trigger an immediate scan

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"}'

Query session state

curl localhost:8000/sessions/session-20260506-100000

Pause / resume / end

curl -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/end

One-shot scan (no session)

curl -X POST localhost:8000/sessions/scan/adhoc \
  -H "Content-Type: application/json" \
  -d '{"context": "look at AI infrastructure names"}'

Portfolio

curl localhost:8000/portfolio

Going to production

A few changes you'll want before pointing this at real money:

  1. Replace Temporal dev server with temporalio/auto-setup + Postgres, or use Temporal Cloud. The dev server is ephemeral.
  2. Add stop/target persistence. Currently PortfolioMonitorWorkflow uses ±25%/+50% — should pull target_pct/stop_pct from the persisted decision row.
  3. Tighten risk limits in RISK_* env vars to match your account size and tolerance. Defaults are conservative but generic.
  4. Logfire — set LOGFIRE_TOKEN to get full agent traces, including every MCP tool call and model response.
  5. Schedule the session. Use a Temporal Schedule to start a session at market open and signal end_session at 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
        ),
    )
  6. Authenticate the API. Add JWT middleware (your fastapi-backend skill has the pattern).
  7. Monitor everything. PnL, win rate, slippage, MCP latency, model token spend.

Deploying to AppRunner

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 .

License & disclaimer

Educational/personal use. Not financial advice. Past performance, etc. You're responsible for your own trades. Paper-trade first.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages