agents/quantitative/ — Quantitative Analysis Agents
Data-driven analysis of fundamentals, technicals, sentiment, and valuation
Four specialized agents that analyze different quantitative dimensions of a stock. Unlike the persona agents which interpret data through a philosophical lens, these agents focus on specific analytical domains with structured methodologies and threshold-based scoring.
📚 What is this & why does it exist?
While the persona agents bring qualitative judgment, the quantitative agents provide systematic, data-driven analysis. The fundamentals agent examines financial statements, the technicals agent analyzes price and volume patterns, the sentiment agent gauges market mood from insider trades and news, and the valuation agent calculates intrinsic value using DCF and comparables. Together they form a comprehensive quantitative foundation that the persona agents' qualitative insights build upon.
💡 Real-world analogy
These four agents are like the instruments in a medical checkup. The fundamentals agent is the blood test (financial health metrics — profitability, growth, debt levels). The technicals agent is the EKG (heart rhythm patterns mapped to price and volume patterns). The sentiment agent is the patient interview ("how are you feeling?" mapped to insider trading behavior and news mood). And the valuation agent is the diagnostic assessment (here's what we think you're worth based on multiple models). Each instrument measures something different, and the doctor (Portfolio Tracking & Performance Metrics" href="../../backtesting_doc/portfolio/index.html">portfolio manager) needs all of them to make a diagnosis.
🤔 Why Is It Designed This Way?
The quantitative agents use a fundamentally different approach from the persona agents. Instead of sending data to an LLM with a philosophical prompt, they compute signals algorithmically using threshold-based scoring and weighted ensemble methods. The fundamentals agent checks whether ROE exceeds 15%, the technicals agent calculates moving average crossovers, and the sentiment agent counts bullish vs. bearish insider trades. This deterministic approach makes the quantitative agents faster, cheaper (no LLM calls for most), and reproducible. The valuation agent is the exception — it uses four mathematical models (Owner Earnings, DCF with WACC, comparable multiples, and residual income) and aggregates them with configurable weights. The trade-off is less nuance than LLM-based agents, but the consistency and speed are worth it for systematic metrics.
| Design Choice | What Was Chosen | What Was Rejected | Why |
|---|---|---|---|
| Signal computation | Algorithmic threshold scoring | LLM-based interpretation | Quantitative metrics have well-defined thresholds (ROE > 15%); LLM adds latency and cost without adding value for objective measurements |
| Technical strategies | Weighted ensemble of 5 strategies | Single indicator (e.g., MACD only) | No single technical indicator is reliable; combining trend, momentum, mean-reversion, volatility, and stat-arb produces more robust signals |
| Sentiment sources | Insider trades (30%) + news (70%) | Social media, options flow | Insider trades are legally required disclosures with proven predictive value; news sentiment is available via existing API |
| Valuation models | Four-model weighted average | Single DCF model | Each model has blind spots; Owner Earnings catches FCF quality, DCF handles growth, multiples provide market context |
Contents
Architecture
The four quantitative agents run in parallel within the LangGraph fan-out phase alongside the persona agents. Each takes raw market data and produces a structured signal without LLM dependency (except valuation's WACC estimation).
flowchart TD
State["AgentState"] --> FA["Fundamentals Agent"]
State --> TA["Technical Agent"]
State --> SA["Sentiment Agent"]
State --> VA["Valuation Agent"]
subgraph FA_inner["Fundamentals Pipeline"]
F1["Profitability Analysis"] --> F4["Aggregate Signal"]
F2["Growth Analysis"] --> F4
F3["Financial Health"] --> F4
end
subgraph TA_inner["Technicals Pipeline"]
T1["Trend Following 25%"] --> T5["Weighted Ensemble"]
T2["Mean Reversion 20%"] --> T5
T3["Momentum 25%"] --> T5
T4["Volatility 15%"] --> T5
T4b["Stat Arb 15%"] --> T5
end
subgraph SA_inner["Sentiment Pipeline"]
S1["Insider Trades 30%"] --> S3["Combined Signal"]
S2["News Sentiment 70%"] --> S3
end
subgraph VA_inner["Valuation Pipeline"]
V1["Owner Earnings"] --> V5["Weighted Average"]
V2["Enhanced DCF + WACC"] --> V5
V3["Comparable Multiples"] --> V5
V4["Residual Income"] --> V5
end
FA --> FA_inner
TA --> TA_inner
SA --> SA_inner
VA --> VA_inner
FA_inner --> Signals["analyst_signals"]
TA_inner --> Signals
SA_inner --> Signals
VA_inner --> Signals
Key Classes & Functions
| Class / Function | File | Purpose |
|---|---|---|
fundamentals_analyst_agent | fundamentals.py | Main entry point: fetches financial metrics, runs profitability/growth/health analyses, aggregates into bullish/bearish/neutral signal |
technical_analyst_agent | technicals.py | Main entry point: fetches price data, runs 5 strategy calculations, combines via weighted ensemble |
sentiment_analyst_agent | sentiment.py | Main entry point: fetches insider trades and news, computes weighted sentiment from both sources |
valuation_analyst_agent | valuation.py | Main entry point: fetches line items and metrics, runs 4 valuation models, aggregates with configurable weights |
news_sentiment_agent | news_sentiment.py | Dedicated news-only sentiment analysis using NLP on company news articles |
calculate_trend_signals | technicals.py | Computes EMA crossovers, MACD, and trend direction from price DataFrame |
calculate_mean_reversion_signals | technicals.py | Bollinger Bands, z-score, and RSI-based mean reversion signals |
calculate_momentum_signals | technicals.py | Rate of change, RSI momentum, and volume-weighted momentum indicators |
calculate_volatility_signals | technicals.py | ATR, historical volatility percentile, and volatility regime detection |
calculate_stat_arb_signals | technicals.py | Statistical arbitrage signals including Hurst exponent and half-life estimates |
weighted_signal_combination | technicals.py | Combines 5 strategy signals using configurable weights (trend:0.25, momentum:0.25, etc.) |
calculate_owner_earnings_value | valuation.py | Buffett-style owner earnings: net income + D&A - capex - working capital change, discounted |
calculate_wacc | valuation.py | Weighted average cost of capital from market cap, debt, interest coverage, and risk-free rate |
safe_float | technicals.py | Safely converts pandas/numpy values to float, handling NaN and overflow gracefully |
Data Structures
| Type | Name | Description |
|---|---|---|
| dict | signals | List of per-aspect signals ("bullish"/"bearish"/"neutral") accumulated during fundamentals analysis |
| dict | reasoning | Structured reasoning dict with sub-keys for each analysis dimension (profitability_signal, growth_signal, etc.) |
| dict | strategy_weights | Technicals weighting: {trend: 0.25, mean_reversion: 0.20, momentum: 0.25, volatility: 0.15, stat_arb: 0.15} |
| DataFrame | prices_df | Pandas DataFrame of OHLCV price data, converted from API response via prices_to_df() |
| dict | valuation_analysis | Per-ticker dict with owner_earnings, dcf, multiples, and residual_income sub-model results |
Interactions
| Direction | Partner | Interface | Description |
|---|---|---|---|
| → | Financial API | get_financial_metrics() | Fundamentals and valuation agents fetch TTM metrics (ROE, margins, growth rates, ratios) |
| → | Price API | get_prices() + prices_to_df() | Technicals agent fetches historical OHLCV data and converts to pandas DataFrame |
| → | News API | get_company_news() | Sentiment agent fetches up to 100 recent news articles with pre-computed sentiment labels |
| → | Insider API | get_insider_trades() | Sentiment agent fetches up to 1000 insider trade records to detect buying/selling patterns |
| → | Line Items API | search_line_items() | Valuation agent fetches 12 specific line items (FCF, net income, D&A, capex, working capital, debt, etc.) |
| ← | LangGraph State | state["data"]["analyst_signals"] | All four agents write standardized {signal, confidence, reasoning} dicts to shared state |
| → | NumPy/Pandas | np.where(), pd.Series() | Technicals and sentiment agents use vectorized operations for signal computation |
| → | Progress Tracker | progress.update_status() | Each agent reports step-by-step progress (fetching data, analyzing, combining signals) |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
profitability thresholds | ROE > 15%, Net Margin > 20%, Op Margin > 15% | Fundamentals profitability scoring thresholds; 2/3 triggers bullish |
growth thresholds | Revenue > 10%, Earnings > 10%, Book Value > 10% | Fundamentals growth scoring; same 2/3 trigger logic |
health thresholds | Current Ratio > 1.5, D/E < 0.5, FCF > 0.8*EPS | Financial health indicators for fundamentals agent |
strategy_weights | trend:0.25, mean_rev:0.20, momentum:0.25, vol:0.15, stat:0.15 | Technicals weighted ensemble; trend and momentum weighted highest |
insider_weight / news_weight | 0.3 / 0.7 | Sentiment weighting between insider trades and news; news weighted higher due to volume |
valuation limit | 8 periods | Number of historical TTM periods for valuation models |
Directory Map
Source tree for quantitative agents
src/agents/
├── fundamentals.py ← Financial statement analysis (profitability, growth, health)
├── technicals.py ← Price/volume technical analysis (5-strategy ensemble)
├── sentiment.py ← Market sentiment from insider trades + news
├── valuation.py ← Intrinsic value models (DCF, owner earnings, multiples)
└── news_sentiment.py ← Dedicated news-only sentiment via NLP
📑 Annotated Code Walkthrough
The fundamentals agent demonstrates the threshold-based scoring approach used by quantitative agents. Unlike persona agents that rely on LLM reasoning, this agent deterministically scores each financial dimension.
fundamentals_analyst_agent — Threshold-Based Analysis
def fundamentals_analyst_agent(state: AgentState, agent_id: str = "fundamentals_analyst_agent"):
"""Analyzes fundamental data and generates trading signals for multiple tickers."""
data = state["data"]
end_date = data["end_date"]
tickers = data["tickers"]
api_key = get_api_key_from_state(state, "FINANCIAL_DATASETS_API_KEY")
fundamental_analysis = {}
for ticker in tickers:
financial_metrics = get_financial_metrics(
ticker=ticker, end_date=end_date, period="ttm", limit=10, api_key=api_key,
)
if not financial_metrics:
continue
metrics = financial_metrics[0] # Most recent period
signals = []
reasoning = {}
# 1. Profitability Analysis
return_on_equity = metrics.return_on_equity
net_margin = metrics.net_margin
operating_margin = metrics.operating_margin
thresholds = [
(return_on_equity, 0.15), # Strong ROE above 15%
(net_margin, 0.20), # Healthy profit margins
(operating_margin, 0.15), # Strong operating efficiency
]
profitability_score = sum(
metric is not None and metric > threshold
for metric, threshold in thresholds
)
signals.append(
"bullish" if profitability_score >= 2
else "bearish" if profitability_score == 0
else "neutral"
)
# 2. Growth Analysis
revenue_growth = metrics.revenue_growth
earnings_growth = metrics.earnings_growth
book_value_growth = metrics.book_value_growth
thresholds = [
(revenue_growth, 0.10), # 10% revenue growth
(earnings_growth, 0.10), # 10% earnings growth
(book_value_growth, 0.10), # 10% book value growth
]
growth_score = sum(
metric is not None and metric > threshold
for metric, threshold in thresholds
)
# 3. Financial Health
current_ratio = metrics.current_ratio
debt_to_equity = metrics.debt_to_equity
health_score = 0
if current_ratio and current_ratio > 1.5:
health_score += 1
if debt_to_equity and debt_to_equity < 0.5:
health_score += 1
Line-by-line:
- Line 10-12: Fetches TTM metrics with
limit=10periods. The agent only usesfinancial_metrics[0](most recent), but the API call pattern mirrors persona agents for consistency. - Line 13-14: Early exit on missing data — if the API returns nothing, the ticker is silently skipped. No fallback or default signal is emitted.
- Line 15:
metrics = financial_metrics[0]picks only the most recent TTM snapshot, discarding historical data. This is a key design difference from persona agents (e.g., Buffett uses all 10 periods for trend analysis). - Lines 24-31: The threshold pattern: each metric is compared against a hard-coded threshold using a list comprehension. The
metric is not Noneguard preventsTypeErrorwhen the API returns null values. Scoring is binary: meet threshold = 1 point, miss = 0. - Lines 33-37: Signal mapping uses a 2/3 majority rule: 2+ out of 3 metrics passing = bullish, 0 passing = bearish, otherwise neutral. This is simpler than the persona agents' LLM-based scoring but highly reproducible.
- Lines 54-56: Health scoring uses different threshold types — current ratio must be above 1.5 (liquidity) while D/E must be below 0.5 (low leverage). The asymmetry shows domain knowledge encoded as rules.
Technical Agent — Weighted Ensemble
Weighted Signal Combination
# Five independent strategies, each producing its own signal
strategy_weights = {
"trend": 0.25, # EMA crossovers, MACD
"mean_reversion": 0.20, # Bollinger Bands, z-score
"momentum": 0.25, # RSI, rate of change
"volatility": 0.15, # ATR, vol regime
"stat_arb": 0.15, # Hurst exponent, half-life
}
# Each strategy returns {signal, confidence, metrics}
combined_signal = weighted_signal_combination(
{
"trend": trend_signals,
"mean_reversion": mean_reversion_signals,
"momentum": momentum_signals,
"volatility": volatility_signals,
"stat_arb": stat_arb_signals,
},
strategy_weights,
)
Data Flow
How the four quantitative agents process data from different sources and converge on signal output:
sequenceDiagram
participant S as AgentState
participant F as Fundamentals
participant T as Technicals
participant Se as Sentiment
participant V as Valuation
participant API as Financial APIs
participant Out as analyst_signals
par Parallel Execution
S->>F: tickers, end_date
F->>API: get_financial_metrics()
API-->>F: metrics (ROE, margins, ratios)
F->>F: Score profitability, growth, health
F->>Out: signal + confidence + reasoning
and
S->>T: tickers, start_date, end_date
T->>API: get_prices()
API-->>T: OHLCV DataFrame
T->>T: 5 strategies weighted ensemble
T->>Out: signal + confidence + reasoning
and
S->>Se: tickers, end_date
Se->>API: get_insider_trades() + get_company_news()
API-->>Se: trades + news articles
Se->>Se: Weighted count (30/70 split)
Se->>Out: signal + confidence + reasoning
and
S->>V: tickers, end_date
V->>API: search_line_items() + get_financial_metrics()
API-->>V: line items + metrics
V->>V: 4 valuation models + WACC
V->>Out: signal + confidence + reasoning
end
⚠️ Edge Cases & Boundaries
| Edge Case | What Happens | Where in Code | Risk Level |
|---|---|---|---|
| No financial metrics returned | Fundamentals agent skips the ticker entirely (continue), no signal emitted | fundamentals.py:32-34 | 🔴 High |
| No price data for technicals | Agent skips ticker; progress shows "Failed: No price data found" | technicals.py:63-65 | 🔴 High |
| NaN values in price DataFrame | safe_float() returns 0.0 default; prevents downstream math errors | technicals.py:15-31 | 🟡 Medium |
| Zero insider trades | Empty transaction_shares Series; insider_signals is empty list, weight goes entirely to news | sentiment.py:35-36 | 🟢 Low |
| Working capital is None | Valuation agent defaults wc_change = 0; owner earnings calculation proceeds without WC adjustment | valuation.py:78-81 | 🟡 Medium |
| Fewer than 2 line items | Valuation agent skips ticker; cannot compute period-over-period changes | valuation.py:69-71 | 🔴 High |