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:
commit
442b38dff4
76
cli/main.py
76
cli/main.py
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 157–160). |
|
||||
|
||||
### 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 (n−1 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** | ~1–2s for initial data fetch + <100ms for indicator computation | ~0.5–1s per API call × 12 indicators = 6–12s 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 5–10×
|
||||
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. |
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue