Skip to content

dsarkar10/stockaudit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

StockAudit AI

Multi-Agent Stock Analysis Engine with ML-powered Anomaly Detection


Table of Contents

  1. System Overview
  2. Architecture & Data Flow
  3. Market Data Layer
  4. Risk Model
  5. Alpha Signals
  6. RAG Pipeline
  7. Multi-Agent System
  8. API Layer & SSE Streaming
  9. Frontend
  10. End-to-End Walkthrough
  11. Quick Start
  12. API Reference
  13. Performance Benchmarks
  14. Project Structure
  15. Tech Stack

System Overview

StockAudit AI is a full-stack platform that takes a stock ticker or company name and produces a multi-faceted analysis report. It computes 9 financial features from 1 year of daily OHLCV data, scores the stock through two independent risk models (XGBoost binary probability + percentile deviation from 52-week history), detects institutional whale activity and retail concentration, retrieves relevant news via TF-IDF RAG, and feeds everything to three sequential LLM agents (CrewAI + Groq) that output structured JSON. Results stream to a React dashboard via Server-Sent Events.

The system was designed for zero-cost operation: all data comes from the free yfinance API, all ML is CPU-based (XGBoost + scikit-learn), and the LLM calls use Groq's free tier.


Architecture & Data Flow

Request Flow (from browser to final report)

User types "apple" in search bar
        |
        v
EventSource connects to GET /api/analyze/stream/{symbol}
        |
        v
Backend resolves "apple" -> "AAPL" via yfinance Search
        |
        v
Backend fetches 1 year of daily OHLCV from yfinance
        |
        v
Backend computes 9 engineered features on the full dataframe
        |
        v
Backend computes whale signals (rolling regression), exhaustion, retail concentration
        |
        v
Backend fetches macro context: VIX (^VIX) and 10Y yield (^TNX)
        |
        v
Backend runs XGBoost predict_proba + percentile deviation scoring + SHAP values
        |
        v
Backend pre-populates TF-IDF RAG store with 10-15 news articles + business summary
        |
        v
Backend launches 3 sequential CrewAI agents (each with Research Company tool)
        |
        v
Results progressively stream to browser via SSE (10 events total)

SSE Event Sequence

The streaming endpoint emits exactly these 10 events in order:

 1. market_data      -> MarketSnapshot, macro (VIX/TNX), signals
 2. risk_assessment  -> RiskGauge (dual scores, SHAP, factors, disagreement)
 3. chart_data       -> PriceChart (100-day OHLCV)
 4. alpha_signals    -> AlphaSignals (whale, retail, exhaustion)
 5. rag_status       -> RAG badge (N articles indexed)
 6. agent_start      -> "Market Analyst" status = running
 7. agent_complete   -> MarketAnalysisReport JSON
 8. agent_start      -> "Risk & Compliance" status = running
 9. agent_complete   -> RiskReport JSON
10. agent_start      -> "Report Summary" status = running
11. agent_complete   -> FinalReport JSON
12. complete          -> stream done

Events 6-11 repeat for each of the 3 agents (2 events each = 6 events, plus the 5 pre-agent events = 11 total SSE messages, but agent_start+agent_complete come in pairs so there are 3 pairs = 6 + 5 pre-agent = 11 data events, plus the complete event = 12 lines total, usually referred to as 10 logical stages).

Backend Pipeline (code execution order)

main.py: event_stream()
  1. get_latest_features(symbol) -> (features_df, market_data_dict, full_df, resolved_symbol)
     market_data.py:
       resolve_symbol(query)     -> searches yfinance, returns ticker
       fetch_stock_data(ticker)  -> yf.Ticker.history(period="1y")
       compute_features(df)      -> 9 features + whale signals + exhaustion
       fetch_macro_context()     -> yf.Ticker("^VIX") + yf.Ticker("^TNX")
       returns dict with OHLCV + features + macro + whale + resolved_symbol

  2. predict_risk(features_df, full_df) -> risk_data dict
     risk_model.py:
       XGBoost: model.predict_proba() -> P(anomaly|X)
       Percentile: for each feature, compute F_i(current) vs 52wk empirical CDF
       SHAP: booster.predict(dmat, pred_contribs=True) -> Shapley values
       model_disagreement: flag when severity levels differ by 2+

  3. chart_data = full_df.tail(100) -> [{date, open, high, low, close, volume}]

  4. alpha_signals = get_retail_concentration() + whale_signals from market_data

  5. rag.build_store(symbol) -> fetches news, chunks, TF-IDF vectorizes, caches

  6. stream_analysis(symbol, market_data, risk_data, queue) -> runs 3 Crews
     agents.py:
       Crew 1: Market Analyst (ANALYSIS) -> JSON
       Crew 2: Risk & Compliance (RISK) -> JSON
       Crew 3: Report Summary (REPORT) -> JSON
       Each: emits agent_start -> agent runs -> agent_complete via Queue

