TradingAgent
by cactus001
README.md
# TradingAgent
MCP-native conversational paper trading agent with FinBERT sentiment analysis.
> **"Buy 10 AAPL if it drops below $200 today"** — type it, the agent handles the rest.
## Table of Contents
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Running the Agent](#running-the-agent)
- [Example Conversation](#example-conversation)
- [Tools](#tools)
- [Safety](#safety)
- [FinBERT as a Production Service](#finbert-as-a-production-service)
- [Project Structure](#project-structure)
- [Tests](#tests)
- [Environment Variables](#environment-variables)
- [State Persistence](#state-persistence)
- [Tech Stack](#tech-stack)
---
## Architecture
```
┌──────────────────────┐ ┌──────────────────────────────────────┐
│ CLI Agent (REPL) │ │ Web UI (FastAPI) │
│ claude-sonnet-4-6 │ │ Dark chat · WebSocket · marked.js │
│ agentic loop │ │ ngrok → shareable demo URL │
└──────────┬───────────┘ └──────────────────┬───────────────────┘
│ MCP (stdio) │ MCP (stdio, per session)
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ FastMCP Server │
│ 20 tools · 4 modules │
└────┬──────────┬──────────┬──────────┘
│ │ │ │
trading market-data sentiment watchlist
8 tools 5 tools FinBERT 4 tools
│ │ │ │
└──────────┴──────────┴──────────┘
│
┌─────────────▼──────────────┐
│ 5-Layer Guardrail │
│ input·llm·tool·exec·output │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ Alpaca Paper API │
│ real quotes · fake money │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ Alert Daemon │
│ polls prices every 30s │
│ fires conditional orders │
└────────────────────────────┘
```
---
## Quick Start
**No Alpaca account needed for mock mode.**
### 1 — Install
```bash
# Clone and enter project
git clone https://github.com/cactus001/TRADE-AGENT.git
cd TRADE-AGENT
# Install dependencies (Apple Silicon — use Homebrew Python)
uv sync --python /opt/homebrew/bin/python3.12
# Install dev dependencies for tests
uv sync --extra dev
```
### 2 — Configure
```bash
cp .env.example .env
```
Open `.env` and fill in:
```env
ANTHROPIC_API_KEY=sk-ant-... # required — get from console.anthropic.com
ALPACA_API_KEY= # optional — paper trading keys from alpaca.markets
ALPACA_SECRET_KEY= # optional — leave blank to use --mock mode
```
> **Note:** Mock mode works without any Alpaca keys and runs FinBERT on real fake news headlines.
---
## Running the Agent
### Option A — CLI REPL (terminal chat)
```bash
# Mock mode (no Alpaca keys needed)
uv run python -m agent.cli_agent --mock
# Live paper trading (requires Alpaca keys in .env)
uv run python -m agent.cli_agent
```
Type natural language commands. Press `Ctrl+C` to exit — the session transcript saves automatically to `transcripts/`.
### Option B — Web UI (shareable chat interface)
```bash
# Mock mode
uv run python -m src.webapp --mock
# Live paper trading
uv run python -m src.webapp
```
Open **http://localhost:8000** in your browser — you'll see a dark-themed chat UI with:
- Live tool call chips showing which MCP tools are running
- Animated thinking indicator during inference
- Markdown tables for portfolio/order data
- `PAPER TRADING — NO REAL MONEY` watermark on all order confirmations
### Option C — Share via ngrok (live demo from anywhere)
Use this for interviews, demos, or sharing with anyone over the internet.
**Step 1 — Install ngrok**
```bash
brew install ngrok
```
**Step 2 — Authenticate**
1. Go to [dashboard.ngrok.com/get-started/your-authtoken](https://dashboard.ngrok.com/get-started/your-authtoken)
2. Copy your personal authtoken
3. Run:
```bash
ngrok config add-authtoken YOUR_REAL_TOKEN_HERE
```
**Step 3 — Start the web server**
```bash
uv run python -m src.webapp --mock
```
**Step 4 — Open the tunnel (in a second terminal)**
```bash
ngrok http 8000
```
ngrok will print a public URL like:
```
Forwarding https://oboe-routing-difficult.ngrok-free.app → http://localhost:8000
```
Share that URL — anyone with the link can open the chat interface and interact with TradingAgent in real time from any browser, no setup needed.
### Option D — Docker (Redis + Web UI)
Docker Compose spins up two services:
- **redis** — `redis:7-alpine`, persists to a named volume, health-checked before the agent starts
- **trading-agent** — builds from `Dockerfile`, runs the web UI on port 8000, connects to Redis automatically
```bash
# Copy env file and add your Anthropic key
cp .env.example .env
# edit .env — add ANTHROPIC_API_KEY (REDIS_URL is set automatically by compose)
# Build and start both services
docker-compose up --build
# Or run detached
docker-compose up --build -d
```
Open **http://localhost:8000** — the chat UI is live.
```bash
# Tail logs
docker-compose logs -f trading-agent
# Stop everything
docker-compose down
# Stop and wipe all volumes (resets state + Redis cache)
docker-compose down -v
```
**Run the CLI agent instead of web UI:**
```bash
docker-compose run --rm trading-agent uv run python -m agent.cli_agent --mock
```
---
## Example Conversation
```
You: what's my portfolio looking like
Agent: Portfolio — $101,432.20 total (+1.4% today)
TSLA 15 shares $248.20 +$190.50 (+5.4%)
NVDA 5 shares $134.50 -$43.00 (-6.0%)
Cash $94,890.50 available
You: what's the sentiment on NVDA right now
Agent: NVDA Sentiment (last 4h): BEARISH — score -0.41
Top drivers:
• "Analyst cuts NVDA target to $125" (negative, -0.81)
• "Nvidia faces rising competition" (negative, -0.62)
Recommendation: Consider waiting for stabilisation before adding.
You: buy 10 AAPL if it drops below $200 today
Agent: Day Limit Buy Preview [PAPER TRADING — NO REAL MONEY]
10 shares AAPL · Limit @ $200.00
AAPL currently: $212.95
Expires: 4:00 PM ET today · Max cost: $2,000.00
Confirm? (yes/no)
You: yes
Agent: ✅ Order placed (ID: abc-12345)
Limit buy active — AAPL will be purchased automatically if price hits $200.
[PAPER TRADING — NO REAL MONEY]
You: alert me when TSLA drops below 240
Agent: ✅ Alert set — will notify when TSLA < $240.00
Current price: $248.20 (monitoring every 30s)
```
---
## Tools
20 tools across 4 modules:
| Module | Tools |
|--------|-------|
| **trading** | `get_account`, `get_positions`, `get_orders`, `place_order`, `cancel_order`, `cancel_all_orders`, `get_portfolio_history`, `get_asset_info` |
| **market-data** | `get_quote`, `get_bars`, `get_news`, `get_market_status`, `search_symbol` |
| **sentiment** | `get_sentiment`, `get_market_mood`, `explain_sentiment` |
| **watchlist** | `set_price_alert`, `get_active_alerts`, `cancel_alert`, `get_trade_history` |
---
## Safety
A 5-layer guardrail pipeline runs on every order:
| Layer | What it catches |
|-------|-----------------|
| **Input guard** | Prompt injection patterns in user messages |
| **LLM guard** | Injected instructions hidden in news headlines; hardens system prompt |
| **Tool guard** | Invalid ticker formats, negative qty, missing required prices, sanity limits |
| **Execution guard** | Single order > 20% portfolio, daily loss > 5%, > 10 orders/hr, wash trades (< 5 min), circuit breaker (SPY down > 5%) |
| **Output guard** | Broker rejections, post-trade concentration warnings |
`place_order` enforces a mandatory two-step flow: `confirm=False` (preview) must be called before `confirm=True` (execute). No order reaches the broker without an explicit user confirmation in the conversation.
---
## FinBERT as a Production Service
`src/models/finbert.py` promotes FinBERT (`ProsusAI/finbert`) from a standalone script to a production-grade callable MCP service:
| Pattern | Implementation |
|---------|-----------------|
| **Singleton** | Module-level `_instance`, one model loaded per process |
| **Lazy loading** | Model not loaded until first `analyze()` call |
| **Double-checked locking** | `threading.Lock` with `if self._loaded` checked inside the lock |
| **Device auto-detection** | CUDA → Apple MPS → CPU, no environment config needed |
| **Batch inference** | Chunks news lists into `BATCH_SIZE=16` to avoid OOM |
| **Normalised score** | Returns `pos_prob − neg_prob` in `[-1.0, +1.0]` |
| **Redis cache** | `sha256(headline)` key, 1h TTL — identical headlines never hit GPU twice |
| **Partial-hit pattern** | Per-batch cache lookup; only misses go to FinBERT, hits served in <1ms |
| **Graceful degradation** | Redis unavailable → cache is a no-op, inference runs normally |
On Apple Silicon, inference runs on the **MPS GPU** (confirmed: `device: mps`). With Redis warm, repeated `get_sentiment` calls on the same news cycle return instantly.
---
## Project Structure
```
TRADE-AGENT/
├── agent/
│ └── cli_agent.py # REPL — manual agentic loop, auto-saves transcripts
├── src/
│ ├── server.py # FastMCP entry point — registers all 4 tool modules
│ ├── webapp.py # FastAPI + WebSocket web interface
│ ├── config.py # Pydantic settings — env vars with defaults
│ ├── state_manager.py # Persistent state (~/.trading-agent/state.json)
│ ├── alert_daemon.py # Background thread — polls prices every 30s
│ ├── models/
│ │ └── finbert.py # FinBERT singleton service (production ML pattern)
│ ├── cache/
│ │ └── redis_cache.py # Redis sentiment cache — partial-hit, 1h TTL, graceful degradation
│ ├── clients/
│ │ └── alpaca_client.py # Thin wrapper around alpaca-py SDK
│ ├── guardrails/
│ │ ├── input_guard.py # Regex injection pattern detection
│ │ ├── llm_guard.py # News sanitisation + system prompt hardening
│ │ ├── tool_guard.py # Symbol/qty/price validation
│ │ ├── execution_guard.py # Size, loss, velocity, wash-trade, circuit-breaker
│ │ ├── output_guard.py # Broker rejection + concentration check
│ │ └── guard_registry.py # Wires all 5 layers into one object
│ ├── tools/
│ │ ├── trading.py # 8 trading tools (place_order confirm gate)
│ │ ├── market_data.py # 5 market data tools
│ │ ├── sentiment.py # 3 FinBERT sentiment tools
│ │ └── watchlist.py # 4 alert/history tools
│ └── static/
│ └── index.html # Dark chat UI (WebSocket, marked.js, tool chips)
├── mock/
│ └── mock_provider.py # MockAlpacaClient — full demo without API keys
├── tests/
│ ├── test_guardrails.py # 15 tests across all 5 guardrail layers
│ └── test_sentiment.py # 7 tests — singleton, batch, device detection
├── transcripts/ # Auto-saved session logs (git-ignored)
├── pyproject.toml
├── docker-compose.yml
├── Dockerfile
└── .env.example
```
---
## Tests
```bash
uv run pytest tests/ -v
# 22 passed
```
---
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `ANTHROPIC_API_KEY` | **Yes** | — | Claude API key — [console.anthropic.com](https://console.anthropic.com) |
| `ALPACA_API_KEY` | No | — | Alpaca paper trading key — [alpaca.markets](https://alpaca.markets) |
| `ALPACA_SECRET_KEY` | No | — | Alpaca paper trading secret |
| `REDIS_URL` | No | `redis://localhost:6379` | Redis connection string — auto-set by docker-compose |
| `MAX_ORDER_PCT` | No | `0.20` | Max single order as fraction of portfolio |
| `DAILY_LOSS_LIMIT` | No | `-0.05` | Halt buys if day P&L falls below this |
| `VELOCITY_LIMIT` | No | `10` | Max orders per hour |
---
## State Persistence
The agent persists alerts, trade history, and the velocity/wash-trade counters to:
```
~/.trading-agent/state.json
```
This file lives outside the project directory and is never committed. Delete it to reset all state.
---
## Tech Stack
| Component | Technology |
|-----------|-----------|
| Agent SDK | Anthropic Python SDK (claude-sonnet-4-6) |
| Tool protocol | MCP (Model Context Protocol) via FastMCP |
| Broker API | Alpaca Paper Trading (alpaca-py) |
| Sentiment model | ProsusAI/FinBERT (HuggingFace Transformers) |
| ML runtime | PyTorch 2.x — MPS / CUDA / CPU auto |
| Inference cache | Redis 7 — sha256-keyed, 1h TTL, graceful degradation |
| Web server | FastAPI + Uvicorn + WebSocket |
| Frontend | Vanilla JS, marked.js, CSS custom properties |
| Containers | Docker + docker-compose (two-service: redis + trading-agent) |
| Tunnel | ngrok (free tier) |
| Package manager | uv |
| Tests | pytest + pytest-asyncio |
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues