Skip to main content

Quorai — a quorum of AI agents deliberating trading decisions.

Project description

Quorai

Quorai Logo

A quorum of AI agents deliberating trading decisions. Pronounced "KWOR-eye" (quorum + AI).

GitHub · About · UI

Python License: MIT LangGraph CI

A multi-agent AI trading system where specialized LLM analyst agents deliberate and vote on trading decisions through a portfolio manager. Built on LangGraph and LangChain. For educational purposes only — not intended for real trading or investment.

Quickstart

Prerequisites: Python 3.11+ and uv.

# Install uv (skip if already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

1. Clone and install

git clone https://github.com/quorai/quorai-app.git
cd quorai-app
uv sync

2. Set API keys — you need exactly two

cp .env.example .env

Edit .env and fill in:

Variable Where to get it Cost
FINNHUB_API_KEY finnhub.io/dashboard Free tier
One LLM key (pick any) OpenRouter OPENROUTER_API_KEY · Groq GROQ_API_KEY · DeepSeek DEEPSEEK_API_KEY · Anthropic ANTHROPIC_API_KEY Varies

All other keys in .env.example are optional.

3. Run your first backtest

uv run backtester \
    --tickers AAPL,MSFT \
    --model deepseek/deepseek-chat \
    --model-provider OpenRouter \
    --show-reasoning

Expect ~1–3 min per simulated trading day. The output includes per-analyst signals, debate summaries, portfolio decisions, and performance metrics (Sharpe, alpha vs SPY).

For all flags see Usage — Backtesting. For the MCP server (run analysts from Claude Code/Desktop/Cursor) see MCP server.

Model choice matters. All agents produce structured JSON output and the portfolio manager calls tools via LangChain. Use a model with strong instruction-following: Claude (Anthropic), GPT-4o (OpenAI), Gemini 2.5 Flash/Pro (Google), or DeepSeek Chat. Weaker models may produce malformed JSON that causes agent failures.

Contents

Features

  • 25 analyst agents — value, growth, macro, technical, fundamentals, sentiment, risk, and more
  • Famous investor personas — simulations of Buffett, Munger, Ackman, Burry, Wood, Dalio, Simons, Lynch, and others
  • 14 LLM providers — OpenAI, Anthropic, Groq, Gemini, DeepSeek, xAI, OpenRouter, Ollama (local), Alibaba, Azure OpenAI, GigaChat, Meta, Mistral, Kimi
  • Backtesting engine — replay historical data with full agent deliberation and portfolio metrics
  • Live / paper trading — execute via Alpaca with optional Telegram approval gate
  • Group-level debate node — collapses 25 analyst signals into 6 strategy groups via confidence-weighted aggregation; an LLM moderator summarises contested tickers
  • Market-regime selection — classifies the current SPY regime (bull/bear/risk-off/neutral) each day and narrows the active analyst subset accordingly
  • Conviction-weight feedback loop — tracks each agent's rolling directional hit-rate; high-accuracy agents receive proportionally more weight in the debate aggregation
  • Signal logging + forward-return labeling — persists every per-agent-per-ticker signal to JSONL during a backtest run; a separate labeler attaches 1d/5d/20d forward returns so hit-rates can be computed
  • Token-usage telemetry — captures and accumulates LLM token counts per agent across the full backtest run; Anthropic prompt caching is applied automatically and cache-read/creation tokens are surfaced separately
  • A/B comparison + gate ablation harness — runs two backtest configs back-to-back and prints a side-by-side metrics table; ablation subcommand disables each PM gate in turn and reports the return/Sharpe delta vs a gated baseline; llm-ablation reruns backtests under all-LLM, all-math-twin, PM-rule, and no-debate-context modes and prints a P&L comparison table (optional leave-one-out per persona via --full-loo)
  • LLM-vs-math measurement harness — every analyst agent computes a deterministic "math twin" signal alongside the LLM call; llm-vs-math reports per-agent agreement rate, LLM override accuracy, and LLM premium (ΔIC / Δhit_rate / Δalpha vs the math twin); debate-impact replays group aggregation on both signal sets and reports group-stance flip rate, panel-tilt delta, and contested-set Jaccard; calibrate-math grid-searches per-agent score thresholds to maximise IC and writes feedback/math_thresholds.json
  • Realistic cost model — optional slippage, commission, and short-borrow costs charged to the fill path (--slippage-bps, --commission-bps, --borrow-bps-annual); propagate automatically through NAV → Sharpe/alpha
  • Alpha attributionattribution subcommand reads a labeled signal log and reports per-analyst hit-rate, directional spread (signal IC), and confidence-weighted score, ranked best-first, with group-level roll-ups
  • Per-agent model routing — override model and provider per analyst via --agent-model AGENT=model/PROVIDER; handled by RunRequest (src/llm/request.py)
  • Parallel per-ticker execution — set QUORAI_PARALLEL_TICKERS=N to run N tickers concurrently via a thread pool (src/utils/concurrency.py)
  • SEC EDGAR fundamentals — point-in-time XBRL data via a local SQLite store (.cache/sec_fundamentals.db); eliminates yfinance look-ahead bias on historical share counts and financial statements. Seed with experiments/seed_sec_fundamentals.py; falls through to yfinance for unseeded tickers.
  • Regime-gated allocation — the portfolio manager deterministically filters proposed LLM actions by the detected SPY regime: bull_trend removes short when quant/growth groups are bullish; bear_trend removes buy when quant/quality groups are bearish; risk_off blocks both buy and short
  • MCP server — exposes the full analyst panel as a Model Context Protocol server (quorai-mcp); one-line install for Claude Code, Claude Desktop, Cursor, Cline, and any other MCP host

How it works

Market Data → Analyst Agents → Portfolio Manager → Order Execution
                  ↑                   ↑
           (LangGraph nodes)   (deliberation graph)
flowchart LR
    MD[Market Data] --> PC[PipelineContext<br/>regime + conviction weights]
    PC --> SN([start_node])
    SN --> A1[analyst_1]
    SN --> A2[analyst_2]
    SN --> AN[... analyst_25]
    A1 --> DN[debate_node<br/>6 strategy groups]
    A2 --> DN
    AN --> DN
    DN --> RM[risk_management_agent]
    RM --> PM[portfolio_manager]
    PM --> OE[Order Execution]

Each trading cycle:

  1. Financial data is fetched (price, fundamentals, news, macro indicators)
  2. Each analyst agent runs as a LangGraph node and produces a signal with reasoning
  3. A portfolio manager agent weighs the signals and issues buy/hold/sell orders
  4. Orders are executed via Alpaca (live) or simulated (backtest)

Analyst roster

Category Agents
Value Buffett, Munger, Ackman, Burry, Greenblatt, Pabrai, Damodaran
Growth Cathie Wood, Phil Fisher, Peter Lynch, Jhunjhunwala
Macro Dalio, Druckenmiller, Marks
Quant Simons, Asness, Seykota
Sentiment News sentiment, social sentiment
Risk Risk manager, Taleb (tail-risk)
Special Bull/bear debate node
mindmap
  root((Strategy Groups))
    Deep Value
      Ben Graham
      Michael Burry
      Mohnish Pabrai
      Joel Greenblatt
    Quality Compounders
      Aswath Damodaran
      Charlie Munger
      Phil Fisher
      Warren Buffett
    Growth & Catalyst
      Bill Ackman
      Cathie Wood
      Peter Lynch
      Growth Analyst
    Macro & Cycle
      Nassim Taleb
      Rakesh Jhunjhunwala
      Stanley Druckenmiller
      Ray Dalio
      Howard Marks
    Quant & Systematic
      Technical Analyst
      Cliff Asness
      Ed Seykota
      Jim Simons
    Sentiment & Analytical
      Fundamentals Analyst
      News Sentiment Analyst
      Sentiment Analyst
      Valuation Analyst

Architecture

The pipeline runs as a LangGraph StateGraph: start_node fans out to all selected analyst nodes in parallel, feeds into a debate_node (conviction-weighted group aggregation), then risk_management_agent (pure maths, no LLM), then portfolio_manager (LLM decision). Regime selection and conviction-weight loading happen in src/orchestration/preflight.py:PipelineContext before the graph is invoked each day.

start_node → [analyst_1 … analyst_25] → debate_node → risk_management_agent → portfolio_manager → END

Debate node

The debate node (src/agents/debate_node.py) runs in two phases:

  1. Group aggregation (deterministic). The 25 analyst signals are collapsed into 6 strategy groups: deep_value, growth_and_catalyst, macro_and_cycle, quant_systematic, quality_compounders, sentiment_and_analytical. Within each group, signals are confidence-weighted (not majority-voted): each agent's stance (bullish → +1, neutral → 0, bearish → −1) is multiplied by its confidence (and optionally by its conviction weight from weights.json), then averaged.

    Weighted stance Group signal
    ≥ +0.25 bullish
    ≤ −0.25 bearish
    otherwise neutral
  2. Moderator synthesis (LLM). Only for contested tickers — at least one bullish group AND at least one bearish group — an LLM moderator receives the group stances and their top-2 arguments, and returns a DebateSummary with each group's one-sentence position, the root structural disagreement, and a consensus_strength label (strong_agreement / mixed / structural_split).

Individual agents do not argue with each other; the "debate" is between the six group-level positions, and only contested tickers incur the extra LLM call.

See ARCHITECTURE.md for the full design — data layer, LLM dispatch, backtesting internals, regime classifier, conviction-weight feedback loop, token telemetry, live trading layer, and per-ticker parallelism. See docs/math.md for all quantitative formulas.

Setup

1. Install dependencies

uv sync

2. Configure API keys

cp .env.example .env

Edit .env and add your keys:

# At least one LLM provider is required
OPENAI_API_KEY=...
ANTHROPIC_API_KEY=...
GROQ_API_KEY=...
OPENROUTER_API_KEY=...

# Financial data (Finnhub)
FINNHUB_API_KEY=...

3. Seed SEC fundamentals (recommended for accurate backtests)

The fundamentals tools consult a local SQLite cache of SEC EDGAR XBRL data before falling back to yfinance. Without this cache yfinance's .info returns current share counts and financial metrics for historical dates, introducing look-ahead bias in backtests.

# Required env var — SEC fair-access policy requires a contact identifier
export QUORAI_SEC_USER_AGENT="your.email@example.com"

# Seed a specific subset (fast, ~10-30 s)
uv run python experiments/seed_sec_fundamentals.py --tickers AAPL,MSFT,NVDA

# Seed the full US market (~10 000 tickers, 3-4 hours, 5-10 GB)
uv run python experiments/seed_sec_fundamentals.py

# Skip tickers last synced within N days
uv run python experiments/seed_sec_fundamentals.py --refresh-older-than 30

# Dry run — print what would be downloaded without writing
uv run python experiments/seed_sec_fundamentals.py --dry-run --tickers AAPL

The seeder respects the SEC's 10 req/s rate limit automatically. Tickers not in the cache are silently fetched from yfinance at run-time.

MCP server

Quorai ships a Model Context Protocol server so any MCP-compatible AI tool — Claude Code, Claude Desktop, Cursor, Cline, Continue, VS Code Copilot — can invoke the analyst panel as a tool call.

Install

Claude Code (one-liner):

claude mcp add quorai uvx quorai-mcp

Claude Desktop / Cursor / any MCP host — add to your mcpServers config:

{
  "mcpServers": {
    "quorai": {
      "command": "uvx",
      "args": ["quorai-mcp"],
      "env": {
        "OPENROUTER_API_KEY": "your-key-here",
        "FINNHUB_API_KEY": "your-key-here"
      }
    }
  }
}

The first cold-start with uvx downloads Quorai and its dependencies (~30–90 s). Subsequent starts are instant.

Available tools

Tool Description
run_panel Run the full 25-analyst panel for one or more tickers. Returns signals, debate summary, risk assessment, and portfolio decisions. Takes 2–5 min per run.
list_analysts List all 25 analyst personas with their investing styles and strategy groups.
get_analyst_info Get full metadata for one analyst by key (e.g. warren_buffett).
run_single_analyst Run a single analyst and return its per-ticker signals.

Routing analysts to specific models

Pass agent_models to run_panel to override the LLM used per analyst. Keys are analyst keys (or "*" for all); values are [model_slug, provider]:

run_panel(
  tickers=["AAPL", "NVDA"],
  agent_models={"*": ["nousresearch/hermes-4-70b", "OpenRouter"]}
)

Or use local Ollama (free, no API key needed):

run_panel(
  tickers=["AAPL"],
  agent_models={"*": ["hermes-4-70b", "Local"]}
)

Local dev (without PyPI)

claude mcp add quorai-dev "uv run --directory /path/to/quorai-app quorai-mcp"

Usage

Backtesting

uv run backtester \
    --tickers AAPL,MSFT \
    --model deepseek/deepseek-chat \
    --model-provider OpenRouter \
    --show-reasoning

You can also invoke the module directly: uv run python -m src.backtesting.

Backtester starting up — LLM mode banner, analyst list, and debate node output

Backtest startup: LLM-mode banner, analyst roster, and debate-node aggregation.

Key flags:

  • --tickers — comma-separated list of tickers (required)
  • --model — model name (required)
  • --model-provider — provider string; bypasses catalog, accepts any OpenRouter/provider slug
  • --analysts — comma-separated analyst IDs (default: all)
  • --end-date — end date YYYY-MM-DD (default: today)
  • --start-date — start date YYYY-MM-DD; mutually exclusive with --days
  • --calendar-days / --days — number of calendar days to look back from --end-date (default: 30); mutually exclusive with --start-date
  • --initial-capital — starting cash (default: 100 000)
  • --show-reasoning — print each agent's reasoning
  • --temperature — LLM temperature override
  • --use-regime-selection — classify SPY regime per day and narrow analysts to the relevant group
  • --use-conviction-weights — weight agents by rolling directional hit-rate (requires src/feedback/weights.json from a prior scored run)
  • --risk-profile — choose one of five risk presets: conservative, cautious, balanced (default), aggressive, speculative. Controls per-ticker position sizing and notional/loss-limit caps together.
  • --seed — RNG seed for reproducibility (default: 42)
  • --log-dir — override artifact directory (default: logs/backtest)
  • --run-label — tag embedded in run_id and manifest for later filtering
  • --slippage-bps — per-side fill price impact in basis points (default: 0; recommended: 5 for liquid US equities)
  • --commission-bps — per-trade commission on notional in bps (default: 0; recommended: 2)
  • --borrow-bps-annual — annualised short-borrow carry in bps, accrued daily (default: 0; recommended: 50)
  • --no-analyze — skip the automatic post-backtest analytics suite (debate-impact + labeling + llm-vs-math) that runs by default after each backtest

Post-backtest analytics: after the backtest summary is printed, the engine automatically runs a three-step analytics suite — school-debate impact [1/3], forward-return labeling + conviction weights [2/3], and LLM-vs-math per-agent analysis [3/3]. Each step is guarded so failures never crash the run. Two remaining manual steps (calibrate-math, llm-ablation) are printed as ready-to-paste commands. Use --no-analyze to suppress the entire suite.

Post-backtest analytics output — debate impact, labeling, LLM-vs-math dual-attribution table, and remaining manual steps

Automatic post-backtest analytics: debate impact, forward-return labeling, and LLM-vs-math dual-attribution table with remaining manual steps.

A/B comparison

uv run backtester compare \
    --tickers AAPL,MSFT \
    --model deepseek/deepseek-chat \
    --model-provider OpenRouter \
    --mode regime    # full analysts vs regime subset

# --mode weights    uniform weights vs conviction weights
# --mode both       run both comparisons sequentially

Labeling signals and computing conviction weights

The feedback subcommand labels a signal log with forward returns and writes per-agent conviction weights:

uv run backtester feedback \
    --signal-log logs/backtest/signals/signals-<run-id>.jsonl