Market Data Layer

File: backend/market_data.py

Name Resolution

resolve_symbol(query: str) -> str uses yfinance.Search(query) to map company names to tickers. It iterates search.quotes looking for quoteType == "EQUITY" with a valid symbol. If no match, it returns the uppercase query as-is (so TSLA stays TSLA, AAPL stays AAPL). This enables typing "apple" instead of "AAPL".

Data Fetching

fetch_stock_data(symbol, period="3mo") calls yf.Ticker(symbol).history(period="1y") to get 1 year of daily OHLCV data (Open, High, Low, Close, Volume, Dividends, Stock Splits). If the initial fetch fails, it retries with the resolved symbol.

Feature Engineering

compute_features(df: pd.DataFrame) -> pd.DataFrame takes the raw OHLCV DataFrame and computes these 9 features:

daily_return(t)   = Close(t) / Close(t-1) - 1                    # daily pct change
volume_ma20(t)    = SMA(Volume, 20)                               # 20-day avg volume
volume_change(t)  = (Volume(t) - volume_ma20(t)) / volume_ma20()  # volume vs 20d avg
volatility(t)     = STD(daily_return, 20)                         # 20-day rolling vol
ma20(t)           = SMA(Close, 20)                                # 20-day moving avg
ma50(t)           = SMA(Close, 50)                                # 50-day moving avg
gap_from_ma20(t)  = (Close(t) - ma20(t)) / ma20(t)                # pct distance from MA20
gap_from_ma50(t)  = (Close(t) - ma50(t)) / ma50(t)                # pct distance from MA50
spread_pct(t)     = (High(t) - Low(t)) / Close(t)                 # daily range %

RSI(14):
  delta     = Close(t) - Close(t-1)
  gain(t)   = max(delta, 0)
  loss(t)   = -min(delta, 0)
  avg_gain  = SMA(gain, 14)
  avg_loss  = SMA(loss, 14)
  RS        = avg_gain / avg_loss
  RSI       = 100 - (100 / (1 + RS))

price_position(t) = (Close(t) - low_20d(t)) / (high_20d(t) - low_20d(t))
                    # where high_20d = MAX(High, 20), low_20d = MIN(Low, 20)

volume_price_ratio(t) = Volume(t) / (Close(t) * SMA(Volume, 20))

All NaN rows are dropped after computation.

Macro Context

fetch_macro_context() -> dict fetches the CBOE Volatility Index (^VIX) and 10-Year Treasury Yield (^TNX) via yfinance. Results are cached in an in-memory dict with a 1-hour TTL (_MACRO_CACHE). Returns {"vix": 16.59, "tnx": 4.56} or None if fetch fails.

Data Output Format

get_latest_features() returns a tuple:

  • features: single-row DataFrame with the 9 feature columns (for model inference)
  • market_data_dict: full dict with all OHLCV values, all 9 features, date, whale_signals, macro context, resolved_symbol
  • full_df: complete DataFrame with computed features (for percentile deviation calculation)
  • resolved_symbol: the canonical ticker (e.g., "AAPL" for "apple")

The market_data_dict is what gets serialized into the market_data SSE event.


Risk Model

File: backend/risk_model.py

The risk model generates two independent scores and explanatory metadata. The model is trained by backend/train_model.py.

1. XGBoost Binary Classifier

P(anomaly | X) = sigmoid( sum_t f_t(X) )

Where f_t is the t-th decision tree in an ensemble of 200 trees (max_depth=6, learning_rate=0.1).

