isA Trade
Multi-agent trading system with 4 specialized AI agents for automated market analysis and trade execution.
Overview
isA Trade is a coordinated multi-agent architecture where specialized agents collaborate to make trading decisions. Each agent runs as an independent FastAPI service communicating via NATS messaging.
| Agent | Role |
|---|---|
| Orchestrator | Central CIO — coordinates agents, makes final decisions. Runs on port 18790. |
| Research Agent | Fundamental analysis, earnings, news sentiment |
| Technical Agent | Chart patterns, indicators, signal generation |
| Risk Agent | Portfolio monitoring, position validation, veto power |
All four agents run in-process in a single orchestrator via the isA Agent SDK SwarmOrchestrator — there are no separate per-agent HTTP servers.
Quick Start
# Full dev setup (venv + dependencies)
make dev-setup
cp .env.example .env # add API keys + isA_user connection config
# Start infrastructure (no local Postgres — isA_user + isA_Data are external deps)
make docker-up
# Start the orchestrator (agents run in-process)
make run-local
# Check health
make health-checkArchitecture
isA Trade is a pure-stateless agent team (ADR 0001, epic #429) — it owns zero persistent tables. Agents run in-process via the isA Agent SDK’s SwarmOrchestrator and only ever read current state and decide; they never own data. State ownership is split across three planes:
┌─────────────────────────────────────────────────┐
│ Market Data Feeds │
│ yfinance • Finnhub • FRED • CCXT │
└──────────────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Orchestrator (Port 18790) │
│ Coordinates analysis, makes decisions │
│ (in-cycle memory only — no DB) │
└───────┬──────────────┬──────────────┬───────────┘
↓ ↓ ↓
┌──────────────┐ ┌───────────┐ ┌──────────────┐
│ Research │ │ Technical │ │ Risk │
│ Agent │ │ Agent │ │ Agent │
│ Fundamental │ │ Indicators│ │ VETO POWER │
│ Sentiment │ │ Patterns │ │ Portfolio │
│ Earnings │ │ Signals │ │ Limits │
└──────────────┘ └───────────┘ └──────────────┘
│ │ │
└──────────────┼──────────────┘
↓
┌─────────────────────────────────────────────────┐
│ isA_user (system-of-record) │
│ portfolio • orders • account • audit — Postgres │
├─────────────────────────────────────────────────┤
│ isA_Data (analytics lakehouse) │
│ OHLCV • features • PnL history — Delta/MinIO │
└─────────────────────────────────────────────────┘Data ownership (three-plane separation)
| Plane | Owns | Role |
|---|---|---|
| isA Trade | nothing durable (in-cycle memory only) | compute / decisions — orchestrator + Research/Technical/Risk agents |
| isA_user | portfolio, orders, account, audit | transactional system-of-record, reached via a typed IsaUserClient (httpx + circuit breaker) — never a shared DB |
| isA_Data | OHLCV / features / PnL history | analytics lakehouse (Delta on MinIO), read via embedded DuckDB (isadata://); edition-gated (bigdata_enabled, off by default on SaaS/lite editions) |
isA Trade previously owned a local Postgres trade schema (positions, portfolio
snapshots, trades, signals, risk events). That schema — along with its Alembic
migrations and docker-compose postgres service — was fully removed as part of
epic #429; the trade journal and PnL snapshots are now no-ops on the isA Trade
side since isA_user owns them via its execution + broker-sync flows.
Refuse-to-trade on unavailability. Because isA_user is now a hard runtime
dependency, IsaUserHaltPolicy continuously probes isA_user /health and drives
the Kill Switch: the system halts (never trades on stale or absent state) if
isA_user is unreachable, and auto-resumes once it recovers. The execute path
returns 503 while halted.
[!NOTE]
get_portfolio_historyreturnsnot_availableuntil the isA_Data PnL product lands; PnL time-series is analytics, not transactional, so it is deferred to isA_Data rather than re-homed into isA_user.
Trading Cockpit ↔ Mate auth delegation
The Trading Cockpit UI delegates into isA Trade through isA Mate rather than calling the orchestrator directly. All four hops in that delegation chain (Cockpit → Mate → isA Trade → isA_user) share a consistent 300s timeout, and the end-to-end delegation path is verified green. Autonomous/background trading events currently use a lax expiry check on the delegated token — tighter expiry validation is tracked as follow-up work, not yet shipped.
Risk Management
The Risk Agent enforces hard limits that cannot be overridden:
| Rule | Limit |
|---|---|
| Max position risk | 2% of portfolio |
| Max sector concentration | 25% |
| Daily loss circuit breaker | 3% |
| Max open positions | 15 |
| Drawdown circuit breaker | 10% |
| Max correlation with existing | 0.8 |
Agent Capabilities
Research Agent
- Financial statement analysis
- Earnings reports and guidance
- News sentiment scoring
- Sector and macro analysis
Technical Agent
- Chart pattern recognition
- Technical indicators (RSI, MACD, Bollinger, etc.)
- Signal generation and regime detection
- Volume and momentum analysis
Orchestrator
- Multi-agent coordination and pipeline routing
- Decision fusion from all agent signals
- Trade journal and audit logging
- Session management
Tech Stack
| Tool | Purpose |
|---|---|
| Python 3.11+ | Language |
| FastAPI | Orchestrator HTTP surface |
| isA Agent SDK | Agent framework (in-process SwarmOrchestrator) |
| isA_user | System-of-record — portfolio, orders, account, audit (via IsaUserClient) |
| isA_Data | Analytics lakehouse — OHLCV, features, PnL history (Delta on MinIO, DuckDB reads) |
| Pandas, NumPy, SciPy | Data analysis |
| yfinance, Finnhub, FRED | Market data |
| CCXT | Crypto exchange connectivity |
Risk Management & Approvals
isA Trade enforces multiple layers of safety controls that protect the portfolio from catastrophic losses. These controls are hardcoded and non-overridable — no agent, operator, or configuration change can bypass them during live operation.
[!CAUTION] Safety-critical system. The risk management rules described below are enforced at the code level. Disabling or weakening them requires source code changes and should never be done in a production environment.
Live Trading Approval Gate
Before any real money is at risk, the system requires an explicit approval gate. This prevents accidental live trading during development, testing, or misconfiguration.
How it works:
- The orchestrator starts with a
TRADING_MODEenvironment variable (paperorlive). - When
TRADING_MODE=live, the system checks theLIVE_TRADING_GATEenvironment variable. - If
LIVE_TRADING_GATEis not set to exactlyapproved(case-insensitive), the system silently falls back to paper trading using a simulatedPaperBroker. - Even when the gate is approved, a live broker (Alpaca, CCXT, Moomoo, or Polymarket) must also be configured with valid API keys — otherwise the system still falls back to paper mode.
[!WARNING] Double-lock design. Both
TRADING_MODE=liveANDLIVE_TRADING_GATE=approvedmust be set for real orders to execute. If either is missing or incorrect, the system uses the paper broker with no real money at risk.
# Paper trading (default — no gate required)
TRADING_MODE=paper
# Live trading — requires both settings
TRADING_MODE=live
LIVE_TRADING_GATE=approved
ALPACA_API_KEY=your-key-here # or other broker credentialsRisk Agent Veto Power
The Risk Agent acts as Chief Risk Officer (CRO) with absolute veto authority over all trade decisions. This is the most powerful safety mechanism in the system.
Veto protocol:
- The Risk Agent’s veto is absolute — there is no appeal process and no override mechanism.
- When a veto is issued, immediate compliance is required with no exceptions.
- The Orchestrator must respect the veto even if the Research Agent and Technical Agent both strongly recommend the trade.
- Other agents may request clarification on a veto but cannot challenge the decision.
- All veto reasons are logged to the audit trail but are not subject to debate.
The Risk Agent vetoes a trade when any of these conditions are true:
| Condition | Threshold | Effect |
|---|---|---|
| Position risk too high | > 2% of portfolio per position | Veto with suggested smaller size |
| Sector over-concentrated | > 25% in one sector | Veto |
| Daily loss limit hit | > 3% loss in current day | Veto all new trades |
| Too many open positions | > 15 positions | Veto |
| Drawdown circuit breaker | > 10% from peak equity | Veto all trading |
| High correlation | > 0.8 with existing positions | Veto |
When a trade is vetoed, the ValidationResult includes:
veto: trueand aveto_reasonexplaining which rule was violated- A
risk_score(0-100) indicating overall risk level suggested_adjustmentswhen applicable (e.g., a smaller position size that would pass)
Circuit Breakers
Circuit breakers are automatic safety triggers managed by the Kill Switch module. When a circuit breaker trips, trading halts immediately without human intervention.
[!CAUTION] Circuit breakers cannot be disabled. They are initialized at startup with the execution engine and run continuously. When triggered, they send critical alerts via the notification system and record the event in Prometheus metrics.
Circuit breaker thresholds:
| Trigger | Threshold | Behavior |
|---|---|---|
| Max drawdown | 10% from peak equity | Halt → 5 min cooldown |
| Daily loss | 3% of current equity | Halt → 5 min cooldown |
| Order rate limit | 20 orders per minute | Halt → 5 min cooldown |
| Consecutive losses | 5 losing trades in a row | Halt → 5 min cooldown |
Since the stateless migration (epic #429), the Kill Switch also halts trading
whenever IsaUserHaltPolicy detects isA_user is unreachable — the system-of-record
being unavailable is treated as a circuit-breaker condition, not a soft error.
Kill Switch states:
ACTIVE ──→ COOLDOWN ──→ ACTIVE
│ ↑
│ (auto-halt: drawdown, daily loss,
│ rate limit, consecutive losses)
│
└──→ HALTED ──→ ACTIVE
(manual halt) (manual resume)- ACTIVE — Normal trading. All orders are processed.
- COOLDOWN — Automatic halt triggered by a circuit breaker. Trading resumes automatically after a 5-minute cooldown period. No manual intervention required.
- HALTED — Manual halt triggered by an operator (or emergency close). Requires explicit
resume()call to restart trading.
Emergency close: The /api/v1/execute/emergency-close endpoint triggers an immediate HALT, closes all open positions, and sends critical alerts. Use this when you need to exit the market immediately.
Risk Management Summary
| Rule | Limit | Enforced By |
|---|---|---|
| Max position risk | 2% of portfolio | Risk Agent (veto) |
| Max position size | 10% of portfolio | Risk Agent (warning) |
| Max sector concentration | 25% | Risk Agent (veto) |
| Daily loss circuit breaker | 3% | Kill Switch (auto-halt) + Risk Agent (veto) |
| Drawdown circuit breaker | 10% | Kill Switch (auto-halt) + Risk Agent (veto) |
| Max open positions | 15 | Risk Agent (veto) |
| Max correlation | 0.8 | Risk Agent (veto) |
| Order rate limit | 20/min | Kill Switch (auto-halt) |
| Consecutive losses | 5 | Kill Switch (auto-halt) |
| Position sizing | Half-Kelly criterion | Risk Agent (sizing) |
| Live trading gate | LIVE_TRADING_GATE=approved | Execution module (startup) |
Monitoring & Alerting
isA Trade exposes Prometheus metrics via a /metrics endpoint and ships alerting rules for production monitoring. Metrics use the isa_trade_* naming prefix and are defined in src/orchestrator/metrics.py.
Key Metrics
| Metric | Type | Description |
|---|---|---|
isa_trade_orders_total | Counter | Total orders submitted (labels: side, status, broker) |
isa_trade_order_latency_seconds | Histogram | Order submission latency by broker |
isa_trade_slippage_bps | Histogram | Order slippage in basis points |
isa_trade_kill_switch_state | Gauge | Kill switch state (0=active, 1=halted, 2=cooldown) |
isa_trade_portfolio_equity | Gauge | Current portfolio equity in dollars |
isa_trade_active_positions | Gauge | Number of active positions |
isa_trade_agent_duration_seconds | Histogram | Per-agent invocation duration (labels: agent) |
isa_trade_http_requests_total | Counter | HTTP requests by method, path, and status code |
Alerting Rules
Alerting rules live in deployment/prometheus/alerts.yml and cover three categories. Validate with promtool check rules deployment/prometheus/alerts.yml.
Kill Switch Alerts
| Alert | Expression | Severity | Description |
|---|---|---|---|
TradingKillSwitchHalted | isa_trade_kill_switch_state == 1 | Critical | Kill switch entered HALTED state. All trading suspended. Manual resume() required. |
TradingCircuitBreakerTripped | isa_trade_kill_switch_state != 0 | Warning | Kill switch is not ACTIVE. Check logs for the circuit breaker trigger reason. |
Risk Alerts
| Alert | Expression | Severity | Description |
|---|---|---|---|
TradingDrawdownExceeded | Drawdown from 24h peak > 5% | Critical | Portfolio drawdown exceeded 5% threshold. Review positions. |
TradingDailyLossExceeded | 24h loss > 2% | Critical | Portfolio lost more than 2% in 24 hours. Assess risk exposure. |
Availability Alerts
| Alert | Expression | Severity | Description |
|---|---|---|---|
TradingServiceDown | up{job="isa-trade"} == 0 for 1m | Critical | Prometheus cannot scrape the service. Check pod status. |
TradingHighErrorRate | 5xx rate > 5% over 5m | Warning | HTTP error rate above threshold. Check application logs. |
TradingHighOrderLatency | P95 order latency > 5s over 5m | Warning | Order latency degraded. Possible broker or market data issues. |
Dashboard Setup
To set up Prometheus scraping, add the isA Trade target to your Prometheus config:
scrape_configs:
- job_name: isa-trade
scrape_interval: 15s
static_configs:
- targets: ["localhost:18790"]Load the alerting rules:
rule_files:
- /path/to/isA_Trade/deployment/prometheus/alerts.ymlMonitor key metrics during operation:
# Watch live metrics
watch -n 5 'curl -s http://localhost:18790/metrics | grep -E "isa_trade_(http_requests|agent_invocation|order_latency)"'Load Testing
isA Trade includes a Locust-based load testing framework for benchmarking API performance under concurrent user load. The test file lives at tests/load/locustfile.py.
Setup
# Install dev dependencies (includes locust)
pip install -e ".[dev]"
# Start infrastructure and orchestrator
make docker-up
make run-localRunning Load Tests
# Quick run: 10 users, ramp 2/sec, 60 second duration
make load-test
# Interactive mode with Locust web UI
locust -f tests/load/locustfile.py
# Headless with custom parameters
locust -f tests/load/locustfile.py --headless \
-u 50 -r 5 -t 300s \
--csv=results/load-test \
--html=results/load-test-report.html
# Target a different environment
TARGET_HOST=http://staging:18790 locust -f tests/load/locustfile.py --headless -u 20 -r 5 -t 120s| Flag | Description | Default |
|---|---|---|
-u | Total concurrent users | 10 |
-r | User spawn rate per second | 2 |
-t | Test duration | 60s |
--csv | Export CSV results to path | — |
--html | Export HTML report to path | — |
Test Scenarios
The Locust test simulates realistic traffic with weighted endpoint distribution:
| Endpoint | Method | Weight | Description |
|---|---|---|---|
/health | GET | 5 | Health check (baseline latency) |
/ready | GET | 2 | Readiness probe |
/api/v1/portfolio | GET | 3 | Portfolio state |
/api/v1/portfolio/risk | GET | 2 | Risk metrics |
/api/v1/capabilities | GET | 1 | Capabilities listing |
/api/v1/analyze (equity) | POST | 10 | Full analysis for equity symbols |
/api/v1/analyze (crypto) | POST | 3 | Full analysis for crypto symbols |
/api/v1/analyze (tech-only) | POST | 2 | Technical-only analysis |
/api/v1/pipeline | POST | 5 | Full multi-agent pipeline execution |
/api/v1/trades | GET | 1 | Trade history |
/status | GET | 1 | System status |
Performance Baselines
Track these metrics across load test runs to detect regressions:
| Metric | Target |
|---|---|
| Requests/sec (avg) | Record per environment |
| Failure rate | < 1% |
| p50 latency (ms) | Record per endpoint |
| p95 latency (ms) | Record per endpoint |
| p99 latency (ms) | Record per endpoint |
Known Bottlenecks
- Agent analysis latency —
/api/v1/analyzeand/api/v1/pipelineinvoke LLM calls via the SwarmOrchestrator. Latency is dominated by external LLM API calls, not application code. - Sequential pipeline stages —
/api/v1/pipelineruns agents sequentially (research, technical, risk). Prefer/api/v1/analyzefor latency-sensitive paths where agents run in parallel via DAG. - No connection pooling for external APIs — Each agent call creates independent HTTP connections to LLM providers, which may exhaust connection limits under high concurrency.
Security Scanning
isA Trade integrates Trivy container vulnerability scanning into its CI pipeline. Every Docker image is scanned before it can be pushed to the container registry.
How It Works
The CI workflow (.github/workflows/ci.yml) runs Trivy after each Docker build:
- Build — Docker image is built from
deployment/docker/Dockerfileand tagged with the version andlatest. - Scan — Trivy scans the image for
CRITICALandHIGHseverity vulnerabilities. Unfixed vulnerabilities are ignored. - Gate — If Trivy finds any fixable CRITICAL or HIGH vulnerability, the CI pipeline fails (
exit-code: 1), blocking the image push. - Report — Results are uploaded as a SARIF file to GitHub Code Scanning and as a build artifact (retained for 30 days).
Configuration
| Setting | Value |
|---|---|
| Severity filter | CRITICAL,HIGH |
| Exit code on findings | 1 (pipeline fails) |
| Ignore unfixed | true |
| Output format | SARIF |
| Results retention | 30 days |
Viewing Results
- GitHub Security tab — Trivy SARIF results appear under Code Scanning alerts in the repository’s Security tab.
- CI artifacts — Download the
trivy-scan-resultsartifact from any CI run to inspect the raw SARIF output.
Data Sources
| Source | Data |
|---|---|
| yfinance | Stock prices, fundamentals |
| Finnhub | Real-time quotes, news |
| FRED | Macro economic data |
| CCXT | Crypto exchange data |
| Alpaca | Broker integration (optional) |