Projects / AlphaAgents / Documentation

AlphaAgents — Multi-Agent Equity Research

Mo Minoneshan
2026

Overview

AlphaAgents is a modular multi-agent system for equity research and portfolio construction. It reproduces the framework from the BlackRock paper "Large Language Model based Multi-Agents for Equity Portfolio Constructions" (Zhao et al., arXiv:2508.11152). Three specialist agents analyze a stock from independent angles; a collaborator agent reconciles them through a structured debate; and a leakage-safe backtester converts the resulting signals into portfolio performance.

0Executive Summary

A single model tends to over-weight whichever signal is loudest. By decomposing research into fundamental, sentiment, and valuation specialists — and forcing them to reconcile disagreements — the system produces interpretable, auditable recommendations. On a 90-day out-of-sample window in a weak regime, the strategy returned +7.1% annualized vs. −4.7% for buy-and-hold (Sharpe 0.61 vs. −0.41), with value coming mostly from avoiding losers and controlling risk.

1Motivation

Investment teams pair fundamental analysts, technical traders, and sentiment researchers who debate before committing capital. Mirroring that structure lets each agent specialize, keeps rationales transparent, and makes the final decision explainable — a prerequisite for any system intended to support human decision makers.

2Pipeline & Flow

The end-to-end flow moves from a broad universe to a concentrated, backtested portfolio:

Universe → multi-stage screening (sector momentum → sentiment / fundamental composite → implied-volatility filter) → data hydration (prices, filings, news) → parallel agent analysis → consensus debate → portfolio construction → backtest → export.

def run_single_ticker(ticker, price_history, news_items, config):
    fundamental_agent.prime(filing_bundle)
    valuation_agent.set_ticker(ticker)
    decisions = []
    for date in trading_dates:
        decision = collaborator.evaluate_day(date, price_window, news)
        decisions.append(decision)
    metrics = compute_metrics(decisions, prices)
    return decisions, metrics

2.1Screening Stages

Before any expensive agent analysis, the universe is narrowed by a configurable, stacked prefilter. Each stage is optional and independently tunable via CLI flags.

  • Sector momentum — ranks sectors by recent relative strength and keeps constituents of the leading sectors. (Implemented for the S&P 500 universe; skipped automatically for S&P 100.)
  • Sentiment / fundamental composite — a fast, LLM-free score that removes names with weak news/fundamental signals before deeper analysis.
  • Implied-volatility filter — keeps names whose average implied volatility clears a floor (--min-avg-iv, default \(0.35\)), focusing the portfolio on names with tradable opportunity.

A representative S&P 100 run narrowed 101 → 94 (composite) → 10 (IV filter), after which agent consensus flagged one name (VZ) for removal.

2.2Data Layer

Every source is wrapped so agents consume typed dataclasses, never raw API payloads — decoupling analysis from vendor formats.

  • Market (Yahoo Finance / yfinance) → OHLCV DataFrame, cached locally.
  • Filings (SEC Edgar) → FilingBundle text sections for 10-K / 10-Q.
  • News (yfinance RSS) → NewsItem list with title, description, timestamp.
  • Universe (Wikipedia S&P lists) → ticker symbols, cached.

Design consequence: swapping to a paid provider (e.g., Polygon) means changing one connector, not the agents.

3Fundamental Agent (RAG)

The fundamental agent runs retrieval-augmented generation over SEC 10-K/10-Q filings. Filings are chunked into 512-token segments with 50-token overlap, embedded with all-MiniLM-L6-v2 (384-dim), and indexed. For a query, the top \(k = 10\) chunks are retrieved and passed to an LLM (OpenAI GPT-4o-mini or local Mistral) which returns a BUY/HOLD/SELL opinion with confidence and rationale. A deterministic keyword fallback runs when no LLM is configured.

# RAG retrieval (full mode)
retriever = VectorIndexRetriever(index=index, similarity_top_k=10)

# Fast-mode keyword scoring (deterministic fallback)
score = (count_positive - count_negative) / total_words
signal = "BUY" if score > 0.05 else "SELL" if score < -0.05 else "HOLD"