Flags:

  • --signal-log — path to JSONL signal log from a backtest or live run (required)
  • --horizon — forward-return horizon in trading days (default: 5)
  • --window — rolling scoring window in trading days (default: 60)
  • --output-dir — directory for labeled log and accuracy report (default: same directory as signal log)

Writes src/feedback/weights.json and accuracy_report.json. Re-run backtesting with --use-conviction-weights to apply the computed weights.

Attribution report

After labeling a signal log (see feedback above), run attribution to rank analysts by signal quality:

uv run backtester attribution \
    --signal-log logs/backtest/signals/labeled_signals-<run-id>.jsonl

Prints a ranked table of hit-rate, directional spread (mean bullish return − mean bearish return), and confidence-weighted score per analyst and strategy group. Writes attribution_<log-stem>.json next to the log file.

Flags:

  • --signal-log — path to the labeled JSONL (produced by feedback; required)
  • --horizon — forward-return horizon to use for scoring (default: 5)
  • --output — override output JSON path

Gate ablation

Quantify each PM gate's contribution by running baseline vs gate-disabled variants:

uv run backtester ablation \
    --tickers AAPL,MSFT \
    --model deepseek/deepseek-chat \
    --model-provider OpenRouter \
    --slippage-bps 5 --commission-bps 2

Runs four sequential backtests (baseline + no-regime-gate + no-panel-gate + no-min-hold-gate) and prints a delta table showing return/Sharpe change vs the gated baseline. Accepts all standard backtest flags; cost flags are forwarded so churn deltas are friction-adjusted.

See docs/backtest-output.md for a guide to reading and interpreting the output metrics (including the new post-backtest analytics blocks).

LLM-vs-math analysis

After labeling a signal log (see feedback above), compare what each analyst's LLM layer adds over its deterministic math twin:

uv run backtester llm-vs-math \
    --signal-log logs/backtest/signals/labeled_signals-<run-id>.jsonl

Flags:

  • --signal-log — path to the labeled JSONL (required)
  • --horizon — forward-return horizon in trading days (default: 5)
  • --output — override output path for the JSON report

Prints per-agent agreement rate, LLM override accuracy, and a dual-attribution table (LLM vs math-twin hit-rate, directional spread, Δspread, Δalpha). Writes llm_vs_math_<log-stem>.json next to the log file.

Debate impact

Replay group aggregation on LLM vs math-twin signals to quantify how much the LLM reshapes the school debate:

uv run backtester debate-impact \
    --run-id <run_id>

Flags:

  • --run-id — backtest run_id (required)
  • --log-dir — root log directory (default: logs)
  • --modebacktest or live (default: backtest)
  • --output — path for the JSON report output

Prints group-stance flip rate per strategy group, mean panel-tilt delta |LLM − math|, and contested-set Jaccard (agreement on which tickers are structurally contested).

Math-twin calibration

Grid-search per-agent score thresholds that maximise directional IC, then persist them for the next backtest run:

uv run backtester calibrate-math \
    --signal-log logs/backtest/signals/labeled_signals-<run-id>.jsonl

Flags:

  • --signal-log — path to the labeled JSONL with math_score_pct fields (required)
  • --horizon — forward-return horizon in trading days (default: 5)
  • --min-samples — minimum signal count per agent to calibrate (default: 10)
  • --output — override output path (default: feedback/math_thresholds.json at repo root)

Writes per-agent bull_cut/bear_cut thresholds to feedback/math_thresholds.json; the math twin picks them up automatically on the next backtest run (falls back to defaults 0.60/0.40 if the file is absent). Recommended once you have ≥ 3 labeled runs.

LLM P&L ablation

Rerun backtests under different LLM-usage modes and compare P&L side-by-side:

uv run backtester llm-ablation \
    --tickers AAPL,MSFT \
    --model deepseek/deepseek-chat \
    --model-provider OpenRouter \
    --start-date 2026-01-01 --end-date 2026-03-31

Runs four sequential backtests (Baseline / All-math / PM-rule / No-debate-context) and prints a delta table. Add --full-loo to also run ~19 leave-one-out variants (one persona at a time on the math twin) — expensive. Accepts all standard backtest flags.

