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:27bvia Ollama athttp://192.168.50.76:11434(local, free) - deep_think:
deepseek/deepseek-r1-0528via 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()catchesAlphaVantageError(base class) to trigger fallback, not justRateLimitError.- AV scanner functions raise
AlphaVantageErrorwhen 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 catchtradingagents/dataflows/alpha_vantage_scanner.py— raise on total failuretradingagents/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:
- Invoke chain
- If tool_calls present → execute tools → append ToolMessages → re-invoke
- Repeat up to
MAX_TOOL_ROUNDS=5until 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.