Skip to main content
Glama
cactus001

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 |