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.

5
Agent Files
4
Analysis Domains
~3.2k
Lines
15+
Signal Strategies

📚 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 ChoiceWhat Was ChosenWhat Was RejectedWhy
Signal computationAlgorithmic threshold scoringLLM-based interpretationQuantitative metrics have well-defined thresholds (ROE > 15%); LLM adds latency and cost without adding value for objective measurements
Technical strategiesWeighted ensemble of 5 strategiesSingle 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 sourcesInsider trades (30%) + news (70%)Social media, options flowInsider trades are legally required disclosures with proven predictive value; news sentiment is available via existing API
Valuation modelsFour-model weighted averageSingle DCF modelEach 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 / FunctionFilePurpose
fundamentals_analyst_agentfundamentals.pyMain entry point: fetches financial metrics, runs profitability/growth/health analyses, aggregates into bullish/bearish/neutral signal
technical_analyst_agenttechnicals.pyMain entry point: fetches price data, runs 5 strategy calculations, combines via weighted ensemble
sentiment_analyst_agentsentiment.pyMain entry point: fetches insider trades and news, computes weighted sentiment from both sources
valuation_analyst_agentvaluation.pyMain entry point: fetches line items and metrics, runs 4 valuation models, aggregates with configurable weights
news_sentiment_agentnews_sentiment.pyDedicated news-only sentiment analysis using NLP on company news articles
calculate_trend_signalstechnicals.pyComputes EMA crossovers, MACD, and trend direction from price DataFrame
calculate_mean_reversion_signalstechnicals.pyBollinger Bands, z-score, and RSI-based mean reversion signals
calculate_momentum_signalstechnicals.pyRate of change, RSI momentum, and volume-weighted momentum indicators
calculate_volatility_signalstechnicals.pyATR, historical volatility percentile, and volatility regime detection
calculate_stat_arb_signalstechnicals.pyStatistical arbitrage signals including Hurst exponent and half-life estimates
weighted_signal_combinationtechnicals.pyCombines 5 strategy signals using configurable weights (trend:0.25, momentum:0.25, etc.)
calculate_owner_earnings_valuevaluation.pyBuffett-style owner earnings: net income + D&A - capex - working capital change, discounted
calculate_waccvaluation.pyWeighted average cost of capital from market cap, debt, interest coverage, and risk-free rate
safe_floattechnicals.pySafely converts pandas/numpy values to float, handling NaN and overflow gracefully

Data Structures

TypeNameDescription
dictsignalsList of per-aspect signals ("bullish"/"bearish"/"neutral") accumulated during fundamentals analysis
dictreasoningStructured reasoning dict with sub-keys for each analysis dimension (profitability_signal, growth_signal, etc.)
dictstrategy_weightsTechnicals weighting: {trend: 0.25, mean_reversion: 0.20, momentum: 0.25, volatility: 0.15, stat_arb: 0.15}
DataFrameprices_dfPandas DataFrame of OHLCV price data, converted from API response via prices_to_df()
dictvaluation_analysisPer-ticker dict with owner_earnings, dcf, multiples, and residual_income sub-model results

Interactions

DirectionPartnerInterfaceDescription
→Financial APIget_financial_metrics()Fundamentals and valuation agents fetch TTM metrics (ROE, margins, growth rates, ratios)
→Price APIget_prices() + prices_to_df()Technicals agent fetches historical OHLCV data and converts to pandas DataFrame
→News APIget_company_news()Sentiment agent fetches up to 100 recent news articles with pre-computed sentiment labels
→Insider APIget_insider_trades()Sentiment agent fetches up to 1000 insider trade records to detect buying/selling patterns
→Line Items APIsearch_line_items()Valuation agent fetches 12 specific line items (FCF, net income, D&A, capex, working capital, debt, etc.)
←LangGraph Statestate["data"]["analyst_signals"]All four agents write standardized {signal, confidence, reasoning} dicts to shared state
→NumPy/Pandasnp.where(), pd.Series()Technicals and sentiment agents use vectorized operations for signal computation
→Progress Trackerprogress.update_status()Each agent reports step-by-step progress (fetching data, analyzing, combining signals)

Configuration

Knob / ParameterDefaultDescription
profitability thresholdsROE > 15%, Net Margin > 20%, Op Margin > 15%Fundamentals profitability scoring thresholds; 2/3 triggers bullish
growth thresholdsRevenue > 10%, Earnings > 10%, Book Value > 10%Fundamentals growth scoring; same 2/3 trigger logic
health thresholdsCurrent Ratio > 1.5, D/E < 0.5, FCF > 0.8*EPSFinancial health indicators for fundamentals agent
strategy_weightstrend:0.25, mean_rev:0.20, momentum:0.25, vol:0.15, stat:0.15Technicals weighted ensemble; trend and momentum weighted highest
insider_weight / news_weight0.3 / 0.7Sentiment weighting between insider trades and news; news weighted higher due to volume
valuation limit8 periodsNumber 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=10 periods. The agent only uses financial_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 None guard prevents TypeError when 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 CaseWhat HappensWhere in CodeRisk Level
No financial metrics returnedFundamentals agent skips the ticker entirely (continue), no signal emittedfundamentals.py:32-34🔴 High
No price data for technicalsAgent skips ticker; progress shows "Failed: No price data found"technicals.py:63-65🔴 High
NaN values in price DataFramesafe_float() returns 0.0 default; prevents downstream math errorstechnicals.py:15-31🟡 Medium
Zero insider tradesEmpty transaction_shares Series; insider_signals is empty list, weight goes entirely to newssentiment.py:35-36🟢 Low
Working capital is NoneValuation agent defaults wc_change = 0; owner earnings calculation proceeds without WC adjustmentvaluation.py:78-81🟡 Medium
Fewer than 2 line itemsValuation agent skips ticker; cannot compute period-over-period changesvaluation.py:69-71🔴 High