Training (train_model.py):

  1. Fetches 6 months of data for 16 stocks (AAPL, MSFT, GOOGL, AMZN, TSLA, META, NVDA, JPM, V, JNJ, RELIANCE.NS, TCS.NS, HDFCBANK.NS, INFY.NS, ICICIBANK.NS, SBIN.NS)
  2. Computes features, then labels each row: the top 7% by multi-feature z-score are labeled "anomaly" (class 1), the rest "normal" (class 0)
  3. Augments with 10,000 synthetic samples where anomalies have extreme daily_returns (+/-8-25%) and volume_changes (2-10x)
  4. Trains XGBClassifier with scale_pos_weight to balance the 93:7 class split
  5. Achieves ~94% accuracy on holdout test set

Inference: model.predict_proba(X)[:, 1] returns the probability of the "anomaly" class. For most stocks on most days, this is near 0 because true multi-feature anomalies are rare in the training distribution.

2. Percentile Deviation Score

R = ( sum_i w_i * D_i ) / ( sum_i w_i )

Where:

D_i = 2 * | F_i(x_current) - 0.5 |   (deviation from median, 0 to 1)
F_i(x) = rank_t(x) / T                 (empirical CDF over 52-week window)
w_i = feature_importance_i             (from XGBoost model)

For each of the 9 features, the current value is ranked against all historical values in the 52-week window. The percentile F_i(x) ranges from 0 (feature at all-time low) to 1 (feature at all-time high). The deviation D_i is 0 when the feature is at its 52-week median and approaches 1 as it nears the extremes (0th or 100th percentile).

The final score is a weighted average across all 9 features using XGBoost's feature_importances_ as weights. This score:

  • Is never zero (even normal days have some deviation from the median)
  • Varies meaningfully between stocks
  • Has a clear interpretation ("52-week percentile rank")
  • Ranges from 0% (all features at median) to 100% (all features at extremes)

3. SHAP Explainability

Per-prediction Shapley values are computed using XGBoost's native support:

booster = model.get_booster()
dmat = xgb.DMatrix(current, feature_names=FEATURE_COLUMNS)
contribs = booster.predict(dmat, pred_contribs=True)

The output matrix has shape (n_samples, n_features + 1) where:

  • Columns 0..n-1 are the SHAP contribution for each feature
  • Column n is the bias term (expected base margin)

The sum property: sum(phi_i) + bias = raw_margin_score (before sigmoid transformation to probability).

Each SHAP value represents the marginal contribution of that feature to pushing the prediction away from the average. Positive SHAP = feature pushes toward anomaly; negative = pushes toward normal.

4. Model Disagreement Signal

The two scores are independently mapped to severity levels:

XGBoost:       < 0.3  -> LOW    |  0.3-0.6  -> MEDIUM  |  >= 0.6  -> HIGH
Percentile:    < 0.2  -> LOW    |  0.2-0.4  -> MEDIUM  |  >= 0.4  -> HIGH

If the levels differ by 2 or more (e.g., XGBoost says LOW but percentile says HIGH), model_disagreement: true is set. This is common because the two methods measure fundamentally different things:

  • XGBoost: "is this a statistical anomaly compared to what the model learned?"
  • Percentile: "is this extreme relative to the stock's own recent range?"

Thresholds for Direction Labels (Percentile Method)

Percentile Range Label
>= 95 extreme
80-94.9 elevated
60-79.9 above avg
40-59.9 near median
20-39.9 below avg
< 20 depressed

Output Schema

{
  "risk_score": 0.4643,
  "xgb_score": 0.0006,
  "xgb_margin": -3.2914,
  "severity": "HIGH",
  "model_disagreement": true,
  "prediction": 0,
  "scores": {
    "xgboost": 0.0006,
    "xgboost_severity": "LOW",
    "percentile_deviation": 0.4643,
    "percentile_severity": "HIGH"
  },
  "shap_values": [
    {"feature": "rsi_14", "shap": 0.0083},
    {"feature": "volume_change", "shap": -0.0012}
  ],
  "contributing_factors": [
    {
      "feature": "rsi_14",
      "importance": 0.1729,
      "value": 91.1026,
      "percentile": 99.5,
      "direction": "extreme"
    }
  ]
}

Alpha Signals

File: backend/alphas.py

Three independent signals computed from the same OHLCV data:

1. Whale Detection (Institutional Accumulation/Distribution)

Uses rolling regression to detect anomalous volume that doesn't correspond to price movement:

# For each 60-day window:
#   Independent variable: |daily_return| (absolute price change)
#   Dependent variable: Volume
#   Residual = actual_volume - predicted_volume_from_regression
#
# z_residual = (residual - mean(residuals_60d)) / std(residuals_60d)

Interpretation:

  • z_residual > 1.5 AND price change is small (|return| < 0.02): ACCUMULATION — large volume with minimal price impact suggests informed buying
  • z_residual < -1.5 AND price trending down: DISTRIBUTION — large volume with downward drift suggests informed selling
  • Otherwise: NONE

The z-score uses a rolling 60-window for local normalization.

2. Momentum Exhaustion

Detects when a price rally is losing steam:

# Conditions for exhaustion:
#   1. RSI(14) > 70 (overbought)
#   2. recent_return (5d avg) > 2 * volatility (20d) (strong recent move)
#   3. volume_residual < 0 (volume lower than expected for this price move)

Flagged as exhaustion_signal: true when all three conditions are met. The frontend shows "EXHAUSTED" vs "NORMAL".

3. Retail Concentration

Uses yfinance's institutional holdings data:

retail_float_pct = 100 - institutional_pct
# Where institutional_pct = ticker.institutional_holders (sum of heldPercent)

If retail_float_pct > 50%, the stock is flagged as high retail concentration. The hypothesis: high retail ownership makes stocks prone to emotional trading and mean-reversion.


RAG Pipeline

File: backend/rag.py

Zero new dependencies — uses sklearn.feature_extraction.text.TfidfVectorizer and sklearn.metrics.pairwise.cosine_similarity which are already installed.

Sources

  1. yfinance news: yf.Ticker(symbol).news returns up to 15 recent articles from Yahoo Finance. Each has: title, summary, publisher, providerPublishTime, canonicalUrl. The system extracts title + summary (full articles are not available through this API, only previews).

  2. Business summary: yf.Ticker(symbol).info.longBusinessSummary returns the company's business description (typically 1-3 paragraphs).

Chunking Strategy

  • Business summary: split on periods, keep sentences > 60 characters as separate documents
  • News articles: concatenate as "Title: {title}\nSummary: {summary}" — each article is one document
  • Total: typically 10-20 documents per symbol

Vectorization

vectorizer = TfidfVectorizer(stop_words="english", max_features=2000)
tfidf_matrix = vectorizer.fit_transform(documents)

The vectorizer and matrix are stored in an in-memory cache keyed by symbol.

Search/Retrieval

When an agent calls the Research Company tool with a query:

query_vec = vectorizer.transform([query])
similarities = cosine_similarity(query_vec, tfidf_matrix)[0]
top_indices = argsort(similarities)[-3:][::-1]  # top 3
# Filter out similarities < 0.05

Results are returned as a text block prefixed with [NEWS] or [BUSINESS].

Cache Management

CACHE: dict[str, dict] = {}
CACHE_TTL = 3600  # 1 hour

The store is pre-populated during the SSE stream (before agents start) so agent tool calls are fast in-memory lookups. A status() function returns {"indexed": True, "article_count": N} for the frontend badge.


Multi-Agent System

File: backend/agents.py

Architecture

Three agents run sequentially (not in parallel). Each agent is its own Crew with a single agent + task. Agents share context through the stream_analysis() function which passes market_data_dict and risk_data to each subsequent agent.

Agent 1: Market Analyst (tag: ANALYSIS)

Role: Performs technical analysis.

Schema output:

{
  "summary": "string (1-2 sentences with numbers)",
  "trend": {"direction": "up|down|sideways", "ma20_gap_pct": "float", "ma50_gap_pct": "float"},
  "levels": {"support": "float", "resistance": "float"},
  "rsi": {"value": "float", "signal": "overbought|oversold|neutral"},
  "volume": {"shares": "int", "vs_avg_pct": "float"},
  "outlook": {"direction": "string", "key_level": "float"}
}

Agent 2: Risk & Compliance (tag: RISK)

Role: Interprets the dual risk scores, identifies top drivers using SHAP values and percentile extremes.

Schema output:

