TradingAgents/DECISIONS.md

7.0 KiB

Architecture Decisions Log

Decision 001: Hybrid LLM Setup (Ollama + OpenRouter)

Date: 2026-03-17 Status: Implemented

Context: Need cost-effective LLM setup for scanner pipeline with different complexity tiers.

Decision: Use hybrid approach:

  • quick_think + mid_think: qwen3.5:27b via Ollama at http://192.168.50.76:11434 (local, free)
  • deep_think: deepseek/deepseek-r1-0528 via OpenRouter (cloud, paid)

Config location: tradingagents/default_config.py — per-tier _llm_provider and _backend_url keys.

Consequence: Removed top-level llm_provider and backend_url from config. Each tier must have its own {tier}_llm_provider set explicitly.


Decision 002: Data Vendor Fallback Strategy

Date: 2026-03-17 Status: Implemented

Context: Alpha Vantage free/demo key doesn't support ETF symbols and has strict rate limits. Need reliable data for scanner.

Decision:

  • route_to_vendor() catches AlphaVantageError (base class) to trigger fallback, not just RateLimitError.
  • AV scanner functions raise AlphaVantageError when ALL queries fail (not silently embedding errors in output strings).
  • yfinance is the fallback vendor and uses SPDR ETF proxies for sector performance instead of broken Sector.overview.

Files changed:

  • tradingagents/dataflows/interface.py — broadened catch
  • tradingagents/dataflows/alpha_vantage_scanner.py — raise on total failure
  • tradingagents/dataflows/yfinance_scanner.py — ETF proxy approach

Decision 003: yfinance Sector Performance via ETF Proxies

Date: 2026-03-17 Status: Implemented

Context: yfinance.Sector("technology").overview returns only metadata (companies_count, market_cap, etc.) — no performance data (oneDay, oneWeek, etc.).

Decision: Use SPDR sector ETFs as proxies:

sector_etfs = {
    "Technology": "XLK", "Healthcare": "XLV", "Financials": "XLF",
    "Energy": "XLE", "Consumer Discretionary": "XLY", ...
}

Download 6 months of history via yf.download() and compute 1-day, 1-week, 1-month, YTD percentage changes from closing prices.

File: tradingagents/dataflows/yfinance_scanner.py


Decision 004: Inline Tool Execution Loop for Scanner Agents

Date: 2026-03-17 Status: Implemented

Context: The existing trading graph uses separate ToolNode graph nodes for tool execution (agent → tool_node → agent routing loop). Scanner agents are simpler single-pass nodes — no ToolNode in the graph. When the LLM returned tool_calls, nobody executed them, resulting in empty reports.

Decision: Created tradingagents/agents/utils/tool_runner.py with run_tool_loop() that runs an inline tool execution loop within each scanner agent node:

  1. Invoke chain
  2. If tool_calls present → execute tools → append ToolMessages → re-invoke
  3. Repeat up to MAX_TOOL_ROUNDS=5 until LLM produces text response

Alternative considered: Adding ToolNode + conditional routing to scanner_setup.py (like trading graph). Rejected — too complex for the fan-out/fan-in pattern and would require 4 separate tool nodes with routing logic.

Files:

  • tradingagents/agents/utils/tool_runner.py (new)
  • All scanner agents updated to use run_tool_loop()

Decision 005: LangGraph State Reducers for Parallel Fan-Out

Date: 2026-03-17 Status: Implemented

Context: Phase 1 runs 3 scanners in parallel. All write to shared state fields (sender, etc.). LangGraph requires reducers for concurrent writes — otherwise raises INVALID_CONCURRENT_GRAPH_UPDATE.

Decision: Added _last_value reducer to all ScannerState fields via Annotated[str, _last_value].

File: tradingagents/agents/utils/scanner_states.py


Decision 006: CLI --date Flag for Scanner

Date: 2026-03-17 Status: Implemented

Context: python -m cli.main scan was interactive-only (prompts for date). Needed non-interactive invocation for testing/automation.

Decision: Added --date / -d option to scan command. Falls back to interactive prompt if not provided.

File: cli/main.py


Decision 008: Git Remote Strategy — origin = fork

Date: 2026-03-17 Status: Documented

Setup: There is only one configured remote:

origin → http://127.0.0.1:46699/git/aguzererler/TradingAgents  (the user's fork)

No upstream remote for the parent repo.

Rule: Always push feature branches to origin. Never push directly to main. PRs are created from claude/* branches on the fork via the Gitea web UI (no gh CLI available).


Decision 009: Medium-Term Upgrade — Macro Regime via yfinance Only

Date: 2026-03-17 Status: Implemented

Context: Macro regime classification needs VIX, credit spreads, yield curve, market breadth, sector rotation — all free signals.

Decision: Use yfinance exclusively for all macro regime signals (no Alpha Vantage endpoint for this data). 6 signals from ^VIX, ^GSPC, HYG, LQD, TLT, SHY, and sector ETFs. No vendor routing needed.

Scoring: Each signal ±1. Total ≥3 = risk-on, ≤-3 = risk-off, else transition. Confidence based on absolute score: |score| ≥4 → high, ≥2 → medium, else low.

File: tradingagents/dataflows/macro_regime.py


Decision 009: TTM Tool Prefers Alpha Vantage (More History)

Date: 2026-03-17 Status: Implemented

Context: yfinance returns only 4-5 quarterly periods. Alpha Vantage INCOME_STATEMENT endpoint returns up to 20 quarters. For 8-quarter trend analysis, AV is significantly better.

Decision: get_ttm_analysis tool uses route_to_vendor with AV as primary, yfinance as fallback. TTM module handles <8 quarters gracefully (computes with what's available, reports quarters_available).

File: tradingagents/dataflows/ttm_analysis.py


Decision 010: Peer Comparison via Hardcoded Sector Tickers

Date: 2026-03-17 Status: Implemented

Context: No Alpha Vantage endpoint for peer comparison. yfinance top_companies was unreliable (Mistake 3). Need deterministic peer lists.

Decision: Hardcode _SECTOR_TICKERS mapping (20 tickers per sector) and _SECTOR_ETFS in peer_comparison.py. Peer data via yf.download() — reliable and fast. Sector ETF used as benchmark for alpha calculation.

Trade-off: Peers don't auto-update if sector composition changes. Acceptable for current use case.

File: tradingagents/dataflows/peer_comparison.py


Decision 007: .env Loading Strategy

Date: 2026-03-17 Status: Implemented

Context: load_dotenv() loads from CWD. When running from a git worktree, the worktree .env may have placeholder values while the main repo .env has real keys.

Decision: cli/main.py calls load_dotenv() (CWD) then load_dotenv(Path(__file__).parent.parent / ".env") as fallback. The worktree .env was also updated with real API keys.

Note for future: If .env issues recur, check which .env file is being picked up. The worktree and main repo each have their own .env.