On a manual review of 50 generated recommendations, rationales were factually grounded 86% of the time and relevant 92% of the time.

3.1SEC Table Challenge

The hardest unsolved problem was parsing financial tables from filings. Four approaches were attempted:

  1. BeautifulSoup flat extraction — lost header/value relationships
  2. Pandas read_html() — merged cells, spurious columns
  3. Custom header-matching — failed on nested multi-level headers
  4. Embeddings + table-metadata dictionary — context overflow beyond ~20 tables

Honest status: partially solved

The metadata approach works for simple queries (<5 tables) but complex, multi-table queries remain brittle. Table-specific models (TaPas / Table-BERT) or a SQL layer over extracted tables are the proposed next steps.

4Sentiment Agent

The sentiment agent aggregates recent headlines into a score in \([-1, +1]\). In fast mode it uses a lexicon; in full mode it adds LLM synthesis for a summary, recommendation, and supporting quotes. Recency weighting emphasizes newer articles:

$$ s = \frac{\sum_i w_i \, x_i}{\sum_i w_i}, \qquad w_i = e^{-\lambda i}, $$

where \(x_i\) is the sentiment of article \(i\) (ordered newest-first) and \(\lambda\) controls decay.

5Valuation Agent

The valuation agent computes technical features — returns, moving averages, and RSI — then maps momentum and mean-reversion cues to a signal. It is the fastest and most deterministic agent (no LLM, no external calls beyond cached prices).

ma_20 = close.rolling(20).mean().iloc[-1]
ret_20d = close.iloc[-1] / close.iloc[-21] - 1
signal = "BUY" if (ret_20d > 0.05 and close.iloc[-1] > ma_20) else \
         "SELL" if ret_20d < -0.05 else "HOLD"

6Consensus & Debate

Each agent signal is normalized to \(\{-1, 0, +1\}\). The collaborator computes a confidence-weighted consensus:

$$ C = \frac{\sum_a c_a \, s_a}{\sum_a c_a}, \qquad a \in \{\text{fund}, \text{sent}, \text{val}\}, $$

mapping \(C > 0.33\) to BUY and \(C < -0.33\) to SELL. A debate is triggered when the specialists disagree and the valuation signal strength clears a small threshold (debate_threshold, default \(0.01\)); in debate, the valuation view is used to break ties and the agents' rationales are reconciled before the consensus is recomputed.

6.1Worked Example

A representative disagreement on AAPL:

  • Fundamental: SELL (0.75) — margin pressure in China
  • Sentiment: BUY (0.82) — strong iPhone pre-orders
  • Valuation: HOLD (0.60) — price near 20-day MA

The spread triggers a debate. The fundamental agent moderates to HOLD after acknowledging the product cycle; sentiment softens its confidence. Final consensus: HOLD (0.67). Outcome: the stock fell ~2.3% over the next five days, so the moderated call beat the overly bullish sentiment signal.

7Signal Definition

Signals are categorical (BUY/HOLD/SELL), mapped to numeric \(\{+1, 0, -1\}\) for consensus. Positions are long-or-flat (no shorting in the base implementation): the position manager enters long on a sufficiently confident BUY and moves to cash on SELL or on a confidence collapse. This keeps the strategy retail-realistic and avoids borrow/short mechanics.

  • Enter long: consensus signal \(> 0.5\) and confidence \(> 0.6\)
  • Exit to cash: consensus signal \(< -0.3\) or confidence \(< 0.4\)
  • Otherwise: hold current position (subject to min-hold / stop)

8Backtesting Engine

The backtest is not a trained model — it replays agent signals through a rules-based state machine. Positions honor a minimum holding period, exit on a stop-loss, and pay a fixed transaction cost (10 bps) on each change. Trades are assumed at the close and P&L is realized on the next bar. The minimum holding period reduces overtrading and approximates realistic execution constraints.