The ablation modes are controlled via env gates that can also be set manually for any backtest or live run:

Env var Values Effect
QUORAI_LLM_MODE llm (default), math, math_only:<agents>, math_except:<agents> Switch individual or all persona agents to their math twin (no LLM call)
QUORAI_PM_MODE llm (default), rule Replace the portfolio-manager LLM call with a deterministic tilt-following rule
QUORAI_PM_DEBATE_CONTEXT 1 (default), 0 Omit the debate-moderator summary from the PM prompt

Live / Paper Trading

uv run python src/live_trading.py \
    --tickers AAPL,MSFT,NVDA \
    --model openrouter/anthropic/claude-3.5-sonnet \
    --model-provider OpenRouter \
    --dry-run

Key flags:

  • --tickers — comma-separated list of tickers (required)
  • --model — model name (required)
  • --model-provider — provider string (required)
  • --analysts — comma-separated analyst IDs to include (default: all)
  • --use-regime-selection — classify today's SPY regime and narrow analysts to the matching strategy groups (same logic as BacktestEngine)
  • --use-conviction-weights — apply per-agent conviction weights from src/feedback/weights.json; warns if the file is absent but does not abort
  • --risk-profile — choose one of five risk presets: conservative, cautious, balanced (default), aggressive, speculative. Controls per-ticker position sizing and RiskGate caps (notional, quantity, daily loss limit) together. See the safety table below for the values per preset.
  • --show-reasoning — print each agent's reasoning and debate summaries
  • --agent-model AGENT=model[:PROVIDER] — override model for a specific analyst; repeatable; use *=model/PROVIDER for a wildcard fallback. Also reads QUORAI_AGENT_MODELS_JSON env var (JSON dict).
  • --no-signal-log — disable writing logs/live/signals/signals-YYYY-MM-DD-live.jsonl (signal logging is on by default)
  • --dry-run — print decisions without submitting orders
  • --confirm — skip interactive confirmation prompt
  • --require-approval — send orders to Telegram for human approval before submitting
  • --auto-submit — submit immediately and send an execution report to Telegram afterwards
  • --allow-queue — allow running before market open on a valid trading day; orders are submitted as DAY market orders and queue for the opening cross (still skips weekends and holidays)
  • --catch-up — missed-cron recovery: fetches prior-close equity from Alpaca portfolio history to use as the daily-loss baseline when no SOD equity file exists
  • --force — skip the market-open check entirely (useful for development/testing)
  • --margin-requirement — margin requirement fraction (default: 0.0)
  • --temperature — LLM temperature override

The signal JSONL feeds the same feedback/labeler.py → scorer.py → weights.json pipeline used in backtesting, so conviction weights improve over time as live-run history accumulates.

Telegram approval gate

When --require-approval is set and TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID are configured, each run sends the proposed orders as an inline message. Tap Approve ✅ or Reject ❌ within the timeout window (default 30 min) to decide whether orders are submitted.

Required .env keys:

TELEGRAM_BOT_TOKEN=...        # BotFather token
TELEGRAM_CHAT_ID=...          # your chat / group ID
TELEGRAM_APPROVAL_TIMEOUT_SECONDS=1800  # optional, default 30 min

Bot command inbox

You can send plain-text messages to the bot at any time. They are read at the start of the next run and take effect immediately:

Message (case-insensitive) Effect
accept only sales / only sells Suppress all buy orders for the next run only
skip next day / skip next Skip the next scheduled run entirely
pause / stop trading / skip until continue Pause all runs until you send continue
continue / resume Clear an active pause

The bot replies with a confirmation message when a command is recognised. Command state is persisted in logs/command_state.json so it survives process restarts and cron jobs.

Experiments

The regime evaluation harness sweeps 10 curated (period × ticker-set) scenarios across BULL/BEAR/RISK_OFF/NEUTRAL regimes and writes a markdown summary to experiments/results/eval-<date>.md.

# Run all 10 scenarios (default model: google/gemini-2.5-flash-lite via OpenRouter)
uv run python experiments/run_scenarios.py

