Quorai — a quorum of AI agents deliberating trading decisions.
Project description
Quorai
A quorum of AI agents deliberating trading decisions. Pronounced "KWOR-eye" (quorum + AI).
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
uv sync
cp .env.example .env # add OPENROUTER_API_KEY and FINNHUB_API_KEY
uv run backtester --tickers AAPL,MSFT --model deepseek/deepseek-chat --model-provider OpenRouter
The backtester console script is installed by uv sync. For all options see Usage — Backtesting.
Model choice matters. All agents produce structured JSON output and the portfolio manager calls tools via LangChain. Use a model with strong instruction-following and structured-output capabilities — Claude (Anthropic), GPT-4o (OpenAI), or Gemini 2.5 Flash/Pro (Google) are reliable choices. Weaker models may produce malformed JSON that causes agent failures.
Contents
- Features
- How it works
- Analyst roster
- Architecture
- Math & quantitative methods
- Setup
- Usage
- Safety mechanisms
- Project structure
- Running tests
- Adding an analyst
- Troubleshooting
- Python version
- Changelog
- Disclaimer
- Acknowledgements
- Companion projects
- License
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
- Multi-provider LLM support — OpenAI, Anthropic, Groq, Gemini, DeepSeek, xAI, OpenRouter, Ollama (local)
- 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 harness — runs two backtest configs back-to-back and prints a side-by-side metrics table (full-vs-regime analysts, uniform-vs-conviction weights)
- Per-agent model routing — override model and provider per analyst via
--agent-model AGENT=model/PROVIDER; handled byRunRequest(src/llm/request.py) - Parallel per-ticker execution — set
QUORAI_PARALLEL_TICKERS=Nto 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 withexperiments/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_trendremovesshortwhen quant/growth groups are bullish;bear_trendremovesbuywhen quant/quality groups are bearish;risk_offblocks bothbuyandshort
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:
- Financial data is fetched (price, fundamentals, news, macro indicators)
- Each analyst agent runs as a LangGraph node and produces a signal with reasoning
- A portfolio manager agent weighs the signals and issues buy/hold/sell orders
- 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:
-
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 fromweights.json), then averaged.Weighted stance Group signal ≥ +0.25 bullish ≤ −0.25 bearish otherwise neutral -
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
DebateSummarywith each group's one-sentence position, the root structural disagreement, and aconsensus_strengthlabel (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.
Math & quantitative methods
This section documents the quantitative formulas used throughout the codebase. All annualisation uses 252 trading days.
Portfolio metrics (src/backtesting/metrics.py)
| Metric | Formula |
|---|---|
| Daily return | (price_t − price_{t−1}) / price_{t−1} |
| Excess return | daily_return − risk_free_rate / 252 (RF = 4.34% annual) |
| Sharpe ratio | √252 × mean(excess) / std(excess) |
| Sortino ratio | √252 × mean(excess) / √mean(min(excess, 0)²) |
| Max drawdown | (value_t − max(value_{0..t})) / max(value_{0..t}) — tracked as a running peak |
| Total return | (final_value / initial_capital − 1) × 100% |
| Benchmark return | (SPY_last / SPY_first − 1) × 100% (buy-and-hold over the same window) |
| Alpha vs SPY | strategy_total_return − SPY_total_return |
| Alpha vs basket | strategy_total_return − equal_weight_basket_total_return |
| Information ratio vs SPY | √252 × mean(daily_active_return) / std(daily_active_return) where active_return = strategy_return − SPY_return |
| Information ratio vs basket | Same formula with active_return = strategy_return − equal_weight_basket_return |
Portfolio exposure (src/backtesting/valuation.py)
| Metric | Formula |
|---|---|
| NAV | cash + Σ(long_shares × price) − Σ(short_shares × price) |
| Long exposure | Σ(long_shares × price) |
| Short exposure | Σ(short_shares × price) |
| Gross exposure | long + short |
| Net exposure | long − short |
| L/S ratio | long / short |
| Weighted-average cost basis | (old_basis × old_qty + new_price × new_qty) / total_qty (updated on every fill) |
Position sizing (src/agents/risk_manager.py)
The risk manager derives a per-ticker notional limit in two steps:
-
Volatility adjustment — annualised vol
= std(60-day returns) × √252:Annualised vol Multiplier < 15% 1.25× 15–30% 1.0 − (vol − 0.15) × 0.530–50% 0.75 − (vol − 0.30) × 0.5> 50% 0.50× -
Correlation adjustment — average correlation with existing open positions:
Avg correlation Multiplier ≥ 0.80 0.70× 0.60–0.80 0.85× 0.40–0.60 1.00× 0.20–0.40 1.05× < 0.20 1.10× Final limit:
base_limit × vol_multiplier × corr_multiplierCash/margin constraints are applied last:
max_long = min(position_limit, available_cash),max_short = min(position_limit, available_margin)whereavailable_margin = equity / margin_requirement − margin_used.
Valuation models (src/agents/valuation.py)
Owner earnings (Buffett)
owner_earnings = net_income + D&A − capex − Δworking_capital
Projected forward for 10 years then discounted; terminal value uses a Gordon Growth model. A 25 % margin of safety is applied to the resulting intrinsic value.
DCF (free cash flow)
intrinsic = Σ_{t=1}^{n} FCF_t / (1+r)^t + TV / (1+r)^n
Terminal value: TV = FCF_n × (1 + g_terminal) / (r − g_terminal)
Multi-stage DCF — three growth phases discounted at WACC, with a quality adjustment:
quality_factor = max(0.7, 1 − fcf_volatility × 0.5) where fcf_volatility = std(FCF) / mean(FCF) (coefficient of variation).
A scenario overlay applies bear/base/bull growth assumptions weighted 20 / 60 / 20 %.
EV/EBITDA cross-check
implied_equity = median_sector_EV/EBITDA × current_EBITDA − net_debt
Residual income (Edwards-Bell-Ohlson)
RI_t = net_income_t − cost_of_equity × book_value_{t−1}
intrinsic = book_value + Σ PV(RI_t) + PV(terminal_RI)
WACC
cost_of_equity = RF + β × MRP (RF = 4.5 %, MRP = 6 %, β from TTM metrics)
cost_of_debt = max(RF + 0.01, RF + 10 / interest_coverage)
WACC = (E/V) × CoE + (D/V) × CoD × (1 − 0.25), floored at 6 % and capped at 20 %.
Blended signal
The four methods are weighted DCF 35 %, Owner Earnings 35 %, EV/EBITDA 20 %, Residual Income 10 %. The resulting valuation gap = (weighted_intrinsic − market_cap) / market_cap drives the bullish/neutral/bearish signal (thresholds ±15 %).
Technical indicators (src/agents/technicals.py, src/agents/jim_simons.py)
| Indicator | Formula / Definition |
|---|---|
| EMA | close.ewm(span=N, adjust=False).mean() |
| RSI | 100 − 100 / (1 + avg_gain / avg_loss) over 14 periods |
| Bollinger Bands | SMA(20) ± 2 × σ(20) |
| Z-score | (price − MA) / σ — signals at ±2 |
| ADX | EWM(DX) where `DX = 100 × |
| ATR | SMA(true_range, 14) where `TR = max(H−L, |
| Momentum (1/3/6 m) | returns.rolling(21/63/126).sum(), blended as 0.4×mom_1m + 0.3×mom_3m + 0.3×mom_6m |
| 12-1 momentum (AQR) | (price_{−21d} − price_{−252d}) / price_{−252d} — skips the most recent month to avoid short-term reversal |
| Hurst exponent | OLS slope of log(lag) vs log(std(returns at lag)); H < 0.5 → mean-reverting, H > 0.5 → trending |
| Lag-1 autocorrelation | corr(returns[:-1], returns[1:]) — negative ACF supports mean-reversion entry |
| Volume spike | current_volume / SMA(volume, 21) — > 2× on a down day flags potential capitulation |
The final technical signal is a weighted sum: Trend 25 %, Mean Reversion 20 %, Momentum 25 %, Volatility 15 %, Stat-Arb 15 %; mapped to bullish/bearish via a ±0.2 threshold.
AQR multi-factor scoring (src/agents/cliff_asness.py)
| Factor | Key sub-signals | Max pts |
|---|---|---|
| Value | P/E, P/B, FCF yield, EV/EBITDA vs thresholds | 8 |
| Momentum | 12-1 momentum vs ±5 % / ±20 % thresholds; −1 if 1-month gain > 15 % | 4 |
| Quality | ROIC, gross margin, earnings stability (% positive years) | 6 |
| Low volatility | 63-day annualised vol bucketed into five tiers | 4 |
Overall signal strength scales with how many factors align simultaneously (max 22 pts → 90–100 % confidence).
Conviction-weight feedback loop (src/feedback/)
After each run, signals are labeled with 1 d / 5 d / 20 d forward returns. The rolling directional hit-rate for each agent is used to upweight high-accuracy agents in the debate aggregation. Weights are persisted in src/feedback/weights.json and reloaded at the start of the next run when --use-conviction-weights is set.
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.
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 (requiressrc/feedback/weights.jsonfrom 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 inrun_idand manifest for later filtering
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.
Reading the backtest output
The engine prints two blocks of output.
Rolling PORTFOLIO SUMMARY — printed at the end of each trading day:
PORTFOLIO SUMMARY:
Cash Balance: $136,342.28 ← uninvested cash
Total Position Value: $-36,520.48 ← market value of open positions (negative = net short)
Total Value: $99,821.80 ← cash + position value = current portfolio NAV
Portfolio Return: -0.18% ← return vs starting capital since inception
Benchmark Return: +0.77% ← S&P 500 return over the same period
Sharpe Ratio: -4.58 ← risk-adjusted return (annualised); see below
Sortino Ratio: -6.01 ← like Sharpe but only penalises downside volatility
Max Drawdown: -0.74% ← largest peak-to-trough decline so far
At the end of the run three final blocks are printed:
ENGINE RUN COMPLETE — core metrics:
ENGINE RUN COMPLETE
Total Return: -0.18%
Sharpe: -2.25
Sortino: -3.00
Max DD: 0.74% on 2026-05-08
BASELINES — buy-and-hold returns over the same window:
BASELINES (2026-04-22 → 2026-05-22)
SPY: +1.12%
AAPL: +0.85%
MSFT: +2.31%
Equal-weight (AAPL,MSFT): +1.58%
ACTIVE PERFORMANCE — strategy performance vs benchmarks:
ACTIVE PERFORMANCE
Strategy: -0.18%
Alpha vs SPY: -1.30% ← strategy total return − SPY total return
Alpha vs Equal-weight basket: -0.45% ← strategy − equal-weight ticker basket
IR vs SPY: -0.83 ← (mean active daily return / std) × √252
IR vs Equal-weight basket: -0.61
The Sharpe/Sortino in the final summary may differ slightly from the last rolling figure because the two blocks use marginally different timing for their calculation windows.
Interpreting the metrics
| Metric | Good | Acceptable | Poor |
|---|---|---|---|
| Portfolio Return | Beats benchmark | Roughly flat vs benchmark | Lags benchmark |
| Sharpe Ratio | > 1.0 | 0 – 1.0 | < 0 |
| Sortino Ratio | > 1.5 | 0 – 1.5 | < 0 |
| Max Drawdown | < 10% | 10 – 20% | > 20% |
| Alpha vs SPY | > 0% | ~ 0% | < 0% |
| IR vs SPY | > 0.5 | 0 – 0.5 | < 0 |
| IR vs basket | > 0.5 | 0 – 0.5 | < 0 |
Important caveats for short backtests
- Sharpe and Sortino are annualised from daily returns. With only a few days of data there are too few samples for the figures to be statistically meaningful — treat them as noise until the test window covers at least several months.
- A negative
Total Position Valuemeans the portfolio manager issued net short orders. This is valid behaviour but unusual; check--show-reasoningto understand why. - Always compare against the benchmark return over the same period before drawing conclusions.
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 asBacktestEngine)--use-conviction-weights— apply per-agent conviction weights fromsrc/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/PROVIDERfor a wildcard fallback. Also readsQUORAI_AGENT_MODELS_JSONenv var (JSON dict).--no-signal-log— disable writinglogs/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
After each run the console prints:
Signal log: logs/signals-live-2026-05-12.jsonl
Tokens: 12 calls, 84 200 in / 3 100 out
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,000of 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.jsonis absent at run-time (e.g. after a crash),src/live/runner.py:99-103resets 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:83roundsqtyto 3 decimals; a tiny allocation on a high-priced stock rounds to0.000and is classified asskippedwith no warning. - No
fractionablepre-check in the Alpaca client. Fractionalqtyon 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-105partially 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.
Project structure
| Path | Purpose |
|---|---|
src/main.py |
run_quorai() — single-run entry point; create_workflow() — LangGraph builder |
src/live_trading.py |
Live/paper trading CLI entry point |
src/agents/ |
25 analyst agents (personality + quant) plus risk manager and portfolio manager |
src/backtesting/ |
Engine, portfolio, metrics, CLI (backtester / python -m src.backtesting [compare]), signal log, A/B harness |
src/orchestration/ |
PipelineContext — pre-graph helper shared by live and backtest |
src/regime/ |
MarketRegime classifier + analyst-selection policy |
src/feedback/ |
Forward-return labeler, rolling per-agent scorer, weights loader |
src/broker/ |
Broker protocol + Alpaca client |
src/live/ |
Live executor, runner, risk gate, audit journal |
src/notifications/ |
Telegram approval client + command store |
src/data/ |
Disk-persisted cache (cache.py), Pydantic data models, SEC EDGAR XBRL store (sec_store.py) |
src/llm/ |
Multi-provider LLM dispatch, OpenRouter catalog |
src/utils/ |
Analyst registry (ANALYST_CONFIG), shared helpers |
src/config.py |
Centralised env-var config via pydantic-settings |
experiments/run_scenarios.py |
Regime evaluation harness — sweeps 10 period × ticker-set scenarios and writes a markdown report |
experiments/seed_sec_fundamentals.py |
Seeds .cache/sec_fundamentals.db from SEC EDGAR XBRL; requires QUORAI_SEC_USER_AGENT |
tests/ |
Unit and integration tests |
Running tests
uv run python -m pytest
Use
python -m pytest, notuv run pytest— the latter invokes a stale venv shebang that resolves to the wrong Python.
Adding an analyst
- Create
src/agents/my_analyst.pywith amy_analyst_agent(state, agent_id)function. - Register it in
src/utils/analysts.py— add an entry toANALYST_CONFIG.
See CONTRIBUTING.md for full contribution guidelines.
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-appwrites tologs/<mode>/runs/and presents them as browsable, comparable views; no write access to the trading system. PointQUORAI_LOGS_DIRat this repo'slogs/directory and runnpm run devon 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quorai-1.1.0.tar.gz.
File metadata
- Download URL: quorai-1.1.0.tar.gz
- Upload date:
- Size: 767.0 kB
- 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22ffe522afb5984bab8180f046f7c1b82b84fc3b9c1ebb15f52ac164f93c1443
|
|
| MD5 |
98e8155f425905812da8ae936f07e5cc
|
|
| BLAKE2b-256 |
6aa7e895a07c95b596fd0ad8a231d891fff175c277b086616de0c499dc355417
|
File details
Details for the file quorai-1.1.0-py3-none-any.whl.
File metadata
- Download URL: quorai-1.1.0-py3-none-any.whl
- Upload date:
- Size: 281.9 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aee72c546d4623bc4f0aee82aaf87a96d0e0098f6dd11bb94852d1d273e33aec
|
|
| MD5 |
8d5b15ef6282b4029d5e65edbdf5b4d5
|
|
| BLAKE2b-256 |
31f28f2f1b585f673a852bb99ae2362e07658d932eaded477943055411bd9949
|