TradingAgents/TECHNICAL_DEBT.md

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.txt lacks 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