# Use a different model
uv run python experiments/run_scenarios.py \
    --model deepseek/deepseek-chat --model-provider OpenRouter

# Run a single named scenario
uv run python experiments/run_scenarios.py --scenarios bull-megacap-2024Q1

# Smoke run — 2 tickers × 10 days (~6% of full cost)
uv run python experiments/run_scenarios.py --max-tickers 2 --max-days 10

# Skip scenarios that already have a completed manifest
uv run python experiments/run_scenarios.py --skip-existing

# Regenerate the report without re-running any scenarios
uv run python experiments/run_scenarios.py --summary-only

# Disable features for ablation testing
uv run python experiments/run_scenarios.py --no-regime-selection --no-conviction-weights

# Force fresh LLM calls (after editing prompts)
uv run python experiments/run_scenarios.py --no-llm-cache

The report groups results by observed SPY regime and flags notable outliers (|α vs SPY| > 5pp).

Safety mechanisms

The Alpaca client (src/broker/alpaca_client.py:66-67) refuses to construct a live-trading client unless ALPACA_PAPER=True, making this paper-trading software by construction. Within that sandbox, multiple caps apply:

Layer Source Default (balanced)
Per-ticker volatility cap src/agents/risk_manager.py 5–25% of NAV (lower for high-vol or correlated names)
Cycle-wide cash guard src/agents/portfolio_manager.py:111-149 Cumulative buys across all tickers cannot exceed available cash
Backtest cash invariant src/backtesting/portfolio.py:82-106 Over-budget buys truncated to cash / price
Per-order notional cap src/live/risk_gate.py (MAX_ORDER_NOTIONAL) $10,000
Per-order quantity cap src/live/risk_gate.py (MAX_ORDER_QTY) 1,000 shares
Daily loss limit src/live/risk_gate.py (DAILY_LOSS_LIMIT_PCT) 5% of start-of-day equity
Kill switch src/config.py (KILL_SWITCH) Off by default; flip to reject all orders immediately
Telegram approval gate (opt-in) src/live_trading.py (--require-approval) Fail-closed: missing creds, Telegram error, reject, or timeout all abort with zero orders submitted
Prior-run idempotency re-prompt src/live/idempotency_guard.py:34 (TelegramPriorRunApprover) Re-asks via Telegram if today already has submissions; fail-closed if Telegram unreachable

The three RiskGate caps and the position-sizing base_limit are bundled into five presets selectable via --risk-profile:

Profile base_limit Notional cap Qty cap Daily loss limit
conservative 10% $5,000 500 shares 2%
cautious 15% $7,500 750 shares 3%
balanced (default) 20% $10,000 1,000 shares 5%
aggressive 30% $20,000 2,000 shares 8%
speculative 50% $50,000 5,000 shares 15%

Individual caps are still overridable via env vars (see src/config.py). The --risk-profile flag takes precedence over the env defaults for that run only.

Known limitations

  • Notional cap is per-order, not per-cycle. With N tickers, up to N × $10,000 of orders can be submitted in a single cycle before any cap fires.
  • No portfolio-level concentration cap. Four low-vol uncorrelated names can each hit the 25% per-ticker ceiling and effectively go all-in across the basket.
  • Daily loss limit re-baselines if SOD equity is missing. If logs/sod_equity.json is absent at run-time (e.g. after a crash), src/live/runner.py:99-103 resets the baseline to current (already drawn-down) equity, defeating the limit for that day.
  • Sub-$1 fractional buys are silently dropped. src/live/executor.py:83 rounds qty to 3 decimals; a tiny allocation on a high-priced stock rounds to 0.000 and is classified as skipped with no warning.
  • No fractionable pre-check in the Alpaca client. Fractional qty on a non-fractionable asset fails after order submission rather than being caught early (src/broker/alpaca_client.py:118-128).
  • Backtest silent truncation. Over-budget buys in src/backtesting/portfolio.py:93-105 partially fill without any log line, which can mask LLM or risk-manager miscalculations in backtest results.

The paper-only hard-stop in alpaca_client.py is the base safety net. Running with --require-approval adds a human-in-the-loop gate on top of it. The limitations above are documented, not fixed.