{
  "score": {"value": "float", "level": "LOW|MEDIUM|HIGH"},
  "top_driver": {"feature": "string", "importance": "float", "value": "float", "signal": "string"},
  "flag": "string (red flag or 'None')",
  "watch": "string (what to monitor)"
}

Agent 3: Report Summary (tag: REPORT)

Role: Synthesizes all agent outputs into a final verdict.

Schema output:

{
  "verdict": "BUY|SELL|HOLD",
  "confidence": "float (0-100)",
  "summary": "string (1-2 sentences with numbers)",
  "metrics": [{"measure": "string", "value": "string", "signal": "string"}],
  "takeaways": ["string", "string", "string"]
}

Prompt Design Principles

  • Maximum 4-6 sentences per agent
  • No hedging language ("suggests", "may", "could")
  • Every sentence contains a number
  • All floats pre-formatted before injection: $308.82 not $308.82000732421875, 1.3% not 0.0125578
  • All prompt data presented as explicit labeled lines: "RSI(14): 91.1", "MA20: $289.22"
  • Agent receives the pre-populated RAG store via a Research Company tool that accepts queries like "recent events AAPL" or "risks AAPL"

JSON Extraction

Since LLMs sometimes wrap JSON in markdown code fences or add trailing text, the _extract_json() function handles parsing:

def _extract_json(text: str) -> dict:
    # Strip ```json and ``` fences
    # Find first { and last }
    # Try JSON.parse
    # If fails, try _extract_all which uses regex for partial extraction
    # Last resort: {"verdict": "HOLD", "summary": "...", _fallback: true}

Cache Fix (Monkey Patch)

CrewAI 1.14.5 has a bug where it passes cache parameters to Groq's API, which unsupported fields. Fixed by patching at module load:

import crewai.llms.cache as _cache
_cache.mark_cache_breakpoint = lambda msg: msg

API Layer & SSE Streaming

File: backend/main.py

SSE Implementation

Uses asyncio.Queue + StreamingResponse:

async def event_stream():
    queue = asyncio.Queue()
    # 1. Fetch market data, features, risk score, chart data, alpha signals, RAG status
    #    Yield each as data: {"type": "...", "data": {...}}
    # 2. Launch agents as background task: stream_analysis(..., queue)
    # 3. Read from queue: agent_start -> agent_complete pairs
    # 4. agent_task emits events via queue.put()
    # 5. On complete or error, break

return StreamingResponse(event_stream(), media_type="text/event-stream")

Key design decisions:

  • Queue-based: Agents put events on an asyncio.Queue, the main coroutine reads and yields. This separates agent execution from SSE framing.
  • Sequential agents: Each agent runs as its own Crew so events can be emitted between agents. A single multi-agent Crew would block until all agents finish.
  • Background task cancellation: On client disconnect (GeneratorExit), the agent task is cancelled to avoid wasted compute.
  • No WebSocket dependency: SSE works with standard HTTP, EventSource handles reconnection, no special proxy config needed.

CORS

All origins allowed (allow_origins=["*"]) — suitable for development.

Agent Context Sharing

stream_analysis() in agents.py receives the market_data_dict and risk_data dict. Each agent gets the full context, but the prompts selectively extract relevant fields. The RAG store is pre-populated before the first agent starts.

Error Handling

  • Market data fetch errors return 404 (no data for symbol)
  • Risk model errors return partial data (score=0, no factors)
  • Agent errors are caught per-agent and replaced with error messages (remaining agents still run)
  • Background tasks are cancelled in finally blocks

Frontend

Files: frontend/src/App.jsx and components in frontend/src/components/

EventSource Progressive Loading

The dashboard uses a single EventSource connection that progressively accumulates state:

const es = new EventSource(`/api/analyze/stream/${symbol}`)
es.onmessage = (event) => {
  const d = JSON.parse(event.data)
  switch (d.type) {
    case 'market_data':    setMarketData(d.data); break
    case 'risk_assessment': setRiskAssessment(d.data); break
    case 'chart_data':     setChartData(d.data); break
    case 'alpha_signals':  setAlphaSignals(d.data); break
    case 'rag_status':     setRagInfo(d.data); break
    case 'agent_start':    setAgentStatus(prev => ({...prev, [d.role]: 'running'})); break
    case 'agent_complete':
      setAgentStatus(prev => ({...prev, [d.role]: 'complete'}))
      setAgentReports(prev => [...prev, {role, tag, output}])
      break
    case 'complete':       es.close(); setStatus('complete'); break
    case 'error':          es.close(); setError(d.message); break
  }
}

