Merge pull request #69 from aguzererler/copilot/review-financial-tools-implementation

Financial tools analysis doc and fix YoY revenue growth off-by-one
This commit is contained in:
ahmet guzererler 2026-03-21 22:41:55 +01:00 committed by GitHub
commit 442b38dff4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1433 additions and 1 deletions

View File

@ -38,6 +38,7 @@ from tradingagents.graph.scanner_graph import ScannerGraph
from cli.announcements import fetch_announcements, display_announcements
from cli.stats_handler import StatsCallbackHandler
from tradingagents.observability import RunLogger, set_run_logger
from tradingagents.api_usage import format_vendor_breakdown, format_av_assessment
console = Console()
@ -1212,12 +1213,17 @@ def run_analysis():
log_dir.mkdir(parents=True, exist_ok=True)
run_logger.write_log(log_dir / "run_log.jsonl")
summary = run_logger.summary()
vendor_breakdown = format_vendor_breakdown(summary)
av_assessment = format_av_assessment(summary)
console.print(
f"[dim]LLM calls: {summary['llm_calls']} | "
f"Tokens: {summary['tokens_in']}{summary['tokens_out']} | "
f"Tools: {summary['tool_calls']} | "
f"Vendor calls: {summary['vendor_success']}ok/{summary['vendor_fail']}fail[/dim]"
)
if vendor_breakdown:
console.print(f"[dim] Vendors: {vendor_breakdown}[/dim]")
console.print(f"[dim] {av_assessment}[/dim]")
set_run_logger(None)
# Prompt to display full report
@ -1295,12 +1301,17 @@ def run_scan(date: Optional[str] = None):
# Write observability log
run_logger.write_log(save_dir / "run_log.jsonl")
scan_summary = run_logger.summary()
vendor_breakdown = format_vendor_breakdown(scan_summary)
av_assessment = format_av_assessment(scan_summary)
console.print(
f"[dim]LLM calls: {scan_summary['llm_calls']} | "
f"Tokens: {scan_summary['tokens_in']}{scan_summary['tokens_out']} | "
f"Tools: {scan_summary['tool_calls']} | "
f"Vendor calls: {scan_summary['vendor_success']}ok/{scan_summary['vendor_fail']}fail[/dim]"
)
if vendor_breakdown:
console.print(f"[dim] Vendors: {vendor_breakdown}[/dim]")
console.print(f"[dim] {av_assessment}[/dim]")
set_run_logger(None)
# Append to daily digest and sync to NotebookLM
@ -1419,12 +1430,17 @@ def run_pipeline(
output_dir.mkdir(parents=True, exist_ok=True)
run_logger.write_log(output_dir / "run_log.jsonl")
pipe_summary = run_logger.summary()
vendor_breakdown = format_vendor_breakdown(pipe_summary)
av_assessment = format_av_assessment(pipe_summary)
console.print(
f"[dim]LLM calls: {pipe_summary['llm_calls']} | "
f"Tokens: {pipe_summary['tokens_in']}{pipe_summary['tokens_out']} | "
f"Tools: {pipe_summary['tool_calls']} | "
f"Vendor calls: {pipe_summary['vendor_success']}ok/{pipe_summary['vendor_fail']}fail[/dim]"
)
if vendor_breakdown:
console.print(f"[dim] Vendors: {vendor_breakdown}[/dim]")
console.print(f"[dim] {av_assessment}[/dim]")
set_run_logger(None)
# Append to daily digest and sync to NotebookLM
@ -1591,5 +1607,65 @@ def auto(
run_portfolio(portfolio_id, date, macro_path)
@app.command(name="estimate-api")
def estimate_api(
command: str = typer.Argument("all", help="Command to estimate: analyze, scan, pipeline, or all"),
num_tickers: int = typer.Option(5, "--tickers", "-t", help="Expected tickers for pipeline estimate"),
num_indicators: int = typer.Option(6, "--indicators", "-i", help="Expected indicator calls per ticker"),
):
"""Estimate API usage per vendor (helps decide if AV premium is needed)."""
from tradingagents.api_usage import (
estimate_analyze,
estimate_scan,
estimate_pipeline,
format_estimate,
AV_FREE_DAILY_LIMIT,
AV_PREMIUM_PER_MINUTE,
)
console.print(Panel("[bold green]API Usage Estimation[/bold green]", border_style="green"))
console.print(
f"[dim]Alpha Vantage tiers: FREE = {AV_FREE_DAILY_LIMIT} calls/day | "
f"Premium ($30/mo) = {AV_PREMIUM_PER_MINUTE} calls/min, unlimited daily[/dim]\n"
)
estimates = []
if command in ("analyze", "all"):
estimates.append(estimate_analyze(num_indicators=num_indicators))
if command in ("scan", "all"):
estimates.append(estimate_scan())
if command in ("pipeline", "all"):
estimates.append(estimate_pipeline(num_tickers=num_tickers, num_indicators=num_indicators))
if not estimates:
console.print(f"[red]Unknown command: {command}. Use: analyze, scan, pipeline, or all[/red]")
raise typer.Exit(1)
for est in estimates:
console.print(Panel(format_estimate(est), title=est.command, border_style="cyan"))
# Overall AV assessment
console.print("\n[bold]Alpha Vantage Subscription Recommendation:[/bold]")
max_av = max(e.vendor_calls.alpha_vantage for e in estimates)
if max_av == 0:
console.print(
" [green]✓ Current config uses yfinance (free) for all data.[/green]\n"
" [green] Alpha Vantage subscription is NOT needed.[/green]\n"
" [dim] To switch to AV, set TRADINGAGENTS_VENDOR_* env vars to 'alpha_vantage'.[/dim]"
)
else:
total_daily = sum(e.vendor_calls.alpha_vantage for e in estimates)
if total_daily <= AV_FREE_DAILY_LIMIT:
console.print(
f" [green]✓ Total AV calls ({total_daily}) fit the FREE tier ({AV_FREE_DAILY_LIMIT}/day).[/green]\n"
f" [green] No premium subscription needed for a single daily run.[/green]"
)
else:
console.print(
f" [yellow]⚠ Total AV calls ({total_daily}) exceed the FREE tier ({AV_FREE_DAILY_LIMIT}/day).[/yellow]\n"
f" [yellow] Premium subscription recommended ($30/month).[/yellow]"
)
if __name__ == "__main__":
app()

View File

@ -0,0 +1,616 @@
# Financial Tools & Indicators — Comprehensive Analysis
> **Scope**: All technical-indicator, fundamental, and risk implementations in
> `tradingagents/dataflows/` and `tradingagents/portfolio/risk_metrics.py`.
>
> **Perspective**: Dual review — Quantitative Economist × Senior Software Developer.
---
## Table of Contents
1. [Implementation Accuracy](#1-implementation-accuracy)
2. [Library Assessment](#2-library-assessment)
3. [The Alpha Vantage Debate](#3-the-alpha-vantage-debate)
4. [Data Flow & API Mapping](#4-data-flow--api-mapping)
---
## 1. Implementation Accuracy
### 1.1 Technical Indicators (stockstats via yfinance)
| Indicator | Key | Library | Mathematically Correct? | Notes |
|-----------|-----|---------|------------------------|-------|
| 50-day SMA | `close_50_sma` | stockstats | ✅ Yes | Standard arithmetic rolling mean of closing prices over 50 periods. |
| 200-day SMA | `close_200_sma` | stockstats | ✅ Yes | Same as above over 200 periods. |
| 10-day EMA | `close_10_ema` | stockstats | ✅ Yes | Recursive EMA: `EMA_t = α·P_t + (1-α)·EMA_{t-1}`, `α = 2/(n+1)`. stockstats implements the standard Wilder/exponential formula. |
| MACD | `macd` | stockstats | ✅ Yes | Difference of 12-period and 26-period EMAs. |
| MACD Signal | `macds` | stockstats | ✅ Yes | 9-period EMA of the MACD line. |
| MACD Histogram | `macdh` | stockstats | ✅ Yes | MACD line minus Signal line. |
| RSI (14) | `rsi` | stockstats | ✅ Yes | Wilder's RSI: `100 - 100/(1 + avg_gain/avg_loss)`. Uses EMA smoothing of gains/losses (Wilder's method, which is the industry standard). |
| Bollinger Middle | `boll` | stockstats | ✅ Yes | 20-period SMA of close. |
| Bollinger Upper | `boll_ub` | stockstats | ✅ Yes | Middle + 2 × rolling standard deviation. |
| Bollinger Lower | `boll_lb` | stockstats | ✅ Yes | Middle 2 × rolling standard deviation. |
| ATR (14) | `atr` | stockstats | ✅ Yes | Wilder's smoothed average of True Range: `max(H-L, |H-C_prev|, |L-C_prev|)`. |
| VWMA | `vwma` | stockstats | ✅ Yes | Volume-weighted moving average: `Σ(P_i × V_i) / Σ(V_i)`. Only available via the yfinance/stockstats vendor (not Alpha Vantage or Finnhub). |
| MFI | `mfi` | stockstats | ✅ Yes | Money Flow Index: volume-weighted RSI variant. yfinance-only. |
**Verdict**: All technical indicators delegate to the `stockstats` library, which
implements the canonical formulas (Wilder RSI, standard EMA, Bollinger 2σ, etc.).
No custom re-implementations exist for these indicators — the code is a thin data-fetching
and formatting layer around stockstats.
### 1.2 Alpha Vantage Indicators
The Alpha Vantage vendor (`alpha_vantage_indicator.py`) calls the Alpha Vantage REST API
endpoints directly (e.g., `SMA`, `EMA`, `MACD`, `RSI`, `BBANDS`, `ATR`). These endpoints
return pre-computed indicator values. The app does **no local calculation** — it fetches
CSV data, parses it, and filters by date range.
| Aspect | Assessment |
|--------|-----------|
| API call mapping | ✅ Correct — each indicator maps to the right AV function. |
| CSV parsing | ✅ Correct — column name mapping (`COL_NAME_MAP`) accurately targets the right CSV column for each indicator. |
| Date filtering | ✅ Correct — filters results to the `[before, curr_date]` window. |
| VWMA handling | ⚠️ Known limitation — returns an informative message since Alpha Vantage has no VWMA endpoint. Documented in code (line 157160). |
### 1.3 Finnhub Indicators
The Finnhub vendor (`finnhub_indicators.py`) calls the `/indicator` endpoint with
Unix-timestamp date ranges. It handles multi-value indicators (MACD: 3 values per row;
BBANDS: 3 values per row) and single-value indicators correctly.
| Aspect | Assessment |
|--------|-----------|
| Timestamp conversion | ✅ Correct — adds 86 400s to end date to ensure inclusive. |
| Multi-value formatting | ✅ Correct — MACD returns macd + signal + histogram; BBANDS returns upper + middle + lower. |
| Error handling | ✅ Raises `FinnhubError` on empty/no_data responses. |
| Output format | ✅ Mirrors Alpha Vantage output style for downstream agent consistency. |
### 1.4 Portfolio Risk Metrics (`risk_metrics.py`)
All computed in **pure Python** (stdlib `math` only — no pandas/numpy dependency).
| Metric | Formula | Correct? | Notes |
|--------|---------|----------|-------|
| Sharpe Ratio | `(μ / σ) × √252` | ✅ Yes | Annualised, risk-free rate = 0. Uses sample std (ddof=1). |
| Sortino Ratio | `(μ / σ_down) × √252` | ✅ Yes | Denominator uses only negative returns. Correct minimum of 2 downside observations. |
| 95% VaR | `-percentile(returns, 5)` | ✅ Yes | Historical simulation — 5th percentile with linear interpolation. Expressed as positive loss fraction. |
| Max Drawdown | peak-to-trough | ✅ Yes | Walks NAV series tracking running peak. Returns most negative (worst) drawdown. |
| Beta | `Cov(r_p, r_b) / Var(r_b)` | ✅ Yes | Correctly uses sample covariance (n1 denominator). |
| Sector Concentration | `holdings_value / total_value × 100` | ✅ Yes | From the most-recent snapshot's `holdings_snapshot`. |
### 1.5 Macro Regime Classifier (`macro_regime.py`)
Uses 6 market signals to classify: risk-on / transition / risk-off.
| Signal | Data Source | Method | Correct? |
|--------|------------|--------|----------|
| VIX level | `^VIX` via yfinance | `< 16 → risk-on, > 25 → risk-off` | ✅ Standard thresholds from CBOE VIX interpretation guides. |
| VIX trend | `^VIX` 5-SMA vs 20-SMA | Rising VIX (SMA5 > SMA20) → risk-off | ✅ Standard crossover approach. |
| Credit spread | HYG/LQD ratio | 1-month change of HY-bond / IG-bond ratio | ✅ Well-established proxy for credit spread changes. |
| Yield curve | TLT/SHY ratio | TLT outperformance → flight to safety | ✅ TLT (20yr) vs SHY (1-3yr) is a standard duration proxy. |
| Market breadth | `^GSPC` vs 200-SMA | SPX above/below 200-SMA | ✅ Classic breadth indicator used by institutional investors. |
| Sector rotation | Defensive vs Cyclical ETFs | 1-month return spread (XLU/XLP/XLV vs XLY/XLK/XLI) | ✅ Correct sector classification; standard rotation analysis. |
**Custom calculations**: The `_sma()` and `_pct_change_n()` helpers are simple 5-line
implementations. They are mathematically correct and use pandas `rolling().mean()`.
No need to replace with a library — the overhead would outweigh the benefit.
### 1.6 TTM Analysis (`ttm_analysis.py`)
Computes trailing twelve months metrics by summing the last 4 quarterly income-statement
flow items and using the latest balance-sheet stock items. Handles transposed CSV layouts
(Alpha Vantage vs yfinance) via auto-detection.
| Metric | Correct? | Notes |
|--------|----------|-------|
| TTM Revenue | ✅ | Sum of last 4 quarterly revenues. |
| Margin calculations | ✅ | Gross/operating/net margins = profit / revenue × 100. |
| ROE | ✅ | TTM net income / latest equity × 100. |
| Debt/Equity | ✅ | Latest total debt / latest equity. |
| Revenue QoQ | ✅ | `(latest - previous) / |previous| × 100`. |
| Revenue YoY | ✅ | Compares latest quarter to 4 quarters prior (`quarterly[-5]`). |
| Margin trend | ✅ | Classifies last 3 values as expanding/contracting/stable. |
### 1.7 Peer Comparison (`peer_comparison.py`)
| Aspect | Assessment |
|--------|-----------|
| Return calculation | ✅ `(current - base) / base × 100` for 1W/1M/3M/6M/YTD horizons using trading-day counts (5, 21, 63, 126). |
| Alpha calculation | ✅ Stock return minus ETF return per period. |
| Sector mapping | ✅ 11 GICS sectors mapped to SPDR ETFs. Yahoo Finance sector names normalised correctly. |
| Batch download | ✅ Single `yf.download()` call for all symbols (efficient). |
---
## 2. Library Assessment
### 2.1 Current Library Stack
| Library | Version | Role | Industry Standard? |
|---------|---------|------|-------------------|
| **stockstats** | ≥ 0.6.5 | Technical indicator computation (SMA, EMA, MACD, RSI, BBANDS, ATR, VWMA, MFI) | ⚠️ Moderate — well-known in Python quant community but not as widely used as TA-Lib or pandas-ta. ~1.3K GitHub stars. |
| **yfinance** | ≥ 0.2.63 | Market data fetching (OHLCV, fundamentals, news) | ✅ De facto standard for free Yahoo Finance access. ~14K GitHub stars. |
| **pandas** | ≥ 2.3.0 | Data manipulation, CSV parsing, rolling calculations | ✅ Industry standard. Used by virtually all quantitative Python workflows. |
| **requests** | ≥ 2.32.4 | HTTP API calls to Alpha Vantage and Finnhub | ✅ Industry standard for HTTP in Python. |
### 2.2 Alternative Libraries Considered
| Alternative | What It Provides | Pros | Cons |
|-------------|-----------------|------|------|
| **TA-Lib** (via `ta-lib` Python wrapper) | 200+ indicators, C-based performance | ✅ Gold standard in quant finance<br>✅ Extremely fast (C implementation)<br>✅ Widest indicator coverage | ❌ Requires C library system install (complex CI/CD)<br>❌ No pip-only install<br>❌ Platform-specific build issues |
| **pandas-ta** | 130+ indicators, pure Python/pandas | ✅ Pure Python — pip install only<br>✅ Active maintenance<br>✅ Direct pandas DataFrame integration | ⚠️ Slightly slower than TA-Lib<br>⚠️ Larger dependency footprint |
| **tulipy** | Technical indicators, C-based | ✅ Fast (C implementation)<br>✅ Simple API | ❌ Requires C build<br>❌ Less maintained than TA-Lib |
### 2.3 Recommendation: Keep stockstats
**Current choice is appropriate** for this application. Here's why:
1. **Indicators are consumed by LLMs, not HFT engines**: The indicators are formatted
as text strings for LLM agents. The performance difference between stockstats and
TA-Lib is irrelevant at this scale (single-ticker, daily data, <15 years of history).
2. **Pure Python install**: stockstats requires only pip — no C library builds.
This simplifies CI/CD, Docker images, and contributor onboarding significantly.
3. **Sufficient coverage**: All indicators used by the trading agents (SMA, EMA, MACD,
RSI, Bollinger Bands, ATR, VWMA, MFI) are covered by stockstats.
4. **Mathematical correctness**: stockstats implements the canonical formulas (verified
above). The results will match TA-Lib and pandas-ta to within floating-point precision.
5. **Migration cost**: Switching to pandas-ta or TA-Lib would require changes to
`stockstats_utils.py`, `y_finance.py`, and all tests — with no user-visible benefit.
**When to reconsider**: If the project adds high-frequency backtesting (thousands of
tickers × minute data), TA-Lib's C performance would become relevant.
---
## 3. The Alpha Vantage Debate
### 3.1 Available Indicators via Alpha Vantage API
All indicators used by TradingAgents are available as **pre-computed endpoints** from
the Alpha Vantage Technical Indicators API:
| Indicator | AV Endpoint | Available? |
|-----------|------------|-----------|
| SMA | `function=SMA` | ✅ |
| EMA | `function=EMA` | ✅ |
| MACD | `function=MACD` | ✅ (returns MACD, Signal, Histogram) |
| RSI | `function=RSI` | ✅ |
| Bollinger Bands | `function=BBANDS` | ✅ (returns upper, middle, lower) |
| ATR | `function=ATR` | ✅ |
| VWMA | — | ❌ Not available |
| MFI | `function=MFI` | ✅ (but not currently mapped in our AV adapter) |
### 3.2 Comparative Analysis
| Dimension | Local Calculation (stockstats + yfinance) | Alpha Vantage API (pre-computed) |
|-----------|------------------------------------------|----------------------------------|
| **Cost** | Free (yfinance) | 75 calls/min premium; 25/day free tier. Each indicator = 1 API call. A full analysis (12 indicators × 1 ticker) consumes 12 calls. |
| **Latency** | ~12s for initial data fetch + <100ms for indicator computation | ~0.51s per API call × 12 indicators = 612s total |
| **Rate Limits** | No API rate limits from yfinance (though Yahoo may throttle aggressive use) | Strict rate limits. Premium tier: 75 calls/min. Free tier: 25 calls/day. |
| **Indicator Coverage** | Full: any indicator stockstats supports (200+ including VWMA, MFI) | Limited to Alpha Vantage's supported functions. No VWMA. |
| **Data Freshness** | Real-time — downloads latest OHLCV data then computes | Real-time — Alpha Vantage computes on their latest data |
| **Reproducibility** | Full control — same input data + code = exact same result. Can version-control parameters. | Black box — AV may change smoothing methods, seed values, or data adjustments without notice. |
| **Customisation** | Full — change period, smoothing, add custom indicators | Limited to AV's parameter set per endpoint |
| **Offline/Testing** | Cacheable — OHLCV data can be cached locally for offline dev and testing | Requires live API calls (no offline mode without caching raw responses) |
| **Accuracy** | Depends on stockstats implementation (verified correct above) | Presumably correct — Alpha Vantage is a major data vendor |
| **Multi-ticker Efficiency** | One yf.download call for many tickers, then compute all indicators locally | Separate API call per ticker × per indicator |
### 3.3 Verdict: Local Calculation (Primary) with API as Fallback
The current architecture — **yfinance + stockstats as primary, Alpha Vantage as fallback
vendor** — is the correct design for these reasons:
1. **Cost efficiency**: A single analysis run needs 12+ indicators. At the free AV tier
(25 calls/day), this exhausts the quota on 2 tickers. Local computation is unlimited.
2. **Latency**: A single yfinance download + local stockstats computation is 510×
faster than 12 sequential Alpha Vantage API calls with rate limiting.
3. **Coverage**: VWMA and MFI are not available from Alpha Vantage. Local computation
is the only option for these indicators.
4. **Testability**: Local computation can be unit-tested with synthetic data and cached
OHLCV files. API-based indicators require live network access or complex mocking.
5. **Fallback value**: Alpha Vantage's pre-computed indicators serve as an independent
verification and as a fallback when yfinance is unavailable (e.g., Yahoo Finance
outages or API changes). The vendor routing system in `interface.py` already supports
this.
The Alpha Vantage vendor is **not a wasted implementation** — it provides resilience
and cross-validation capability. However, it should remain the secondary vendor.
---
## 4. Data Flow & API Mapping
### 4.1 Technical Indicators Tool
**Agent-Facing Tool**: `get_indicators(symbol, indicator, curr_date, look_back_days)`
in `tradingagents/agents/utils/technical_indicators_tools.py`
#### yfinance Vendor (Primary)
```
Agent → get_indicators() tool
→ route_to_vendor("get_indicators", ...)
→ get_stock_stats_indicators_window() [y_finance.py]
→ _get_stock_stats_bulk() [y_finance.py]
→ yf.download(symbol, 15yr range) [External: Yahoo Finance API]
→ _clean_dataframe() [stockstats_utils.py]
→ stockstats.wrap(data) [Library: stockstats]
→ df[indicator] # triggers calculation
→ format as date: value string
→ return formatted indicator report to agent
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Yahoo Finance via `yfinance` library |
| **Calculation** | `stockstats` library — wraps OHLCV DataFrame, indicator access triggers lazy computation |
| **Caching** | CSV file cache in `data_cache_dir` (15-year OHLCV per symbol) |
| **External API** | Yahoo Finance (via yfinance `download()`) — 1 call per symbol |
#### Alpha Vantage Vendor (Fallback)
```
Agent → get_indicators() tool
→ route_to_vendor("get_indicators", ...)
→ get_indicator() [alpha_vantage_indicator.py]
→ _fetch_indicator_data()
→ _make_api_request("SMA"|"EMA"|...) [External: Alpha Vantage API]
→ _parse_indicator_data() # CSV parsing + date filtering
→ return formatted indicator report to agent
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Alpha Vantage REST API |
| **Calculation** | Pre-computed by Alpha Vantage — no local calculation |
| **Caching** | None (live API call per request) |
| **External API** | Alpha Vantage `https://www.alphavantage.co/query` — 1 call per indicator |
#### Finnhub Vendor
```
Agent → (not routed by default — only if vendor="finnhub" configured)
→ get_indicator_finnhub() [finnhub_indicators.py]
→ _make_api_request("indicator", ...) [External: Finnhub API]
→ parse JSON response (parallel lists: timestamps + values)
→ return formatted indicator report
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Finnhub REST API `/indicator` endpoint |
| **Calculation** | Pre-computed by Finnhub — no local calculation |
| **Caching** | None |
| **External API** | Finnhub `https://finnhub.io/api/v1/indicator` — 1 call per indicator |
**Supported Indicators by Vendor**:
| Indicator | yfinance (stockstats) | Alpha Vantage | Finnhub |
|-----------|:---:|:---:|:---:|
| SMA (50, 200) | ✅ | ✅ | ✅ |
| EMA (10) | ✅ | ✅ | ✅ |
| MACD / Signal / Histogram | ✅ | ✅ | ✅ |
| RSI | ✅ | ✅ | ✅ |
| Bollinger Bands (upper/middle/lower) | ✅ | ✅ | ✅ |
| ATR | ✅ | ✅ | ✅ |
| VWMA | ✅ | ❌ | ❌ |
| MFI | ✅ | ❌ (endpoint exists but unmapped) | ❌ |
---
### 4.2 Fundamental Data Tools
**Agent-Facing Tools**: `get_fundamentals`, `get_balance_sheet`, `get_cashflow`,
`get_income_statement` in `tradingagents/agents/utils/fundamental_data_tools.py`
#### yfinance Vendor (Primary)
```
Agent → get_fundamentals() tool
→ route_to_vendor("get_fundamentals", ...)
→ get_fundamentals() [y_finance.py]
→ yf.Ticker(ticker).info [External: Yahoo Finance API]
→ extract 27 key-value fields
→ return formatted fundamentals report
```
```
Agent → get_balance_sheet() / get_cashflow() / get_income_statement()
→ route_to_vendor(...)
→ yf.Ticker(ticker).quarterly_balance_sheet / quarterly_cashflow / quarterly_income_stmt
[External: Yahoo Finance API]
→ DataFrame.to_csv()
→ return CSV string with header
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Yahoo Finance via `yfinance` library |
| **Calculation** | No calculation — raw financial statement data |
| **External APIs** | Yahoo Finance (1 API call per statement) |
#### Alpha Vantage Vendor (Fallback)
```
Agent → get_balance_sheet() / get_cashflow() / get_income_statement()
→ route_to_vendor(...)
→ _make_api_request("BALANCE_SHEET" | "CASH_FLOW" | "INCOME_STATEMENT")
[External: Alpha Vantage API]
→ CSV parsing
→ return CSV string
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Alpha Vantage REST API |
| **Calculation** | No calculation — pre-computed by Alpha Vantage |
| **External APIs** | Alpha Vantage (1 call per statement) |
---
### 4.3 TTM Analysis Tool
**Agent-Facing Tool**: `get_ttm_analysis(ticker, curr_date)`
in `tradingagents/agents/utils/fundamental_data_tools.py`
```
Agent → get_ttm_analysis() tool
→ route_to_vendor("get_income_statement", ticker, "quarterly") [1 vendor call]
→ route_to_vendor("get_balance_sheet", ticker, "quarterly") [1 vendor call]
→ route_to_vendor("get_cashflow", ticker, "quarterly") [1 vendor call]
→ compute_ttm_metrics(income_csv, balance_csv, cashflow_csv) [ttm_analysis.py]
→ _parse_financial_csv() × 3 # auto-detect AV vs yfinance layout
→ sum last 4 quarters (flow items)
→ latest value (stock items)
→ compute margins, ROE, D/E
→ compute QoQ/YoY revenue growth
→ classify margin trends
→ format_ttm_report(metrics, ticker)
→ return Markdown report
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | 3 quarterly financial statements via configured vendor |
| **Calculation** | Local: TTM summation, margin ratios, growth rates, trend classification |
| **Internal Requests** | 3 `route_to_vendor()` calls for financial statements |
| **External APIs** | Yahoo Finance (3 calls) or Alpha Vantage (3 calls), depending on vendor config |
---
### 4.4 Peer Comparison Tool
**Agent-Facing Tool**: `get_peer_comparison(ticker, curr_date)`
in `tradingagents/agents/utils/fundamental_data_tools.py`
```
Agent → get_peer_comparison() tool
→ get_peer_comparison_report(ticker) [peer_comparison.py]
→ get_sector_peers(ticker)
→ yf.Ticker(ticker).info [External: Yahoo Finance]
→ map sector → _SECTOR_TICKERS list
→ compute_relative_performance(ticker, sector_key, peers)
→ yf.download([ticker, ...peers, ETF]) [External: Yahoo Finance — 1 batch call]
→ _safe_pct() for 1W/1M/3M/6M horizons
→ _ytd_pct() for YTD
→ rank by 3-month return
→ compute alpha vs sector ETF
→ return Markdown peer ranking table
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Yahoo Finance for OHLCV prices (6-month history) |
| **Calculation** | Local: percentage returns, ranking, alpha computation |
| **Internal Requests** | 1 ticker info lookup + 1 batch price download |
| **External APIs** | Yahoo Finance (2 calls: `.info` + `download()`) |
---
### 4.5 Sector Relative Tool
**Agent-Facing Tool**: `get_sector_relative(ticker, curr_date)`
```
Agent → get_sector_relative() tool
→ get_sector_relative_report(ticker) [peer_comparison.py]
→ get_sector_peers(ticker)
→ yf.Ticker(ticker).info [External: Yahoo Finance]
→ yf.download([ticker, sector_ETF]) [External: Yahoo Finance — 1 call]
→ _safe_pct() for 1W/1M/3M/6M
→ compute alpha per period
→ return Markdown comparison table
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Yahoo Finance for ticker + sector ETF prices |
| **Calculation** | Local: return percentages, alpha = stock return ETF return |
| **External APIs** | Yahoo Finance (2 calls: `.info` + `download()`) |
---
### 4.6 Macro Regime Tool
**Agent-Facing Tool**: `get_macro_regime(curr_date)`
in `tradingagents/agents/utils/fundamental_data_tools.py`
```
Agent → get_macro_regime() tool
→ classify_macro_regime() [macro_regime.py]
→ _fetch_macro_data()
→ yf.download(["^VIX"], period="3mo") [External: Yahoo Finance]
→ yf.download(["^GSPC"], period="14mo") [External: Yahoo Finance]
→ yf.download(["HYG", "LQD"], period="3mo") [External: Yahoo Finance]
→ yf.download(["TLT", "SHY"], period="3mo") [External: Yahoo Finance]
→ yf.download([def_ETFs + cyc_ETFs], period="3mo") [External: Yahoo Finance]
→ _evaluate_signals()
→ _signal_vix_level() # threshold check
→ _signal_vix_trend() # SMA5 vs SMA20 crossover
→ _signal_credit_spread() # HYG/LQD 1-month change
→ _signal_yield_curve() # TLT vs SHY performance spread
→ _signal_market_breadth() # SPX vs 200-SMA
→ _signal_sector_rotation() # defensive vs cyclical ETF spread
→ _determine_regime_and_confidence()
→ format_macro_report(regime_data)
→ return Markdown regime report
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Yahoo Finance for VIX, S&P 500, bond ETFs, sector ETFs |
| **Calculation** | Local: 6 signal evaluators with custom thresholds. Simple helper functions `_sma()`, `_pct_change_n()`. |
| **Internal Requests** | 5 batch `yf.download()` calls |
| **External APIs** | Yahoo Finance only (5 calls, batched by symbol group) |
---
### 4.7 Core Stock Data Tool
**Agent-Facing Tool**: `get_stock_data(symbol, start_date, end_date)`
in `tradingagents/agents/utils/core_stock_tools.py`
#### yfinance Vendor (Primary)
```
Agent → get_stock_data() tool
→ route_to_vendor("get_stock_data", ...)
→ get_YFin_data_online() [y_finance.py]
→ yf.Ticker(symbol).history(...) [External: Yahoo Finance]
→ round numerics, format CSV
→ return CSV string
```
#### Alpha Vantage Vendor (Fallback)
```
Agent → get_stock_data() tool
→ route_to_vendor("get_stock_data", ...)
→ get_stock() [alpha_vantage_stock.py]
→ _make_api_request("TIME_SERIES_DAILY_ADJUSTED")
[External: Alpha Vantage]
→ return CSV string
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Yahoo Finance (primary) or Alpha Vantage (fallback) |
| **Calculation** | None — raw OHLCV data |
| **External APIs** | Yahoo Finance or Alpha Vantage (1 call) |
---
### 4.8 News Data Tools
**Agent-Facing Tools**: `get_news`, `get_global_news`, `get_insider_transactions`
in `tradingagents/agents/utils/news_data_tools.py`
| Tool | Primary Vendor | Fallback | External API Sequence |
|------|---------------|----------|----------------------|
| `get_news(ticker, ...)` | yfinance | Alpha Vantage | 1. `yf.Ticker(ticker).news` → Yahoo Finance |
| `get_global_news(...)` | yfinance | Alpha Vantage | 1. `yf.Search("market").news` → Yahoo Finance |
| `get_insider_transactions(ticker)` | **Finnhub** | Alpha Vantage, yfinance | 1. Finnhub `/stock/insider-transactions` API |
---
### 4.9 Scanner Data Tools
**Agent-Facing Tools**: `get_market_movers`, `get_market_indices`, `get_sector_performance`,
`get_industry_performance`, `get_topic_news`
in `tradingagents/agents/utils/scanner_tools.py`
| Tool | Primary Vendor | External API Sequence |
|------|---------------|----------------------|
| `get_market_movers(category)` | yfinance | 1. `yf.Screener()` → Yahoo Finance |
| `get_market_indices()` | yfinance | 1. `yf.download(["^GSPC","^DJI",...])` → Yahoo Finance |
| `get_sector_performance()` | yfinance | 1. `yf.Sector(key)` → Yahoo Finance (per sector) |
| `get_industry_performance(sector)` | yfinance | 1. `yf.Industry(key)` → Yahoo Finance (per industry) |
| `get_topic_news(topic)` | yfinance | 1. `yf.Search(topic).news` → Yahoo Finance |
---
### 4.10 Calendar Tools (Finnhub Only)
**Agent-Facing Tools**: `get_earnings_calendar`, `get_economic_calendar`
| Tool | Vendor | External API |
|------|--------|-------------|
| `get_earnings_calendar(from, to)` | Finnhub (only) | Finnhub `/calendar/earnings` |
| `get_economic_calendar(from, to)` | Finnhub (only) | Finnhub `/calendar/economic` (FOMC, CPI, NFP, GDP, PPI) |
---
### 4.11 Portfolio Risk Metrics
**Agent-Facing Tool**: `compute_portfolio_risk_metrics()`
in `tradingagents/agents/utils/portfolio_tools.py`
```
Agent → compute_portfolio_risk_metrics() tool
→ compute_risk_metrics(snapshots, benchmark_returns) [risk_metrics.py]
→ _daily_returns(nav_series) # NAV → daily % changes
→ Sharpe: μ/σ × √252
→ Sortino: μ/σ_down × √252
→ VaR: -percentile(returns, 5)
→ Max drawdown: peak-to-trough walk
→ Beta: Cov(r_p, r_b) / Var(r_b)
→ Sector concentration from holdings
→ return JSON metrics dict
```
| Attribute | Detail |
|-----------|--------|
| **Data Source** | Portfolio snapshots from Supabase database |
| **Calculation** | 100% local — pure Python `math` module, no external dependencies |
| **External APIs** | None — operates entirely on stored portfolio data |
---
### 4.12 Vendor Routing Architecture
All data tool calls flow through `route_to_vendor()` in `tradingagents/dataflows/interface.py`:
```
@tool function (agents/utils/*_tools.py)
→ route_to_vendor(method_name, *args, **kwargs)
→ get_category_for_method(method_name) # lookup in TOOLS_CATEGORIES
→ get_vendor(category, method_name) # check config: tool_vendors → data_vendors
→ try primary vendor implementation
→ if FALLBACK_ALLOWED and primary fails:
try remaining vendors in order
→ if all fail: raise RuntimeError
```
**Fallback-Allowed Methods** (cross-vendor fallback is safe for these):
- `get_stock_data` — OHLCV data is fungible
- `get_market_indices` — index quotes are fungible
- `get_sector_performance` — ETF-based, same approach
- `get_market_movers` — approximation acceptable for screening
- `get_industry_performance` — ETF-based proxy
**Fail-Fast Methods** (no fallback — data contracts differ between vendors):
- `get_indicators`, `get_fundamentals`, `get_balance_sheet`, `get_cashflow`,
`get_income_statement`, `get_news`, `get_global_news`, `get_insider_transactions`,
`get_topic_news`, `get_earnings_calendar`, `get_economic_calendar`
---
## Summary
| Area | Verdict |
|------|---------|
| **Implementation accuracy** | ✅ All indicators and metrics are mathematically correct. No custom re-implementations of standard indicators — stockstats handles the math. |
| **Library choice** | ✅ stockstats is appropriate for this use case (LLM-consumed daily indicators). TA-Lib would add build complexity with no user-visible benefit. |
| **Alpha Vantage role** | ✅ Correctly positioned as fallback vendor. Local computation is faster, cheaper, and covers more indicators. |
| **Data flow architecture** | ✅ Clean vendor routing with configurable primary/fallback. Each tool has a clear data source → calculation → formatting pipeline. |

View File

@ -0,0 +1,288 @@
"""Tests for tradingagents/api_usage.py — API consumption estimation."""
import pytest
from tradingagents.api_usage import (
AV_FREE_DAILY_LIMIT,
AV_PREMIUM_PER_MINUTE,
UsageEstimate,
VendorEstimate,
estimate_analyze,
estimate_pipeline,
estimate_scan,
format_av_assessment,
format_estimate,
format_vendor_breakdown,
)
# ──────────────────────────────────────────────────────────────────────────────
# VendorEstimate
# ──────────────────────────────────────────────────────────────────────────────
class TestVendorEstimate:
def test_total(self):
ve = VendorEstimate(yfinance=10, alpha_vantage=5, finnhub=2)
assert ve.total == 17
def test_default_zeros(self):
ve = VendorEstimate()
assert ve.total == 0
# ──────────────────────────────────────────────────────────────────────────────
# UsageEstimate
# ──────────────────────────────────────────────────────────────────────────────
class TestUsageEstimate:
def test_av_fits_free_tier_true(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=10),
)
assert est.av_fits_free_tier() is True
def test_av_fits_free_tier_false(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=100),
)
assert est.av_fits_free_tier() is False
def test_av_daily_runs_free(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=5),
)
assert est.av_daily_runs_free() == AV_FREE_DAILY_LIMIT // 5
def test_av_daily_runs_free_zero_av(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=0),
)
assert est.av_daily_runs_free() == -1 # unlimited
# ──────────────────────────────────────────────────────────────────────────────
# estimate_analyze — default config (yfinance primary)
# ──────────────────────────────────────────────────────────────────────────────
class TestEstimateAnalyze:
def test_default_config_no_av_calls(self):
"""With default config (yfinance primary), AV calls should be 0."""
est = estimate_analyze()
assert est.vendor_calls.alpha_vantage == 0
assert est.vendor_calls.yfinance > 0
def test_all_analysts_nonzero_total(self):
est = estimate_analyze(selected_analysts=["market", "news", "fundamentals", "social"])
assert est.vendor_calls.total > 0
def test_market_only(self):
est = estimate_analyze(selected_analysts=["market"], num_indicators=4)
# 1 stock data + 4 indicators = 5 calls
assert est.vendor_calls.total >= 5
def test_fundamentals_includes_insider(self):
"""Fundamentals analyst should include insider_transactions (Finnhub default)."""
est = estimate_analyze(selected_analysts=["fundamentals"])
# insider_transactions defaults to finnhub
assert est.vendor_calls.finnhub >= 1
def test_num_indicators_varies_total(self):
est_low = estimate_analyze(selected_analysts=["market"], num_indicators=2)
est_high = estimate_analyze(selected_analysts=["market"], num_indicators=8)
assert est_high.vendor_calls.total > est_low.vendor_calls.total
def test_av_config_counts_av_calls(self):
"""When AV is configured as primary, calls should show up under alpha_vantage."""
av_config = {
"data_vendors": {
"core_stock_apis": "alpha_vantage",
"technical_indicators": "alpha_vantage",
"fundamental_data": "alpha_vantage",
"news_data": "alpha_vantage",
"scanner_data": "alpha_vantage",
"calendar_data": "finnhub",
},
"tool_vendors": {
"get_insider_transactions": "alpha_vantage",
},
}
est = estimate_analyze(config=av_config, selected_analysts=["market", "fundamentals"])
assert est.vendor_calls.alpha_vantage > 0
assert est.vendor_calls.yfinance == 0
def test_method_breakdown_has_entries(self):
est = estimate_analyze(selected_analysts=["market"])
assert len(est.method_breakdown) > 0
def test_notes_populated(self):
est = estimate_analyze()
assert len(est.notes) > 0
# ──────────────────────────────────────────────────────────────────────────────
# estimate_scan — default config (yfinance primary)
# ──────────────────────────────────────────────────────────────────────────────
class TestEstimateScan:
def test_default_config_uses_yfinance(self):
est = estimate_scan()
assert est.vendor_calls.yfinance > 0
def test_finnhub_for_calendars(self):
"""Calendars should always use Finnhub."""
est = estimate_scan()
assert est.vendor_calls.finnhub >= 2 # earnings + economic calendar
def test_scan_total_reasonable(self):
est = estimate_scan()
# Should be between 15-40 calls total
assert 10 <= est.vendor_calls.total <= 50
def test_notes_have_phases(self):
est = estimate_scan()
phase_notes = [n for n in est.notes if "Phase" in n]
assert len(phase_notes) >= 3 # Phase 1A, 1B, 1C, 2, 3
# ──────────────────────────────────────────────────────────────────────────────
# estimate_pipeline
# ──────────────────────────────────────────────────────────────────────────────
class TestEstimatePipeline:
def test_pipeline_larger_than_scan(self):
scan_est = estimate_scan()
pipe_est = estimate_pipeline(num_tickers=3)
assert pipe_est.vendor_calls.total > scan_est.vendor_calls.total
def test_pipeline_scales_with_tickers(self):
est3 = estimate_pipeline(num_tickers=3)
est7 = estimate_pipeline(num_tickers=7)
assert est7.vendor_calls.total > est3.vendor_calls.total
def test_pipeline_av_config(self):
"""Pipeline with AV config should report AV calls."""
av_config = {
"data_vendors": {
"core_stock_apis": "alpha_vantage",
"technical_indicators": "alpha_vantage",
"fundamental_data": "alpha_vantage",
"news_data": "alpha_vantage",
"scanner_data": "alpha_vantage",
"calendar_data": "finnhub",
},
"tool_vendors": {},
}
est = estimate_pipeline(config=av_config, num_tickers=5)
assert est.vendor_calls.alpha_vantage > 0
# ──────────────────────────────────────────────────────────────────────────────
# format_estimate
# ──────────────────────────────────────────────────────────────────────────────
class TestFormatEstimate:
def test_contains_vendor_counts(self):
est = estimate_analyze()
text = format_estimate(est)
assert "yfinance" in text
assert "Total:" in text
def test_no_av_shows_not_needed(self):
est = estimate_analyze() # default config → no AV
text = format_estimate(est)
assert "NOT needed" in text
def test_av_shows_assessment(self):
av_config = {
"data_vendors": {
"core_stock_apis": "alpha_vantage",
"technical_indicators": "alpha_vantage",
"fundamental_data": "alpha_vantage",
"news_data": "alpha_vantage",
"scanner_data": "alpha_vantage",
"calendar_data": "finnhub",
},
"tool_vendors": {},
}
est = estimate_analyze(config=av_config)
text = format_estimate(est)
assert "Alpha Vantage" in text
# ──────────────────────────────────────────────────────────────────────────────
# format_vendor_breakdown (actual run data)
# ──────────────────────────────────────────────────────────────────────────────
class TestFormatVendorBreakdown:
def test_empty_summary(self):
assert format_vendor_breakdown({}) == ""
def test_yfinance_only(self):
summary = {"vendors_used": {"yfinance": {"ok": 10, "fail": 0}}}
text = format_vendor_breakdown(summary)
assert "yfinance:10ok/0fail" in text
def test_multiple_vendors(self):
summary = {
"vendors_used": {
"yfinance": {"ok": 8, "fail": 1},
"alpha_vantage": {"ok": 3, "fail": 0},
"finnhub": {"ok": 2, "fail": 0},
}
}
text = format_vendor_breakdown(summary)
assert "yfinance:8ok/1fail" in text
assert "AV:3ok/0fail" in text
assert "Finnhub:2ok/0fail" in text
# ──────────────────────────────────────────────────────────────────────────────
# format_av_assessment (actual run data)
# ──────────────────────────────────────────────────────────────────────────────
class TestFormatAvAssessment:
def test_no_av_used(self):
summary = {"vendors_used": {"yfinance": {"ok": 10, "fail": 0}}}
text = format_av_assessment(summary)
assert "not used" in text
def test_av_within_free(self):
summary = {"vendors_used": {"alpha_vantage": {"ok": 5, "fail": 0}}}
text = format_av_assessment(summary)
assert "free tier" in text
assert "5 calls" in text
def test_av_exceeds_free(self):
summary = {"vendors_used": {"alpha_vantage": {"ok": 30, "fail": 0}}}
text = format_av_assessment(summary)
assert "exceeds" in text
assert "Premium" in text
# ──────────────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────────────
class TestConstants:
def test_av_free_daily_limit(self):
assert AV_FREE_DAILY_LIMIT == 25
def test_av_premium_per_minute(self):
assert AV_PREMIUM_PER_MINUTE == 75

View File

@ -169,6 +169,36 @@ class TestComputeTTMMetrics:
assert qoq is not None
assert abs(qoq - 5.0) < 0.5
def test_revenue_yoy_is_four_quarters_back(self):
"""YoY growth must compare latest quarter to the quarter 4 periods earlier."""
result = self.compute(
_make_income_csv(8), _make_balance_csv(8), _make_cashflow_csv(8)
)
yoy = result["trends"]["revenue_yoy_pct"]
assert yoy is not None
# With 5% QoQ compounding, YoY = 1.05^4 - 1 ≈ 21.55%
expected_yoy = ((1.05 ** 4) - 1) * 100
assert abs(yoy - expected_yoy) < 0.5
def test_revenue_yoy_with_exactly_5_quarters(self):
"""YoY is available when exactly 5 quarters exist (minimum for 4-quarter lookback)."""
result = self.compute(
_make_income_csv(5), _make_balance_csv(5), _make_cashflow_csv(5)
)
yoy = result["trends"]["revenue_yoy_pct"]
assert yoy is not None
# quarterly[-5] vs quarterly[-1] with 5% QoQ → 1.05^4 - 1 ≈ 21.55%
expected_yoy = ((1.05 ** 4) - 1) * 100
assert abs(yoy - expected_yoy) < 0.5
def test_revenue_yoy_none_with_4_quarters(self):
"""YoY should be None when fewer than 5 quarters are available."""
result = self.compute(
_make_income_csv(4), _make_balance_csv(4), _make_cashflow_csv(4)
)
yoy = result["trends"]["revenue_yoy_pct"]
assert yoy is None
def test_margin_trend_expanding(self):
"""Expanding margin should be detected."""
# Create data where net margin expands over time

412
tradingagents/api_usage.py Normal file
View File

@ -0,0 +1,412 @@
"""API consumption estimation for TradingAgents.
Provides static estimates of how many external API calls each command
(analyze, scan, pipeline) will make, broken down by vendor. This helps
users decide whether they need an Alpha Vantage premium subscription.
Alpha Vantage tiers
-------------------
- **Free**: 25 API calls per day
- **Premium (30 $/month)**: 75 calls per minute, unlimited daily
Each ``get_*`` method that hits Alpha Vantage counts as **1 API call**,
regardless of how much data is returned.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
# ──────────────────────────────────────────────────────────────────────────────
# Alpha Vantage tier limits
# ──────────────────────────────────────────────────────────────────────────────
AV_FREE_DAILY_LIMIT = 25
AV_PREMIUM_PER_MINUTE = 75
# ──────────────────────────────────────────────────────────────────────────────
# Per-method AV call cost.
# When Alpha Vantage is the vendor, each invocation of a route_to_vendor
# method triggers exactly one AV HTTP request — except get_indicators,
# which the LLM may call multiple times (once per indicator).
# ──────────────────────────────────────────────────────────────────────────────
_AV_CALLS_PER_METHOD: dict[str, int] = {
"get_stock_data": 1, # TIME_SERIES_DAILY_ADJUSTED
"get_indicators": 1, # SMA / EMA / RSI / MACD / BBANDS / ATR (1 call each)
"get_fundamentals": 1, # OVERVIEW
"get_balance_sheet": 1, # BALANCE_SHEET
"get_cashflow": 1, # CASH_FLOW
"get_income_statement": 1, # INCOME_STATEMENT
"get_news": 1, # NEWS_SENTIMENT
"get_global_news": 1, # NEWS_SENTIMENT (no ticker)
"get_insider_transactions": 1, # INSIDER_TRANSACTIONS
"get_market_movers": 1, # TOP_GAINERS_LOSERS
"get_market_indices": 1, # multiple quote calls
"get_sector_performance": 1, # SECTOR
"get_industry_performance": 1, # sector ETF lookup
"get_topic_news": 1, # NEWS_SENTIMENT (topic filter)
}
@dataclass
class VendorEstimate:
"""Estimated API call counts per vendor for a single operation."""
yfinance: int = 0
alpha_vantage: int = 0
finnhub: int = 0
@property
def total(self) -> int:
return self.yfinance + self.alpha_vantage + self.finnhub
@dataclass
class UsageEstimate:
"""Full API usage estimate for a command."""
command: str
description: str
vendor_calls: VendorEstimate = field(default_factory=VendorEstimate)
# Breakdown of calls by method → count (only for non-zero vendors)
method_breakdown: dict[str, dict[str, int]] = field(default_factory=dict)
notes: list[str] = field(default_factory=list)
def av_fits_free_tier(self) -> bool:
"""Whether the Alpha Vantage calls fit within the free daily limit."""
return self.vendor_calls.alpha_vantage <= AV_FREE_DAILY_LIMIT
def av_daily_runs_free(self) -> int:
"""How many times this command can run per day on the free AV tier."""
if self.vendor_calls.alpha_vantage == 0:
return -1 # unlimited (doesn't use AV)
return AV_FREE_DAILY_LIMIT // self.vendor_calls.alpha_vantage
# ──────────────────────────────────────────────────────────────────────────────
# Estimators for each command type
# ──────────────────────────────────────────────────────────────────────────────
def _resolve_vendor(config: dict, method: str) -> str:
"""Determine which vendor a method will use given the config."""
from tradingagents.dataflows.interface import (
TOOLS_CATEGORIES,
VENDOR_METHODS,
get_category_for_method,
)
# Tool-level override first
tool_vendors = config.get("tool_vendors", {})
if method in tool_vendors:
return tool_vendors[method]
# Category-level
try:
category = get_category_for_method(method)
except ValueError:
# Method not in any category — may be a new/unknown method.
# Return "unknown" so estimation can continue gracefully.
import logging
logging.getLogger(__name__).debug(
"Method %r not found in TOOLS_CATEGORIES — skipping vendor resolution", method
)
return "unknown"
return config.get("data_vendors", {}).get(category, "yfinance")
def estimate_analyze(
config: dict | None = None,
selected_analysts: list[str] | None = None,
num_indicators: int = 6,
) -> UsageEstimate:
"""Estimate API calls for a single stock analysis.
Args:
config: TradingAgents config dict (uses DEFAULT_CONFIG if None).
selected_analysts: Which analysts are enabled.
Defaults to ``["market", "social", "news", "fundamentals"]``.
num_indicators: Expected number of indicator calls from the market
analyst (LLM decides, but 4-8 is typical).
Returns:
:class:`UsageEstimate` with per-vendor breakdowns.
"""
if config is None:
from tradingagents.default_config import DEFAULT_CONFIG
config = DEFAULT_CONFIG
if selected_analysts is None:
selected_analysts = ["market", "social", "news", "fundamentals"]
est = UsageEstimate(
command="analyze",
description="Single stock analysis",
)
breakdown: dict[str, dict[str, int]] = {}
def _add(method: str, count: int = 1) -> None:
vendor = _resolve_vendor(config, method)
if vendor == "yfinance":
est.vendor_calls.yfinance += count
elif vendor == "alpha_vantage":
est.vendor_calls.alpha_vantage += count
elif vendor == "finnhub":
est.vendor_calls.finnhub += count
# Track breakdown
if vendor not in breakdown:
breakdown[vendor] = {}
breakdown[vendor][method] = breakdown[vendor].get(method, 0) + count
# Market Analyst
if "market" in selected_analysts:
_add("get_stock_data")
for _ in range(num_indicators):
_add("get_indicators")
est.notes.append(
f"Market analyst: 1 stock data + ~{num_indicators} indicator calls "
f"(LLM chooses which indicators; actual count may vary)"
)
# Fundamentals Analyst
if "fundamentals" in selected_analysts:
_add("get_fundamentals")
_add("get_income_statement")
_add("get_balance_sheet")
_add("get_cashflow")
_add("get_insider_transactions")
est.notes.append(
"Fundamentals analyst: overview + 3 financial statements + insider transactions"
)
# News Analyst
if "news" in selected_analysts:
_add("get_news")
_add("get_global_news")
est.notes.append("News analyst: ticker news + global news")
# Social Media Analyst (uses same news tools)
if "social" in selected_analysts:
_add("get_news")
est.notes.append("Social analyst: ticker news/sentiment")
est.method_breakdown = breakdown
return est
def estimate_scan(config: dict | None = None) -> UsageEstimate:
"""Estimate API calls for a market-wide scan.
Args:
config: TradingAgents config dict (uses DEFAULT_CONFIG if None).
Returns:
:class:`UsageEstimate` with per-vendor breakdowns.
"""
if config is None:
from tradingagents.default_config import DEFAULT_CONFIG
config = DEFAULT_CONFIG
est = UsageEstimate(
command="scan",
description="Market-wide macro scan (3 phases)",
)
breakdown: dict[str, dict[str, int]] = {}
def _add(method: str, count: int = 1) -> None:
vendor = _resolve_vendor(config, method)
if vendor == "yfinance":
est.vendor_calls.yfinance += count
elif vendor == "alpha_vantage":
est.vendor_calls.alpha_vantage += count
elif vendor == "finnhub":
est.vendor_calls.finnhub += count
if vendor not in breakdown:
breakdown[vendor] = {}
breakdown[vendor][method] = breakdown[vendor].get(method, 0) + count
# Phase 1A: Geopolitical Scanner — ~4 topic news calls
topic_news_calls = 4
for _ in range(topic_news_calls):
_add("get_topic_news")
est.notes.append(f"Phase 1A (Geopolitical): ~{topic_news_calls} topic news calls")
# Phase 1B: Market Movers Scanner — 3 market_movers + 1 indices
_add("get_market_movers", 3)
_add("get_market_indices")
est.notes.append("Phase 1B (Market Movers): 3 screener calls + 1 indices call")
# Phase 1C: Sector Scanner — 1 sector performance
_add("get_sector_performance")
est.notes.append("Phase 1C (Sector): 1 sector performance call")
# Phase 2: Industry Deep Dive — ~3 industry perf + ~3 topic news
industry_calls = 3
_add("get_industry_performance", industry_calls)
_add("get_topic_news", industry_calls)
est.notes.append(
f"Phase 2 (Industry Deep Dive): ~{industry_calls} industry perf + "
f"~{industry_calls} topic news calls"
)
# Phase 3: Macro Synthesis — ~2 topic news + calendars
_add("get_topic_news", 2)
_add("get_earnings_calendar")
_add("get_economic_calendar")
est.notes.append("Phase 3 (Macro Synthesis): ~2 topic news + calendar calls")
est.method_breakdown = breakdown
return est
def estimate_pipeline(
config: dict | None = None,
num_tickers: int = 5,
selected_analysts: list[str] | None = None,
num_indicators: int = 6,
) -> UsageEstimate:
"""Estimate API calls for a full pipeline (scan → filter → analyze).
Args:
config: TradingAgents config dict.
num_tickers: Expected number of tickers after filtering (typically 3-7).
selected_analysts: Analysts for each ticker analysis.
num_indicators: Expected indicator calls per ticker.
Returns:
:class:`UsageEstimate` with per-vendor breakdowns.
"""
scan_est = estimate_scan(config)
analyze_est = estimate_analyze(config, selected_analysts, num_indicators)
est = UsageEstimate(
command="pipeline",
description=f"Full pipeline: scan + {num_tickers} ticker analyses",
)
# Scan phase
est.vendor_calls.yfinance += scan_est.vendor_calls.yfinance
est.vendor_calls.alpha_vantage += scan_est.vendor_calls.alpha_vantage
est.vendor_calls.finnhub += scan_est.vendor_calls.finnhub
# Analyze phase × num_tickers
est.vendor_calls.yfinance += analyze_est.vendor_calls.yfinance * num_tickers
est.vendor_calls.alpha_vantage += analyze_est.vendor_calls.alpha_vantage * num_tickers
est.vendor_calls.finnhub += analyze_est.vendor_calls.finnhub * num_tickers
# Merge breakdowns
merged: dict[str, dict[str, int]] = {}
for vendor, methods in scan_est.method_breakdown.items():
merged.setdefault(vendor, {})
for method, count in methods.items():
merged[vendor][method] = merged[vendor].get(method, 0) + count
for vendor, methods in analyze_est.method_breakdown.items():
merged.setdefault(vendor, {})
for method, count in methods.items():
merged[vendor][method] = merged[vendor].get(method, 0) + count * num_tickers
est.method_breakdown = merged
est.notes.append(f"Scan phase: {scan_est.vendor_calls.total} calls")
est.notes.append(
f"Analyze phase: {analyze_est.vendor_calls.total} calls × {num_tickers} tickers "
f"= {analyze_est.vendor_calls.total * num_tickers} calls"
)
return est
# ──────────────────────────────────────────────────────────────────────────────
# Formatting helpers
# ──────────────────────────────────────────────────────────────────────────────
def format_estimate(est: UsageEstimate) -> str:
"""Format an estimate as a human-readable multi-line string."""
lines = [
f"API Usage Estimate — {est.command}",
f" {est.description}",
"",
f" Vendor calls (estimated):",
]
vc = est.vendor_calls
if vc.yfinance:
lines.append(f" yfinance: {vc.yfinance:>4} calls (free, no key needed)")
if vc.alpha_vantage:
lines.append(f" Alpha Vantage: {vc.alpha_vantage:>3} calls (free tier: {AV_FREE_DAILY_LIMIT}/day)")
if vc.finnhub:
lines.append(f" Finnhub: {vc.finnhub:>3} calls (free tier: 60/min)")
lines.append(f" Total: {vc.total:>4} vendor API calls")
# Alpha Vantage assessment
if vc.alpha_vantage > 0:
lines.append("")
lines.append(" Alpha Vantage Assessment:")
if est.av_fits_free_tier():
daily_runs = est.av_daily_runs_free()
lines.append(
f" ✓ Fits FREE tier ({vc.alpha_vantage}/{AV_FREE_DAILY_LIMIT} daily calls). "
f"~{daily_runs} run(s)/day possible."
)
else:
lines.append(
f" ✗ Exceeds FREE tier ({vc.alpha_vantage} calls > {AV_FREE_DAILY_LIMIT}/day limit). "
f"Premium required ($30/month → {AV_PREMIUM_PER_MINUTE}/min)."
)
else:
lines.append("")
lines.append(
" Alpha Vantage Assessment:"
)
lines.append(
" ✓ No Alpha Vantage calls — AV subscription NOT needed with current config."
)
return "\n".join(lines)
def format_vendor_breakdown(summary: dict) -> str:
"""Format a RunLogger summary dict into a per-vendor breakdown string.
This is called *after* a run completes, using the actual (not estimated)
vendor call counts from ``RunLogger.summary()``.
"""
vendors_used = summary.get("vendors_used", {})
if not vendors_used:
return ""
parts: list[str] = []
for vendor in ("yfinance", "alpha_vantage", "finnhub"):
counts = vendors_used.get(vendor)
if counts:
ok = counts.get("ok", 0)
fail = counts.get("fail", 0)
label = {
"yfinance": "yfinance",
"alpha_vantage": "AV",
"finnhub": "Finnhub",
}.get(vendor, vendor)
parts.append(f"{label}:{ok}ok/{fail}fail")
return " | ".join(parts) if parts else ""
def format_av_assessment(summary: dict) -> str:
"""Return a one-line Alpha Vantage assessment from actual run data."""
vendors_used = summary.get("vendors_used", {})
av = vendors_used.get("alpha_vantage")
if not av:
return "AV: not used (no subscription needed with current config)"
av_total = av.get("ok", 0) + av.get("fail", 0)
if av_total <= AV_FREE_DAILY_LIMIT:
daily_runs = AV_FREE_DAILY_LIMIT // max(av_total, 1)
return (
f"AV: {av_total} calls — fits free tier "
f"({AV_FREE_DAILY_LIMIT}/day, ~{daily_runs} runs/day)"
)
return (
f"AV: {av_total} calls — exceeds free tier! "
f"Premium needed ($30/mo → {AV_PREMIUM_PER_MINUTE}/min)"
)

View File

@ -305,7 +305,7 @@ def compute_ttm_metrics(
if n >= 2:
latest_rev = quarterly[-1]["revenue"]
prev_rev = quarterly[-2]["revenue"]
yoy_rev = quarterly[-4]["revenue"] if n >= 5 else None
yoy_rev = quarterly[-5]["revenue"] if n >= 5 else None
result["trends"] = {
"revenue_qoq_pct": _pct_change(latest_rev, prev_rev),

View File

@ -153,6 +153,15 @@ class RunLogger:
else:
vendor_counts[v]["fail"] += 1
# Group vendor calls by vendor → method for detailed breakdown
vendor_methods: dict[str, dict[str, int]] = {}
for e in vendor_events:
v = e.data["vendor"]
m = e.data.get("method", "unknown")
if v not in vendor_methods:
vendor_methods[v] = {}
vendor_methods[v][m] = vendor_methods[v].get(m, 0) + 1
return {
"elapsed_s": round(time.time() - self._start, 1),
"llm_calls": len(llm_events),
@ -167,6 +176,7 @@ class RunLogger:
"vendor_success": vendor_ok,
"vendor_fail": vendor_fail,
"vendors_used": vendor_counts,
"vendor_methods": vendor_methods,
}
def write_log(self, path: Path) -> None: