20 KiB
TradingAgents: Technical Debt & Architectural Improvements
Modernization & Code Quality Enhancements
🔧 TECHNICAL DEBT
1. Type Safety & Static Analysis
Priority: High Effort: 2-3 weeks Impact: Reduces bugs, improves maintainability
Current Issues:
- Limited type hints throughout codebase
- No mypy or pyright validation
- Dynamic typing makes refactoring risky
Solution:
# tradingagents/types.py
"""Comprehensive type definitions for TradingAgents."""
from typing import TypedDict, Literal, Protocol
from decimal import Decimal
from datetime import datetime
# Type aliases
Ticker = str
Signal = Literal["BUY", "SELL", "HOLD"]
Timestamp = str # ISO format
# Structured types
class StockData(TypedDict):
"""Stock price data structure."""
open: Decimal
high: Decimal
low: Decimal
close: Decimal
volume: int
timestamp: datetime
class AnalystReport(TypedDict):
"""Analyst report structure."""
analyst_type: Literal["market", "fundamentals", "news", "social"]
ticker: Ticker
date: str
analysis: str
confidence: float
recommendation: Signal
reasoning: str
class TradingDecision(TypedDict):
"""Final trading decision structure."""
ticker: Ticker
signal: Signal
confidence: float
timestamp: Timestamp
analyst_reports: dict[str, AnalystReport]
risk_assessment: str
position_size: Decimal
# Protocol for data vendors
class DataVendor(Protocol):
"""Interface for data vendors."""
def get_stock_data(
self,
ticker: Ticker,
start_date: str,
end_date: str
) -> list[StockData]:
"""Fetch historical stock data."""
...
def get_fundamentals(
self,
ticker: Ticker
) -> dict[str, any]:
"""Fetch fundamental data."""
...
# Refactor with types
def propagate(
self,
ticker: Ticker,
date: str
) -> tuple[dict[str, any], Signal]:
"""
Run TradingAgents analysis with full type safety.
Args:
ticker: Stock symbol (e.g., "NVDA")
date: Analysis date in YYYY-MM-DD format
Returns:
Tuple of (full_state, signal)
Raises:
ValueError: If ticker or date format is invalid
APIError: If data fetching fails
"""
# Implementation with full type checking
Validation Setup:
# .github/workflows/type-check.yml
name: Type Check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install mypy
pip install -r requirements.txt
- name: Run mypy
run: mypy tradingagents/ --strict
2. Dependency Management
Priority: High Effort: 1 week Impact: Reproducible builds, security
Current Issues:
requirements.txtlacks version pinning- No dependency vulnerability scanning
- Missing dependency groups (dev, test, prod)
Solution:
# pyproject.toml
[project]
name = "tradingagents"
version = "1.0.0"
description = "Multi-Agent LLM Financial Trading Framework"
requires-python = ">=3.9"
dependencies = [
"langchain-openai>=0.1.0,<0.2.0",
"langchain-anthropic>=0.1.0,<0.2.0",
"langchain-google-genai>=1.0.0,<2.0.0",
"langgraph>=0.1.0,<0.2.0",
"pandas>=2.0.0,<3.0.0",
"yfinance>=0.2.0,<0.3.0",
"alpaca-py>=0.7.0,<0.8.0",
"chainlit>=1.0.0,<2.0.0",
"plotly>=5.0.0,<6.0.0",
"fastapi>=0.100.0,<0.101.0",
"uvicorn>=0.23.0,<0.24.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
"isort>=5.12.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
]
test = [
"pytest>=7.0.0",
"pytest-mock>=3.11.0",
"pytest-timeout>=2.1.0",
"freezegun>=1.2.0",
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.0.0",
"mkdocstrings[python]>=0.22.0",
]
[tool.black]
line-length = 100
target-version = ['py39', 'py310', 'py311']
[tool.isort]
profile = "black"
line_length = 100
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --cov=tradingagents --cov-report=html --cov-report=term"
Security Scanning:
# .github/workflows/security.yml
name: Security Scan
on:
push:
schedule:
- cron: '0 0 * * 0' # Weekly
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Snyk
uses: snyk/actions/python-3.9@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run Safety
run: |
pip install safety
safety check --json
- name: Run Bandit
run: |
pip install bandit
bandit -r tradingagents/ -ll
3. Configuration Management
Priority: Medium Effort: 1 week Impact: Flexibility, maintainability
Current Issues:
- Configuration scattered across files
- Hard to override for different environments
- No validation of config values
Solution:
# tradingagents/config/config.py
from pydantic import BaseSettings, Field, validator
from typing import Literal, Optional
from pathlib import Path
class DatabaseConfig(BaseSettings):
"""Database configuration."""
host: str = "localhost"
port: int = 5432
name: str = "tradingagents"
user: str = "postgres"
password: str = Field(..., env="DB_PASSWORD")
class Config:
env_prefix = "DB_"
class LLMConfig(BaseSettings):
"""LLM configuration."""
provider: Literal["openai", "anthropic", "google"] = "openai"
deep_think_model: str = "gpt-4o"
quick_think_model: str = "gpt-4o-mini"
temperature: float = Field(1.0, ge=0.0, le=2.0)
max_tokens: Optional[int] = Field(None, ge=1, le=100000)
@validator("provider")
def validate_provider(cls, v):
"""Ensure API key exists for provider."""
key_env = f"{v.upper()}_API_KEY"
if not os.getenv(key_env):
raise ValueError(f"{key_env} not set")
return v
class Config:
env_prefix = "LLM_"
class BrokerConfig(BaseSettings):
"""Broker configuration."""
type: Literal["alpaca", "ib", "mock"] = "alpaca"
paper_trading: bool = True
api_key: Optional[str] = Field(None, env="BROKER_API_KEY")
secret_key: Optional[str] = Field(None, env="BROKER_SECRET_KEY")
class Config:
env_prefix = "BROKER_"
class TradingConfig(BaseSettings):
"""Trading configuration."""
max_debate_rounds: int = Field(1, ge=0, le=5)
max_risk_discuss_rounds: int = Field(1, ge=0, le=5)
default_position_size: float = Field(0.1, ge=0.01, le=1.0)
risk_tolerance: Literal["conservative", "moderate", "aggressive"] = "moderate"
class Config:
env_prefix = "TRADING_"
class TradingAgentsConfig(BaseSettings):
"""Main configuration."""
# Paths
project_dir: Path = Path(__file__).parent.parent
data_dir: Path = Field(Path("./data"), env="TRADINGAGENTS_DATA_DIR")
results_dir: Path = Field(Path("./results"), env="TRADINGAGENTS_RESULTS_DIR")
# Sub-configs
llm: LLMConfig = Field(default_factory=LLMConfig)
broker: BrokerConfig = Field(default_factory=BrokerConfig)
trading: TradingConfig = Field(default_factory=TradingConfig)
database: Optional[DatabaseConfig] = None
# Environment
environment: Literal["development", "staging", "production"] = "development"
debug: bool = Field(False, env="DEBUG")
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
@validator("data_dir", "results_dir")
def create_directories(cls, v):
"""Ensure directories exist."""
v.mkdir(parents=True, exist_ok=True)
return v
# Usage
config = TradingAgentsConfig()
# Access nested config
print(f"Using {config.llm.provider} with {config.llm.deep_think_model}")
# Environment-specific configs
# development.env, staging.env, production.env
4. Error Handling & Resilience
Priority: High Effort: 2 weeks Impact: Reliability, user experience
Current Issues:
- Inconsistent error handling
- No retry logic for transient failures
- Poor error messages
Solution:
# tradingagents/resilience/retry.py
from functools import wraps
import time
from typing import Type, Tuple
import logging
logger = logging.getLogger(__name__)
def retry_with_backoff(
max_attempts: int = 3,
backoff_factor: float = 2.0,
exceptions: Tuple[Type[Exception], ...] = (Exception,),
on_retry: callable = None
):
"""
Retry decorator with exponential backoff.
Args:
max_attempts: Maximum number of retry attempts
backoff_factor: Multiplier for backoff delay
exceptions: Tuple of exceptions to catch and retry
on_retry: Callback function called on each retry
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempt = 1
delay = 1.0
while attempt <= max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
logger.error(
f"{func.__name__} failed after {max_attempts} attempts: {e}"
)
raise
logger.warning(
f"{func.__name__} failed (attempt {attempt}/{max_attempts}): {e}. "
f"Retrying in {delay}s..."
)
if on_retry:
on_retry(attempt, e)
time.sleep(delay)
delay *= backoff_factor
attempt += 1
return wrapper
return decorator
# Usage
@retry_with_backoff(
max_attempts=3,
backoff_factor=2.0,
exceptions=(APIError, ConnectionError, TimeoutError)
)
def get_stock_data(ticker: str, date: str) -> dict:
"""Fetch stock data with automatic retry."""
return api.fetch_data(ticker, date)
# Circuit breaker pattern
class CircuitBreaker:
"""Circuit breaker for external services."""
def __init__(
self,
failure_threshold: int = 5,
timeout: int = 60,
name: str = "service"
):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.name = name
self.failure_count = 0
self.last_failure_time = None
self.state = "closed" # closed, open, half-open
def call(self, func, *args, **kwargs):
"""Execute function with circuit breaker."""
if self.state == "open":
if self._should_attempt_reset():
self.state = "half-open"
else:
raise CircuitBreakerOpenError(
f"Circuit breaker is OPEN for {self.name}"
)
try:
result = func(*args, **kwargs)
# Success - reset if half-open
if self.state == "half-open":
self.state = "closed"
self.failure_count = 0
logger.info(f"Circuit breaker CLOSED for {self.name}")
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
logger.error(
f"Circuit breaker OPENED for {self.name} "
f"after {self.failure_count} failures"
)
raise
def _should_attempt_reset(self) -> bool:
"""Check if enough time has passed to attempt reset."""
return (
self.last_failure_time and
time.time() - self.last_failure_time >= self.timeout
)
# Usage
alpaca_breaker = CircuitBreaker(name="alpaca_api", failure_threshold=5)
def get_account_info():
return alpaca_breaker.call(broker.get_account)
5. Testing Infrastructure
Priority: High Effort: 2-3 weeks Impact: Quality, confidence
Current Issues:
- Test coverage gaps
- No integration tests
- Slow test suite
- No test fixtures for LLM responses
Solution:
# tests/conftest.py
import pytest
from unittest.mock import Mock, patch
from decimal import Decimal
from tradingagents.graph.trading_graph import TradingAgentsGraph
@pytest.fixture
def mock_llm():
"""Mock LLM for testing."""
llm = Mock()
llm.invoke.return_value = Mock(
content="BUY signal with 85% confidence. Strong fundamentals..."
)
return llm
@pytest.fixture
def mock_broker():
"""Mock broker for testing."""
broker = Mock()
broker.get_account.return_value = BrokerAccount(
account_number="TEST123",
cash=Decimal("100000.00"),
buying_power=Decimal("200000.00"),
portfolio_value=Decimal("100000.00"),
equity=Decimal("100000.00"),
last_equity=Decimal("100000.00"),
multiplier=Decimal("2"),
)
return broker
@pytest.fixture
def sample_stock_data():
"""Sample stock data for testing."""
return {
"AAPL": pd.DataFrame({
"open": [150.0, 151.0, 152.0],
"high": [152.0, 153.0, 154.0],
"low": [149.0, 150.0, 151.0],
"close": [151.0, 152.0, 153.0],
"volume": [1000000, 1100000, 1200000]
})
}
@pytest.fixture
def trading_graph(mock_llm):
"""TradingAgents graph with mocked LLM."""
with patch('tradingagents.llm_factory.LLMFactory.create_llm', return_value=mock_llm):
ta = TradingAgentsGraph(
selected_analysts=["market"],
debug=True
)
yield ta
# Integration tests
# tests/integration/test_full_workflow.py
@pytest.mark.integration
@pytest.mark.slow
def test_full_trading_workflow(trading_graph, mock_broker):
"""Test complete trading workflow."""
# 1. Analyze
_, signal = trading_graph.propagate("AAPL", "2024-05-10")
assert signal in ["BUY", "SELL", "HOLD"]
# 2. Execute
if signal == "BUY":
order = mock_broker.buy_market("AAPL", Decimal("10"))
assert order.status == OrderStatus.SUBMITTED
# 3. Track
positions = mock_broker.get_positions()
assert any(p.symbol == "AAPL" for p in positions)
# Performance tests
@pytest.mark.benchmark
def test_propagate_performance(benchmark, trading_graph):
"""Benchmark propagate performance."""
result = benchmark(
trading_graph.propagate,
"AAPL",
"2024-05-10"
)
# Should complete in < 30 seconds
assert benchmark.stats["mean"] < 30.0
# Property-based testing
from hypothesis import given, strategies as st
@given(
ticker=st.text(min_size=1, max_size=5, alphabet=st.characters(whitelist_categories=('Lu',))),
quantity=st.decimals(min_value=1, max_value=1000)
)
def test_order_creation_properties(ticker, quantity):
"""Property-based test for order creation."""
order = MarketOrder(ticker, quantity)
assert order.symbol == ticker
assert order.quantity == quantity
assert order.order_type == OrderType.MARKET
CI/CD Integration:
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
- name: Install dependencies
run: |
pip install -e ".[dev,test]"
- name: Run unit tests
run: pytest tests/unit -v --cov --cov-report=xml
- name: Run integration tests
run: pytest tests/integration -v --slow
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
6. Documentation
Priority: Medium Effort: 2 weeks Impact: Onboarding, maintenance
Solution:
# Set up MkDocs
# mkdocs.yml
site_name: TradingAgents Documentation
theme:
name: material
features:
- navigation.tabs
- navigation.sections
- search.suggest
- search.highlight
- content.code.copy
nav:
- Home: index.md
- Getting Started:
- Installation: getting-started/installation.md
- Quick Start: getting-started/quickstart.md
- Configuration: getting-started/configuration.md
- User Guide:
- Analysis: guide/analysis.md
- Trading: guide/trading.md
- Portfolio: guide/portfolio.md
- Backtesting: guide/backtesting.md
- API Reference:
- TradingAgentsGraph: api/trading-graph.md
- Portfolio: api/portfolio.md
- Brokers: api/brokers.md
- Advanced:
- Custom Strategies: advanced/strategies.md
- LLM Configuration: advanced/llm.md
- Production Deployment: advanced/production.md
- Contributing:
- Development Guide: contributing/development.md
- Architecture: contributing/architecture.md
- Testing: contributing/testing.md
plugins:
- search
- mkdocstrings:
handlers:
python:
options:
show_source: true
show_root_heading: true
# Auto-generate API docs from docstrings
# docs/api/trading-graph.md
::: tradingagents.graph.trading_graph.TradingAgentsGraph
options:
show_root_heading: true
show_source: true
🏗️ ARCHITECTURAL IMPROVEMENTS
1. Event-Driven Architecture
Current: Synchronous, blocking operations Proposed: Async, event-driven
# tradingagents/events/bus.py
from typing import Callable, List
import asyncio
class EventBus:
"""Central event bus for loosely coupled components."""
def __init__(self):
self.subscribers: dict[str, List[Callable]] = {}
def subscribe(self, event_type: str, handler: Callable):
"""Subscribe to event type."""
if event_type not in self.subscribers:
self.subscribers[event_type] = []
self.subscribers[event_type].append(handler)
async def publish(self, event_type: str, data: dict):
"""Publish event to all subscribers."""
if event_type in self.subscribers:
tasks = [
handler(data)
for handler in self.subscribers[event_type]
]
await asyncio.gather(*tasks)
# Usage
event_bus = EventBus()
# Subscribe
async def on_signal_generated(data):
"""Handle signal generation."""
logger.info(f"Signal generated: {data['signal']} for {data['ticker']}")
await alert_manager.notify(data)
event_bus.subscribe("signal_generated", on_signal_generated)
# Publish
await event_bus.publish("signal_generated", {
"ticker": "NVDA",
"signal": "BUY",
"confidence": 0.85
})
2. Microservices Architecture
Current: Monolithic Proposed: Decomposed services
Services:
- Analysis Service (TradingAgents core)
- Data Service (market data)
- Execution Service (order management)
- Portfolio Service (position tracking)
- Notification Service (alerts)
- API Gateway (unified interface)
📋 Technical Debt Summary
| Area | Priority | Effort | Impact | ROI |
|---|---|---|---|---|
| Type Safety | High | 2-3 weeks | High | ⭐⭐⭐⭐⭐ |
| Dependencies | High | 1 week | High | ⭐⭐⭐⭐⭐ |
| Configuration | Medium | 1 week | Medium | ⭐⭐⭐⭐ |
| Error Handling | High | 2 weeks | High | ⭐⭐⭐⭐⭐ |
| Testing | High | 2-3 weeks | Very High | ⭐⭐⭐⭐⭐ |
| Documentation | Medium | 2 weeks | High | ⭐⭐⭐⭐ |
Total Effort: 10-13 weeks (2.5-3 months)
Expected Benefits:
- 50% fewer production bugs
- 80% faster onboarding
- 3x easier refactoring
- 90% test coverage
- Professional codebase quality
See also: STRATEGIC_IMPROVEMENTS.md, MEDIUM_TERM_ENHANCEMENTS.md, STRATEGIC_INITIATIVES.md