Each event type sets its corresponding state. The dashboard renders progressively — RiskGauge and MarketSnapshot appear as soon as market_data and risk_assessment arrive, without waiting for agents.

Component Tree

App.jsx
  +-- StockSearch          (symbol input + analyze button)
  +-- StatusBar            (loading, agent progress, RAG indicator)
  +-- Dashboard (2-column grid)
  |     +-- RiskGauge      (dual scores, SHAP, disagreement banner)
  |     +-- MarketSnapshot (VIX, 10Y, OHLCV, RSI, volume)
  |     +-- AlphaSignals   (whale, exhaustion, retail concentration)
  |     +-- PriceChart     (100-day Recharts area chart)
  +-- Agent Reports
  |     +-- AuditReport x3 (collapsible, routes to structured widgets)
  |           +-- MarketAnalysisReport (trend/levels/RSI grid)
  |           +-- RiskReport (score/driver/flag panels)
  |           +-- FinalReport (verdict badge + metrics + takeaways)
  +-- Article Modal        (click RAG badge -> shows news articles)

Key Component Details

RiskGauge.jsx: Two MiniGauge components side-by-side showing XGBoost and percentile deviation. Each has an info icon with the full formula tooltip. SHAP values shown in contributing factors as (w=0.1729, SHAP=+0.0083). Model disagreement banner renders when model_disagreement: true.

MarketSnapshot.jsx: 10-metric grid. Shows trading date, VIX, 10Y Yield, Close, Open, High, Low, Volume, Return, Volatility, RSI. VIX > 25 colored red, > 18 colored orange.

AlphaSignals.jsx: Three cards: Whale Detection (signal + z-score), Momentum Exhaustion (status), Retail Concentration (retail % + institutional %).

AuditReport.jsx: Detects JSON vs markdown output via tryParseJSON(). Routes to structured components for JSON, falls back to dangerouslySetInnerHTML for raw markdown.

Article Viewer Modal

Clicking the "RAG active (N articles)" badge triggers a fetch to GET /api/rag/articles/{symbol}. Results display in a modal overlay with article title, publisher, and content. Business summary displayed separately.

Styling