Running tests

uv run python -m pytest

Use python -m pytest, not uv run pytest — the latter invokes a stale venv shebang that resolves to the wrong Python.

Troubleshooting

uv run pytest fails or runs the wrong Python
Use uv run python -m pytest instead.

--use-conviction-weights warns about a missing weights.json
Conviction weights are computed from a prior backtest's signal log. Run a backtest first, then use the feedback subcommand:

uv run backtester feedback \
    --signal-log logs/backtest/signals/signals-<run-id>.jsonl

This writes src/feedback/weights.json. Re-run with --use-conviction-weights to apply the weights.

Sharpe / Sortino look extreme on a short backtest
Both ratios are annualised from daily returns. A handful of data points isn't statistically meaningful — use a test window of at least several months before drawing conclusions.

Live trading fails to connect to Alpaca
Ensure ALPACA_API_KEY, ALPACA_API_SECRET, and ALPACA_BASE_URL are set in .env. For paper trading, ALPACA_BASE_URL should be https://paper-api.alpaca.markets.

Telegram approval bot doesn't respond
Ensure TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are set. Verify the bot is started (/start) and you are in the correct chat. The default timeout is 30 minutes — increase TELEGRAM_APPROVAL_TIMEOUT_SECONDS if needed.

Python version

Python 3.11+ required (>=3.11 in pyproject.toml; .python-version pins 3.11). CI runs 3.12.

Changelog

See CHANGELOG.md.

Disclaimer

This project is for educational and research purposes only.

  • Not intended for real trading or investment
  • No investment advice or guarantees provided
  • Creator assumes no liability for financial losses
  • Past performance does not indicate future results

The agent modules named after real investors (Buffett, Munger, Ackman, Burry, Wood, Asness, Dalio, Marks, Simons, Druckenmiller, Seykota, Greenblatt, Damodaran, Fisher, Lynch, Jhunjhunwala, Pabrai, Taleb, and others) are educational simulations that approximate publicly stated investment philosophies derived from books, interviews, and public writings. They are not affiliated with, endorsed by, or representative of the actual individuals or their organisations.

Acknowledgements

  • virattt/ai-hedge-fund — inspiration for the initial persona-agent architecture and LLM prompt design.
  • TauricResearch/TradingAgents — the bull/bear debate concept that inspired src/agents/debate_node.py.
  • Finnhub — insider trades and company news API.
  • yfinance — prices, financial metrics, and fundamental data.
  • Alpaca — paper and live trading API.

Companion projects

  • quorai-ui — read-only Next.js web UI for inspecting trading runs and experiments. Reads the cycle bundles quorai-app writes to logs/<mode>/runs/ and presents them as browsable, comparable views; no write access to the trading system. Point QUORAI_LOGS_DIR at this repo's logs/ directory and run npm run dev on port 3030.

License

This project is licensed under the MIT License — see LICENSE for details. Third-party notices (including the upstream MIT license for ai-hedge-fund material) are in THIRD_PARTY_NOTICES.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

quorai-2.2.0.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

quorai-2.2.0-py3-none-any.whl (314.2 kB view details)

Uploaded Python 3

File details

Details for the file quorai-2.2.0.tar.gz.

File metadata

  • Download URL: quorai-2.2.0.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for quorai-2.2.0.tar.gz
Algorithm Hash digest
SHA256 6184942a1145dfd5953a2ebc8c2b59088f51d3d04aeab781099b95e12e79a1c7
MD5 e7fcd759e46cbe20305f532256aed375
BLAKE2b-256 133072927582be0f6211badcc92b9b3cba4bb596a62d75783ac6d95c2e73e223

See more details on using hashes here.

File details

Details for the file quorai-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: quorai-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 314.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for quorai-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3f7af8aad119eb11189adcea1d9ed1c2df2ae1252eb58543755752b1d7c5db4f
MD5 02532c6ddf10c26216f8e622b5cd9369
BLAKE2b-256 7237350aa5600a9855008a6cdfbbc3600d93bd17f9b22db8954e42e28dca5d09

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page