class PositionManager:
    def update(self, signal, price, confidence):
        # stop-loss
        if self.position > 0 and (price - self.entry) / self.entry < -self.stop:
            self.position, self.held = 0.0, 0
            return 0.0
        # minimum holding period
        if self.held < self.min_hold:
            self.held += 1
            return self.position
        if signal > 0.5 and confidence > 0.6 and self.position == 0.0:
            self.position, self.entry, self.held = 1.0, price, 0
        elif signal < -0.3 or confidence < 0.4:
            self.position, self.held = 0.0, 0
        return self.position

9Leakage Controls

Signals use only information available at the decision date. Concretely, the one leak found and fixed was cross-sectional ranking on same-day returns — replaced with lagged (prior-day) returns plus timestamp-availability checks. Backtests run strictly chronologically (no shuffling).

Concrete leakage mode prevented

Ranking used today's return to pick today's trade. Fixed by lagging ranking inputs to the previous day and asserting each input existed at the decision timestamp.

10Metrics & Results

Returns are daily; the Sharpe ratio annualizes with \(\sqrt{252}\):

$$ \text{Sharpe} = \frac{\overline{r}}{\sigma_r}\,\sqrt{252}. $$

On a 90-day out-of-sample window (5-stock tech basket, weak regime), the agent strategy materially outperformed buy-and-hold:

Metric Agent Strategy Buy & Hold
Total return+7.1%−4.7%
Annualized return+7.1%−17.8%
Sharpe ratio0.61−0.41
Max drawdown−5.2%−12.3%
Win rate57%
Avg position0.631.00

Ablations show the debate step contributed roughly +0.15 Sharpe and correctly flagged drawdown names (e.g., VZ at −4.71%, removed by consensus).

11Findings

On a 10-stock S&P 100 production run, directional accuracy was 5/10 (50%) overall but 3/4 (≈60–75%) on SELL signals vs. 2/6 on BUY/HOLD. The system was measurably better at avoiding losers than picking winners — suggesting defensive/risk-control use cases are the sweet spot.

  • Consensus/debate improved Sharpe by ~0.15 and added transparency.
  • High LLM confidence did not reliably predict correctness (needs calibration).
  • Data quality mattered more than model choice; free news feeds were the weakest link.
  • The backtesting rig — not the agents — was where most engineering effort and learning happened.

12Human Feedback & RLHF

A human-in-the-loop layer logs reviewer overrides and comments as JSONL, then injects the most recent feedback for a ticker into subsequent agent prompts — closing the loop between human judgment and agent behavior. This dataset is the foundation for future preference tuning:

  1. Collect overrides + realized outcomes (currently a small pilot set).
  2. Train a reward model to predict human approval.
  3. Fine-tune the agent LLM with DPO and A/B test against the baseline.

Status: the feedback capture and prompt-injection loop are implemented; reward-model training is future work (needs 100+ labeled entries for statistical validity).

13Technology Stack

  • Language: Python 3.11+
  • LLM / RAG: LlamaIndex, OpenAI GPT-4o-mini (cloud) or Ollama/Mistral (local)
  • Embeddings: all-MiniLM-L6-v2 (384-dim) via HuggingFace
  • Vector store: FAISS
  • Data: yfinance (market/news), SEC Edgar (filings), Wikipedia (universe)
  • Compute: pandas / NumPy for features and backtest math; ThreadPoolExecutor for parallel agents
  • Interface: Flask dashboard for review & feedback; CLI for pipelines

14Limitations

Honest caveats

Results cover a single 90-day window, so they are regime-dependent; a rolling walk-forward across regimes is the key next step. Free-tier news data is sparse and noisy. Fees are a fixed-bps proxy rather than a spread model. The backtest replays signals — it does not optimize a predictive model.

ACLI Reference

# Quick, LLM-free run
python run_alphaagents.py --mode quick --tickers AAPL MSFT GOOGL --fast

# Full screening + selection + backtest
python -m apps.pipeline.app --universe sp100 --target-count 10 \
    --period 3mo --llm-provider openai \
    --export-pipeline artifacts/exports/pipeline.json