Light ivory/white theme (--bg: #f4f2ed, --surface: #ffffff, --text: #1a1a28, --accent: #50509e). Clean professional look with no emojis. All info tooltips use a CSS-styled (i) icon.


End-to-End Walkthrough

Tracing "apple" through the entire system:

  1. Browser: User types "apple" in StockSearch, clicks Analyze
  2. EventSource connects: GET /api/analyze/stream/apple
  3. Backend resolves: resolve_symbol("apple") -> yfinance Search returns {"symbol": "AAPL", "quoteType": "EQUITY", ...} -> returns "AAPL"
  4. Fetch 1y OHLCV: yf.Ticker("AAPL").history(period="1y") -> 252 rows of daily data
  5. Compute features: 9 features + whale regression + exhaustion signal on the full DataFrame
  6. Fetch macro: yf.Ticker("^VIX") + yf.Ticker("^TNX") -> {"vix": 16.59, "tnx": 4.56}
  7. Risk scoring:
    • XGBoost: features -> predict_proba -> 0.0006 (LOW — model sees no anomaly)
    • Percentile: for each of 9 features, rank current vs 52wk history -> RSI at 99.5th percentile, MA50 gap at 99.5th -> weighted average 0.4643 (HIGH)
    • SHAP: booster.predict(dmat, pred_contribs=True) -> RSI contributes +0.0083 to anomaly score, volatility contributes -0.0012
    • Disagreement: XGBoost LOW vs percentile HIGH -> model_disagreement: true
  8. Chart data: Last 100 rows -> [{date, open, high, low, close, volume}]
  9. Alpha signals: Whale z = -0.82 (NONE), retail = 34.2% institutional (normal)
  10. RAG build: Fetch 10-15 AAPL news articles + business summary -> TF-IDF vectorize -> cache
  11. SSE yield market_data -> browser renders MarketSnapshot + RiskGauge
  12. SSE yield risk_assessment -> browser renders dual scores, factors, SHAP, disagreement banner
  13. SSE yield chart_data -> browser renders PriceChart
  14. SSE yield alpha_signals -> browser renders AlphaSignals
  15. SSE yield rag_status -> browser renders "RAG active (10 articles)" badge
  16. Agent 1 (Market Analyst): Receives market data + risk data + RAG tool. Calls Research Company with "recent events AAPL". Gets news about Apple's latest earnings, product launches. Outputs structured JSON with trend="up", RSI=91.1 "overbought", support/resistance levels.
  17. SSE yield agent_start + agent_complete for Market Analyst -> browser renders MarketAnalysisReport
  18. Agent 2 (Risk & Compliance): Same context + Agent 1's output. Calls Research Company with "risks AAPL". Outputs JSON with score, top driver, flag.
  19. Agent 3 (Report Summary): All previous context. Outputs JSON with verdict, confidence, metrics, takeaways.
  20. SSE yield complete -> browser sets status="complete", status bar hides
  21. User clicks RAG badge -> fetch("/api/rag/articles/AAPL") -> modal shows 10 articles

Quick Start

Prerequisites

  • Python 3.14+
  • Node.js 18+
  • uv package manager (or pip)

1. Setup

uv venv
source .venv/bin/activate
uv pip install -r requirements.txt

cd frontend && npm install && cd ..

2. Configure

echo "GROQ_API_KEY=gsk_your_key_here" > .env

Get a free API key at https://console.groq.com

3. Train the risk model (one-time)

uv run python backend/train_model.py

4. Run

Terminal 1 - Backend API:

uv run uvicorn backend.main:app --host 0.0.0.0 --port 8000

Terminal 2 - Frontend dashboard:

cd frontend && npx vite --port 5173

Open http://localhost:5173 and enter a symbol or company name (e.g., apple, TSLA, RELIANCE.NS).


API Reference

GET /api/analyze/stream/{symbol} (primary — SSE)

Returns Server-Sent Events. Stream ends with {"type": "complete"}.

Event When Contents
market_data ~0.5-1s OHLCV, features, macro (VIX/TNX), whale signals, resolved symbol
risk_assessment ~1ms after Dual scores, SHAP, factors, model disagreement
chart_data immediate 100-day OHLCV array
alpha_signals immediate Whale z-score/signal, retail %, exhaustion flag
rag_status ~0.1s after Article count, indexed status
agent_start per agent Role name + tag
agent_complete per agent (~2-4s) Role + tag + structured JSON output
complete end Signals termination

GET /api/analyze/{symbol} (non-streaming)

Returns full JSON after all agents complete. Same structure as accumulated SSE payload.

GET /api/rag/articles/{symbol}

Returns cached RAG store articles:

{
  "symbol": "AAPL",
  "articles": [
    {"type": "news", "title": "...", "publisher": "...", "content": "..."},
    {"type": "business", "content": "..."}
  ]
}

GET /api/alphas/{symbol}

Returns whale signals, retail concentration, and market data for a single stock.

GET /api/alphas/scan

Scans a 16-stock universe and returns results ranked by alpha score (composite of whale + retail + exhaustion, each contributing 1-2 points, max 4).

GET /api/health

Returns {"status": "ok"}.


Performance Benchmarks

Environment

Parameter Value
LLM groq/llama-3.3-70b-versatile (Groq Cloud free tier)
ML Model XGBoost + Percentile Deviation + SHAP
Backend FastAPI + Uvicorn (single worker)
Python 3.14.3
Hardware Apple Silicon (M-series)
Data yfinance (no cache warm)

Sequential Baseline (3 symbols)

Symbol Time XGBoost Percentile Severity
AAPL 8.9s 0.06% (LOW) 46.43% (HIGH) HIGH
GOOGL 7.8s 0.02% (LOW) 61.10% (HIGH) HIGH
TSLA 11.2s 0.00% (LOW) 42.10% (HIGH) HIGH

Avg: 9.3s | Median: 8.9s | Min: 7.8s | Max: 11.2s

Latency Breakdown

Stage Time % of total
yfinance data fetch + feature engineering ~0.5-1s ~8%
XGBoost + percentile + SHAP inference <1ms ~0%
RAG build (TF-IDF vectorize) ~50-100ms ~1%
Agent 1 - Market Analyst (LLM) ~2-4s ~30%
Agent 2 - Risk & Compliance (LLM) ~2-4s ~30%
Agent 3 - Report Summary (LLM) ~2-4s ~30%

Concurrent Throughput (4 requests)

Metric Value
Total wall time 5.1s
AVG per request 2.0s
Throughput 47.1 req/min

Sequential Throughput (10 symbols)

Metric Value
Total time 25.8s
AVG 2.6s
MEDIAN 1.4s
Throughput 23.3 req/min

Key Observations

  1. Model divergence: Percentile deviation consistently scores HIGH (40-70%) while XGBoost scores near 0%. The binary anomaly classifier sees rare events; the percentile score sees current market extremes vs 52-week history. They measure different things.

  2. Cold vs warm: First request to a symbol takes 8-11s (yfinance uncached). Subsequent requests (with yfinance data cached by pandas-datareader) take 1-4s.

  3. Concurrency scales: 4 concurrent requests complete in ~5s wall time vs ~37s sequential (8x improvement). FastAPI async handles this without extra workers.

  4. LLM is the bottleneck: ~85-90% of latency comes from 3 sequential Groq API calls. ML inference (XGBoost + percentile + SHAP combined) is sub-millisecond.

  5. RAG is negligible: TF-IDF vectorization takes ~50ms for 10-15 documents. Agent tool calls (cosine similarity lookup) are ~2ms each.

  6. Rate limits: Free Groq tier limits to ~5-6 requests before RateLimitError. Paid tier removes this.


Project Structure

.
├── backend/
│   ├── main.py                 # FastAPI entry, SSE streaming, all REST endpoints
│   ├── agents.py               # CrewAI agents, prompts, JSON schemas, extraction
│   ├── market_data.py          # yfinance fetch, 9 features, macro, name resolution
│   ├── risk_model.py           # XGBoost + percentile + SHAP + disagreement
│   ├── train_model.py          # Model training (16 stocks + synthetic)
│   ├── alphas.py               # Whale regression, exhaustion, retail concentration
│   ├── rag.py                  # TF-IDF RAG: fetch, chunk, vectorize, search, cache
│   ├── stock_risk_model.pkl    # Trained XGBoost model
│   └── __pycache__/
├── frontend/
│   ├── src/
│   │   ├── index.jsx               # React entry
│   │   ├── App.jsx                 # EventSource main, progressive state, article modal
│   │   ├── App.css                 # Light ivory/white theme
│   │   └── components/
│   │       ├── StockSearch.jsx         # Input + analyze button
│   │       ├── RiskGauge.jsx           # Dual gauges, SHAP, disagreement banner
│   │       ├── MarketSnapshot.jsx      # VIX, 10Y, OHLCV, RSI, volume grid
│   │       ├── PriceChart.jsx          # Recharts area chart
│   │       ├── AlphaSignals.jsx        # Whale + retail cards
│   │       ├── AuditReport.jsx         # JSON/markdown detection, routing
│   │       ├── MarketAnalysisReport.jsx # Trend/levels/RSI grid
│   │       ├── RiskReport.jsx           # Score/driver/flag panels
│   │       └── FinalReport.jsx          # Verdict, metrics table, takeaways
│   ├── vite.config.js           # /api proxy to :8000
│   └── package.json
├── fraud-pipeline/             # Preserved original fraud detection
├── archive/                    # Preserved original entry points
├── requirements.txt            # Pinned Python dependencies
└── .env                        # GROQ_API_KEY (not committed)

Tech Stack

Category Technology
Orchestration CrewAI 1.14.5
LLM Inference Llama 3.3 70B via Groq Cloud
ML Model XGBoost 3.2 (scikit-learn 1.8)
Backend FastAPI 0.136 + Uvicorn 0.48
Frontend React 18 + Vite 5 + Recharts 2
Data yfinance 1.4, Pandas 3.0, NumPy 2.4
Language Python 3.14

License

MIT

About

Multi-agent stock analysis engine with XGBoost risk scoring, SHAP, TF-IDF RAG, and CrewAI agents

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages