This guide is the deep technical reference for AI Hedge Fund: its architecture, internals, configuration surface, security posture, operations, and the contributor-facing design of the codebase. It is the companion to the User Guide, which covers day-to-day usage.
Caution
AI Hedge Fund is a proof of concept for educational and research purposes only. It does not execute real trades, provides no investment advice, and carries no warranty. Treat every "portfolio" and "decision" as a simulation.
Table of Contents
- Executive Summary
- System Architecture
- Repository Structure
- The Agent Pipeline (Deep Dive)
- LLM Provider Layer
- Data Layer
- The Backtesting Engine
- Web Application Backend
- Web Application Frontend
- The
v2/Quantitative Stack - Configuration Reference
- Deployment
- Security Analysis
- Operations
- Code Quality & Patterns
- Testing
- Potential Improvements & Tech Debt
- Contributing
- Quick Reference
1. Executive Summary
AI Hedge Fund is a multi-agent stock-analysis system built on LangGraph (a graph-orchestration layer over LangChain). It models an investment committee as a directed graph of agents:
- Analyst agents (investor personas + analytical specialists) each fetch financial data, score it deterministically, and use an LLM to emit a trading signal.
- A Risk Manager computes volatility/correlation and sets per-ticker position limits.
- A Portfolio Manager fuses all signals with the risk limits and produces the final per-ticker action and quantity.
The project ships three runnable surfaces:
| Surface | Entry point | Audience |
|---|---|---|
| Main CLI | src/main.py |
Power users, automation |
| Backtester | src/backtester.py + src/backtesting/ |
Strategy evaluation |
| Web app | app/backend (FastAPI) + app/frontend (React) |
Visual users |
A fourth, in-progress surface — v2/ — is a ground-up quantitative rewrite that is not yet integrated.
| Attribute | Value |
|---|---|
| Language (core) | Python ^3.11 |
| Language (frontend) | TypeScript / React 18 |
| Orchestration | LangGraph 0.2.56, LangChain ^0.3.7 |
| Dependency manager | Poetry |
| Data provider | Financial Datasets API |
| LLM providers | 11 cloud providers + Ollama (local) |
| Web backend | FastAPI, SQLAlchemy, Alembic, SQLite |
| Web frontend | Vite, React Flow (@xyflow/react), shadcn/Radix UI, Tailwind |
| License | MIT |
| Package version | 2026.5.14 (pyproject.toml) |
2. System Architecture
High-level topology
┌──────────────────────────────────────────────────────────────────┐
│ USER SURFACES │
│ │
│ CLI (src/main.py) Backtester Web Frontend │
│ │ (src/backtester.py) (React, :5173) │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ Web Backend (FastAPI │
│ │ │ :8000) │
│ │ │ │ │
│ └───────────┬───────────┴────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ run_hedge_fund() │ builds a LangGraph │
│ │ (src/main.py) │ StateGraph and invokes it │
│ └───────────┬───────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LangGraph StateGraph │ │
│ │ start → [analyst nodes…] → risk_mgr → portfolio_mgr │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ src/tools/api.py src/llm/models.py │
│ (Financial Datasets) (LLM provider clients) │
└──────────────────────────────────────────────────────────────────┘
The shared graph state
All agents read and write a single typed state object, defined in src/graph/state.py:
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
data: Annotated[dict[str, any], merge_dicts]
metadata: Annotated[dict[str, any], merge_dicts]
The Annotated reducers are what make parallel fan-out safe:
messagesaccumulates viaoperator.add(list concatenation).dataandmetadatamerge viamerge_dicts({**a, **b}).
Because every analyst node returns a partial state and LangGraph reduces them with these functions, multiple analysts can run concurrently and their analyst_signals are merged without clobbering each other.
| State key | Holds |
|---|---|
data.tickers |
Tickers under analysis |
data.portfolio |
Current simulated portfolio (cash, positions, margin) |
data.start_date / data.end_date |
Analysis window |
data.analyst_signals |
Each agent's signal, keyed by agent name → ticker |
metadata.show_reasoning |
Whether to print agent reasoning |
metadata.model_name / metadata.model_provider |
Active LLM selection |
metadata.request |
(Web only) request object carrying per-agent model config + API keys |
Graph construction
create_workflow(selected_analysts) in src/main.py builds the graph:
- Adds a
start_node(identity passthrough). - For each selected analyst key, looks up its
(node_name, agent_func)viaget_analyst_nodes()and wiresstart_node → analyst_node. - Always adds
risk_management_agentandportfolio_manager. - Wires every analyst →
risk_management_agent → portfolio_manager → END. - Sets
start_nodeas the entry point and compiles.
Note
If no analysts are explicitly selected, the workflow defaults to all analysts (
get_analyst_nodes().keys()).
3. Repository Structure
ai-hedge-fund/
├── pyproject.toml # Poetry project; deps, scripts, version
├── poetry.lock
├── .env.example # Template for all API keys
├── README.md
├── src/ # Core engine (CLI + agents)
│ ├── main.py # run_hedge_fund(), create_workflow(), CLI entry
│ ├── backtester.py # Legacy backtester entry (wraps BacktestEngine)
│ ├── agents/ # 19 analyst agents + risk + portfolio managers
│ ├── backtesting/ # Modular backtesting engine (engine, portfolio, …)
│ ├── cli/input.py # argparse + questionary input parsing
│ ├── data/ # In-memory cache + Pydantic data models
│ ├── graph/state.py # AgentState TypedDict + reasoning printer
│ ├── llm/ # Provider clients + model registries (JSON)
│ ├── tools/api.py # Financial Datasets API client
│ └── utils/ # analysts config, llm helper, display, ollama, …
├── app/
│ ├── backend/ # FastAPI app (routes, services, db, models)
│ └── frontend/ # React + Vite UI (React Flow canvas)
├── docker/ # Dockerfile, docker-compose.yml, run scripts
├── tests/ # pytest suites (cache, rate limiting, backtesting)
└── v2/ # WIP quantitative rewrite (not integrated)
Triage at a glance
The codebase is roughly 1.08M characters of source across ~296 files. The highest-value areas for maintainers:
| Area | Why it matters |
|---|---|
src/agents/ |
The actual investment logic. Largest and most important module. |
src/main.py + src/graph/ |
Graph orchestration — how agents compose. |
src/llm/models.py |
The single switchboard for all 11 cloud providers + Ollama. |
src/tools/api.py |
The only data ingress; all financial facts flow through here. |
src/backtesting/ |
Cleanly decomposed engine — the model for the rest of the codebase. |
app/backend/routes/ |
The REST surface the frontend depends on. |
4. The Agent Pipeline (Deep Dive)
The analyst registry
src/utils/analysts.py defines ANALYST_CONFIG — the single source of truth for which analysts exist, their display names, descriptions, investing styles, agent functions, and ordering. There are 19 analysts: 13 investor personas and 6 analytical specialists.
| Order | Key | Type |
|---|---|---|
| 0 | aswath_damodaran |
persona |
| 1 | ben_graham |
persona |
| 2 | bill_ackman |
persona |
| 3 | cathie_wood |
persona |
| 4 | charlie_munger |
persona |
| 5 | michael_burry |
persona |
| 6 | mohnish_pabrai |
persona |
| 7 | nassim_taleb |
persona |
| 8 | peter_lynch |
persona |
| 9 | phil_fisher |
persona |
| 10 | rakesh_jhunjhunwala |
persona |
| 11 | stanley_druckenmiller |
persona |
| 12 | warren_buffett |
persona |
| 13 | technical_analyst |
specialist |
| 14 | fundamentals_analyst |
specialist |
| 15 | growth_analyst |
specialist |
| 16 | news_sentiment_analyst |
specialist |
| 17 | sentiment_analyst |
specialist |
| 18 | valuation_analyst |
specialist |
Two derived structures are exported for the rest of the system:
ANALYST_ORDER—(display_name, key)tuples, sorted byorder, used to build interactive menus.get_analyst_nodes()— maps each key to("{key}_agent", agent_func)for graph wiring.get_agents_list()— the JSON-friendly list returned by the web API'sget_agentsendpoint.
Tip
To add a new analyst, implement an agent function following the standard contract (below), import it into
analysts.py, and add one entry toANALYST_CONFIG. Every menu, the graph builder, and the web API pick it up automatically — no other wiring needed.
The analyst agent contract
Every analyst agent is a function agent(state: AgentState, agent_id: str = "..._agent") that:
- Reads
tickers,end_date, and the API key fromstate. - For each ticker, fetches the data it needs via
src/tools/api.py(financial metrics, line items, market cap, prices, insider trades, news). - Runs deterministic scoring helpers. For example,
warren_buffett_agentcallsanalyze_fundamentals,analyze_consistency,analyze_moat,analyze_pricing_power,analyze_book_value_growth,analyze_management_quality, andcalculate_intrinsic_value, summing their sub-scores. - Passes the aggregated analysis to
call_llm()with a per-agent Pydantic schema (e.g.WarrenBuffettSignal { signal, confidence, reasoning }). - Writes the resulting signal into
state["data"]["analyst_signals"][agent_id]and emits progress updates via the sharedprogresstracker.
Important
The deterministic scoring is what differentiates each persona; the LLM's job is to weigh those scores and articulate reasoning, not to invent facts. This split keeps the personas faithful and the output explainable.
The Risk Manager
risk_management_agent (src/agents/risk_manager.py):
- Fetches prices for all tickers and all currently-held positions.
- Computes per-ticker volatility metrics (daily/annualized volatility, a volatility percentile, data-point count) with a 5% daily fallback when data is missing.
- Builds a returns DataFrame and computes a correlation matrix when ≥2 tickers have data.
- Derives a
max_position_size(position limit) per ticker and writes it as a risk signal intoanalyst_signals["risk_management_agent"].
The Portfolio Manager
portfolio_management_agent (src/agents/portfolio_manager.py):
- Reads every analyst's signal and the risk limits. Converts each risk limit into
max_shares[ticker] = position_limit // current_price. - Compresses analyst signals into a compact
{sig, conf}form per ticker (excluding the risk manager's own entry). - Calls
compute_allowed_actions()— a deterministic guardrail that, given current long/short positions andmax_shares, computes the maximum permissible quantity for each action (buy,sell,short,cover,hold) and prunes zero-capacity actions to save tokens. - Calls
generate_trading_decision()(an LLM call) constrained to those allowed actions, producingPortfolioDecision { action ∈ {buy,sell,short,cover,hold}, quantity, confidence, reasoning }per ticker.
Note
The guardrail matters: it prevents the LLM from proposing impossible trades (selling unowned shares, exceeding risk limits) by removing those options before the model ever sees them.
Output rendering
run_hedge_fund() returns { "decisions": {...}, "analyst_signals": {...} }. The CLI renders this with print_trading_output() (src/utils/display.py), producing per-ticker agent-signal tables (color-coded BULLISH/BEARISH/NEUTRAL), a trading-decision table, and a portfolio summary with bullish/bearish/neutral signal counts.
5. LLM Provider Layer
The model registry
Models are declared as JSON, not code, so they can change without touching logic:
src/llm/api_models.json→ loaded intoAVAILABLE_MODELSandLLM_ORDER.src/llm/ollama_models.json→ loaded intoOLLAMA_MODELSandOLLAMA_LLM_ORDER.
Each entry is { display_name, model_name, provider } and is parsed into an LLMModel Pydantic object. ModelProvider is an Enum of 14 provider identifiers: Alibaba, Anthropic, DeepSeek, Google, Groq, Kimi, Meta, Mistral, OpenAI, Ollama, OpenRouter, GigaChat, Azure OpenAI, xAI.
Note
The
ModelProviderenum has 14 members, butget_model()instantiates clients for 11 distinct cloud providers plus Ollama.Alibaba,Meta, andMistralappear as providers for Ollama-hosted open models (e.g. Qwen, Llama, Mistral) rather than as separate cloud SDKs.
The provider switchboard
get_model(model_name, model_provider, api_keys) in src/llm/models.py returns the appropriate LangChain chat client:
| Provider | LangChain client | Key source |
|---|---|---|
| OpenAI | ChatOpenAI (+ OPENAI_API_BASE override) |
OPENAI_API_KEY |
| Anthropic | ChatAnthropic |
ANTHROPIC_API_KEY |
| DeepSeek | ChatDeepSeek |
DEEPSEEK_API_KEY |
| Groq | ChatGroq |
GROQ_API_KEY |
ChatGoogleGenerativeAI |
GOOGLE_API_KEY |
|
| xAI | ChatXAI |
XAI_API_KEY |
| Kimi/Moonshot | ChatOpenAI (OpenAI-compatible base URL) |
MOONSHOT_API_KEY / KIMI_API_KEY |
| OpenRouter | ChatOpenAI (OpenRouter base URL + attribution headers) |
OPENROUTER_API_KEY |
| GigaChat | GigaChat (credentials or user/password) |
GIGACHAT_API_KEY / GIGACHAT_CREDENTIALS / user+password |
| Azure OpenAI | AzureChatOpenAI (API version 2024-10-21) |
AZURE_OPENAI_API_KEY + endpoint + deployment |
| Ollama | ChatOllama (base URL, no key) |
OLLAMA_BASE_URL / OLLAMA_HOST |
API keys are resolved with a two-tier lookup: first from an api_keys dict passed in (the web app's per-request keys), then from environment variables. This is what lets the web app accept keys via the UI while the CLI uses .env.
The call_llm helper
src/utils/llm.py::call_llm() centralizes every model invocation:
- Resolves the model + provider from state (per-agent config in the web app, or global defaults
gpt-4.1/OPENAIin the CLI). - Uses LangChain structured output (
with_structured_output(..., method="json_mode")) for JSON-capable models; for models that lack JSON mode (DeepSeek, Gemini, most Ollama models), it parses JSON manually viaextract_json_from_response(). - Retries up to
max_retries=3times, updating agent progress on each failure. - On total failure, returns a safe default via
default_factoryorcreate_default_response()so a single bad call never crashes the whole committee.
extract_json_from_response() is defensively layered: it tries ```json blocks, then plain ``` blocks, then whole-string parse, then brace-matching to find the first balanced top-level object.
Tip
LLMModel.has_json_mode()is the gate that decides which path a model takes. If you add a provider whose models reliably return JSON, ensure this returnsTruefor them to use the more robust structured-output path.
6. Data Layer
The API client
src/tools/api.py is the sole financial-data ingress. It talks to https://api.financialdatasets.ai with an X-API-KEY header sourced from FINANCIAL_DATASETS_API_KEY (or a per-call key). It exposes typed fetchers used by the agents and backtester:
| Function | Endpoint | Returns |
|---|---|---|
get_prices |
/prices/ |
OHLCV price bars |
get_financial_metrics |
/financial-metrics/ |
Ratios, margins, growth metrics |
search_line_items |
(line-items search) | Specific statement line items |
get_insider_trades |
insider trades | Insider transaction records |
get_company_news |
company news | News with optional sentiment |
get_market_cap |
company facts/metrics | Market capitalization |
prices_to_df / get_price_data |
— | Prices as a pandas DataFrame |
Rate limiting & resilience
_make_api_request() wraps every call with retry logic: on HTTP 429, it applies linear backoff (≈60s, 90s, 120s, …) up to max_retries=3 before returning the final response. Parse failures are logged and degrade to empty lists rather than raising.
Caching
src/data/cache.py provides a process-global, in-memory Cache with separate stores for prices, financial metrics, line items, insider trades, and company news. New data is merged with existing data by a per-store key field (e.g. time for prices, report_period for metrics, filing_date for insider trades) to avoid duplicates.
Warning
The cache is not persistent. It lives only for the lifetime of the process. Each fresh CLI run or backtest starts cold and re-fetches from the API. There is no on-disk cache layer.
Data models
src/data/models.py defines Pydantic models for every entity: Price, FinancialMetrics (40+ optional ratio fields), LineItem (with extra="allow" for dynamic fields), InsiderTrade, CompanyNews, CompanyFacts, plus portfolio/analysis models (Position, Portfolio, AnalystSignal, TickerAnalysis, AgentStateData, AgentStateMetadata).
7. The Backtesting Engine
There are two backtesting paths that share semantics:
- Legacy entry —
src/backtester.py, which constructs aBacktestEngineand runs it with gracefulCtrl+Chandling. - Modular engine —
src/backtesting/, a cleanly decomposed package (also exposed as thebacktesterPoetry script viasrc/backtesting/cli.py).
Modular engine components
| Module | Class / role |
|---|---|
engine.py |
BacktestEngine — orchestrates the day-by-day loop |
controller.py |
AgentController — invokes the agent and normalizes/coerces its output |
trader.py |
TradeExecutor — applies an action to the portfolio |
portfolio.py |
Portfolio — cash, long/short positions, cost basis, margin, realized gains |
valuation.py |
calculate_portfolio_value, compute_exposures, compute_portfolio_summary |
metrics.py |
PerformanceMetricsCalculator — Sharpe, Sortino, max drawdown |
benchmarks.py |
BenchmarkCalculator — buy-and-hold return for a benchmark ticker |
output.py |
OutputBuilder — builds and prints the results table |
types.py |
Action enum + TypedDicts for all backtest data contracts |
The loop
BacktestEngine.run_backtest():
_prefetch_data()warms the cache: one year of prices, 10 periods of financial metrics, and up to 1000 insider trades/news items per ticker, plusSPYprices for benchmarking.- Iterates
pd.date_range(start, end, freq="B")(business days). - For each day, fetches the latest close for every ticker (skipping days with missing data), invokes the agent committee via
AgentController.run_agent(), and executes the returned decisions withTradeExecutor. - Recomputes portfolio value and exposures, appends a
PortfolioValuePoint, builds the day's table rows, and prints the full history (latest day first). - After >3 data points, recomputes performance metrics.
The portfolio model
Portfolio (in src/backtesting/portfolio.py) is the canonical accounting model. It supports:
- Long positions with weighted-average cost basis (
apply_long_buy,apply_long_sell). - Short positions with margin tracking (
apply_short_open,apply_short_cover), gating shorts by available cash vs.margin_requirement. - Realized gains per side per ticker.
- Partial fills: if a buy/short exceeds available funds, it executes the maximum affordable quantity.
Performance metrics
PerformanceMetricsCalculator (defaults: annual_trading_days=252, annual_rf_rate=0.0434):
- Sharpe =
√252 × mean(excess) / std(excess). - Sortino =
√252 × mean(excess) / downside_deviation(downside =√mean(min(excess,0)²)). - Max drawdown = the minimum of
(value − cummax) / cummax, as a signed percentage, with the date it occurred.
Note
The modular engine was built to mirror the legacy
src/backtester.pysemantics exactly while allowing incremental refactoring (see the docstrings insrc/backtesting/__init__.py). The interfaces (Protocols/TypedDicts intypes.py) are explicitly designed for drop-in compatibility.
8. Web Application Backend
Application setup
app/backend/main.py builds the FastAPI app (title="AI Hedge Fund API", version="0.1.0"):
- Creates all SQLAlchemy tables on startup (
Base.metadata.create_all). - Configures CORS for
http://localhost:5173andhttp://127.0.0.1:5173with all methods/headers and credentials allowed. - Mounts the aggregated
api_router. - On startup, probes Ollama availability and logs installed/running status and available models.
Note
app/backend/__init__.pyinjectssrc/ontosys.pathso the backend can import the core engine. This is flagged in-code as "a temporary solution while we develop the backend."
Persistence
The backend uses SQLAlchemy with Alembic migrations over a SQLite database (app/backend/database/, app/backend/alembic/). Migrations in app/backend/alembic/versions/ add tables for API keys, hedge-fund flows, flow runs, and flow-run cycles. Repositories (app/backend/repositories/) encapsulate DB access for API keys, flows, and flow runs.
REST API surface
Routes live in app/backend/routes/ and are mounted under resource prefixes. Selected endpoints:
| Group | Method & path (relative) | Purpose |
|---|---|---|
| Hedge fund | POST …/run |
Run the agent committee for a request |
POST …/backtest |
Run a backtest | |
GET …/agents |
List available analysts (get_agents) |
|
| Language models | GET …/ |
List models (get_language_models) |
GET …/providers |
List providers | |
| API keys | POST/GET/PUT/DELETE …/{provider} |
CRUD for stored provider keys |
POST …/bulk |
Bulk upsert keys | |
PATCH …/{provider}/deactivate |
Deactivate a key | |
| Flows | POST/GET/PUT/DELETE …/{flow_id} |
CRUD for saved visual flows |
POST …/{flow_id}/duplicate |
Duplicate a flow | |
GET …/search/{name} |
Search flows by name | |
| Flow runs | POST/GET/PUT/DELETE …/{run_id} |
CRUD for flow-run records |
GET …/active, …/latest, …/count |
Run status queries | |
| Ollama | GET …/status, POST …/start, POST …/stop |
Manage the Ollama server |
POST …/models/download[/progress] |
Pull models with progress | |
GET …/models/recommended |
Recommended local models | |
DELETE …/models/{model_name} |
Remove a model | |
| Storage | POST …/ |
save_json_file |
| Health | GET /, GET /ping |
Liveness checks |
Services
app/backend/services/ contains the business logic invoked by routes:
| Service | Responsibility |
|---|---|
agent_service.py |
Bridges HTTP requests to run_hedge_fund() |
backtest_service.py |
Runs backtests for the API |
graph.py |
Builds the agent graph for web requests |
portfolio.py |
Portfolio handling for web runs |
api_key_service.py |
Stores/retrieves provider keys |
ollama_service.py |
Async Ollama status & model management |
Important
The web request carries a
requestobject into the graph'smetadata, exposingapi_keysand per-agent model config.src/utils/api_key.py::get_api_key_from_state()andsrc/utils/llm.py::get_agent_model_config()read it. This is how the web app supports different models per agent and UI-supplied keys — capabilities the CLI does not have.
9. Web Application Frontend
The frontend (app/frontend/) is a Vite + React 18 + TypeScript single-page app. Key technologies (from package.json):
| Dependency | Role |
|---|---|
@xyflow/react (React Flow) |
The node-based visual flow canvas |
Radix UI (@radix-ui/*) + shadcn-ui |
Accessible UI primitives |
tailwindcss + tailwind-merge |
Styling |
lucide-react, @radix-ui/react-icons |
Icons |
react-resizable-panels |
The resizable panel layout |
react-syntax-highlighter |
Reasoning/JSON output rendering |
cmdk |
Command palette / search box |
sonner |
Toast notifications |
next-themes |
Light/dark theming |
Structure highlights
src/nodes/— custom flow nodes:agent-node,portfolio-manager-node,portfolio-start-node,stock-analyzer-node,investment-report-node,json-output-node, plus their dialogs.src/components/panels/— the editor chrome: left sidebar (flow management), right sidebar (component palette), and bottom panel (output, debug console, terminal, problems tabs).src/components/settings/— settings UI includingapi-keys,appearance, andmodels(withcloudandollamasub-panels).src/services/— typed clients to the backend:api.ts,api-keys-api.ts,backtest-api.ts,flow-service.ts, plustab-service.tsandsidebar-storage.tsfor local persistence.src/contexts/— React contexts for flow, node, layout, and tab state.src/hooks/— flow-management, history (undo/redo), keyboard shortcuts, and connection hooks.
Build & dev scripts
| Script | Command |
|---|---|
dev |
vite |
build |
tsc && vite build |
lint |
eslint . --ext ts,tsx … |
preview |
vite preview |
10. The v2/ Quantitative Stack
v2/ is a work-in-progress, not-yet-integrated rebuild that replaces investor personas with a principled quantitative pipeline:
Data (FD API) → Signals → Features → Portfolio Construction → Risk Management → Execution
| Module | Purpose |
|---|---|
v2/data/ |
Financial Datasets client + caching, with point-in-time guarantees |
v2/event_study/ |
Event-study framework — CARs, market model, significance testing |
v2/signals/ |
Quantitative signals (BaseSignal ABC, output in [-1, +1]) |
v2/features/ |
Feature engineering (earnings surprise, KPI momentum, lead-lag) |
v2/validation/ |
Combinatorial Purged Cross-Validation (CPCV), Probability of Backtest Overfitting (PBO) |
v2/backtesting/ |
Vectorized backtester with transaction-cost modeling |
v2/portfolio/ |
Optimization — mean-variance, Black-Litterman, risk parity, covariance cleaning |
v2/risk/ |
Drawdown controls, position sizing, correlation monitoring, stress testing |
v2/pipeline/ |
Execution simulation — market impact (Almgren-Chriss), fill probability, capacity |
Core data contracts live in v2/models.py: SignalResult, QuantSignals, PortfolioTarget, TradeOrder, ExecutionResult.
Runnable sub-tools expose __main__.py entry points:
python -m v2.backtesting
python -m v2.event_study
Note
v2 emphasizes methodology over personality, transaction costs from day one, built-in validation (CPCV/PBO), and point-in-time data. It is daily-frequency, US-equities, Financial-Datasets-only. Treat it as experimental; it is not wired into the CLI or web app.
11. Configuration Reference
All runtime configuration is via environment variables (loaded from .env with python-dotenv). There is no YAML/TOML runtime config file beyond pyproject.toml/package.json build manifests.
Required
| Variable | Used by | Notes |
|---|---|---|
FINANCIAL_DATASETS_API_KEY |
src/tools/api.py |
Sent as X-API-KEY. Required for data. |
| (one LLM key) | src/llm/models.py |
At least one provider key, or use Ollama. |
LLM provider keys
| Variable | Provider | Resolved at |
|---|---|---|
OPENAI_API_KEY (+ OPENAI_API_BASE) |
OpenAI | models.py:152 |
ANTHROPIC_API_KEY |
Anthropic | models.py:160 |
DEEPSEEK_API_KEY |
DeepSeek | models.py:166 |
GROQ_API_KEY |
Groq | models.py:144 |
GOOGLE_API_KEY |
models.py:172 |
|
XAI_API_KEY |
xAI | models.py:218 |
MOONSHOT_API_KEY / KIMI_API_KEY (+ MOONSHOT_BASE_URL / KIMI_BASE_URL) |
Kimi | models.py:208 |
OPENROUTER_API_KEY (+ YOUR_SITE_URL, YOUR_SITE_NAME) |
OpenRouter | models.py:187 |
GIGACHAT_API_KEY / GIGACHAT_CREDENTIALS / GIGACHAT_USER + GIGACHAT_PASSWORD |
GigaChat | models.py:224 |
AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT + AZURE_OPENAI_DEPLOYMENT_NAME |
Azure OpenAI | models.py:235 |
Ollama
| Variable | Default | Purpose |
|---|---|---|
OLLAMA_HOST |
localhost |
Host used when composing the default base URL (Docker on macOS). |
OLLAMA_BASE_URL |
http://localhost:11434 |
Full base URL; setting it triggers the remote/Docker Ollama workflow. |
Model registries
Editing src/llm/api_models.json or src/llm/ollama_models.json changes the available model menus without code changes. Each entry: { display_name, model_name, provider }.
Defaults
| Setting | Default | Where |
|---|---|---|
| Model / provider | gpt-4.1 / OPENAI |
src/utils/llm.py |
| Initial cash | 100000.0 |
src/cli/input.py |
| Margin requirement | 0.0 |
src/cli/input.py |
| Backtest lookback | 1 month | src/backtesting/cli.py |
| Annual risk-free rate | 0.0434 |
src/backtesting/metrics.py |
| Trading days/year | 252 |
src/backtesting/metrics.py |
12. Deployment
CLI / engine
Standard Python deployment via Poetry:
poetry install --only main
poetry run python src/main.py --ticker AAPL --analysts-all --model gpt-4o
- [ ] Python 3.11 available
- [ ]
.envpresent with data key + one LLM key - [ ]
poetry installcompleted - [ ] Network egress to
api.financialdatasets.aiand your LLM provider
Docker
The docker/ directory provides a Dockerfile, a docker-compose.yml, and run.sh/run.bat wrappers. Compose services:
| Service | Command | Profile |
|---|---|---|
ollama |
ollama/ollama:latest (ports 11434) |
embedded-ollama |
hedge-fund |
python src/main.py --ticker AAPL,MSFT,NVDA |
default |
hedge-fund-reasoning |
… --show-reasoning |
default |
hedge-fund-ollama |
… --ollama |
default |
backtester |
python src/backtester.py --ticker AAPL,MSFT,NVDA |
default |
backtester-ollama |
… --ollama |
default |
Each service mounts ../.env into the container and sets PYTHONUNBUFFERED=1, PYTHONPATH=/app, and OLLAMA_BASE_URL (defaulting to http://ollama:11434).
# Build and run with integrated Ollama
cd docker
./run.sh build
./run.sh compose
Web application
The web app is designed for local use (CORS is locked to localhost:5173). For a manual deploy:
# Backend
cd app/backend && poetry run uvicorn main:app --reload # :8000
# Frontend
cd app/frontend && npm install && npm run build # serve dist/ statically
Warning
Exposing the backend beyond localhost requires widening the hardcoded CORS origins in
app/backend/main.pyand adding authentication — there is none today (see Security Analysis).
13. Security Analysis
Caution
This is a local-first educational project. Several characteristics below are acceptable for local single-user use but would be serious risks if exposed to a network or multiple users.
Authentication & authorization
- None. The FastAPI backend has no authentication or authorization on any endpoint. Anyone who can reach
:8000can run flows, read/write stored API keys, and manage Ollama. - The only network boundary is CORS, hardcoded to
localhost:5173. This protects against cross-site browser requests but not direct API calls.
Secrets handling
- LLM and data API keys are read from environment variables (
.env) for the CLI. - In the web app, provider API keys are stored in the local SQLite database (via the
api_keysroutes/repository) so the UI can manage them. Inspectapp/backend/repositories/api_key_repository.pyfor storage details before treating this DB as sensitive — it should be protected like a secrets store and never committed. .envis intended to be git-ignored; never commit real keys. The repository's.env.examplecarries only placeholders.
Network egress
- Outbound calls go to
api.financialdatasets.aiand the selected LLM provider(s). When using OpenRouter, the app sendsYOUR_SITE_URL/YOUR_SITE_NAMEattribution headers. - The data client retries on
429with backoff but, as a known issue, LLM calls have no timeout — a hung provider connection can stall a run indefinitely.
Input validation
- Dates are validated to
YYYY-MM-DD. Tickers are split on commas and trimmed but not otherwise validated against an allow-list. - The backend includes Pydantic schema validation (e.g.
Trade price must be positive!inapp/backend/models/schemas.py). - The Portfolio Manager's
compute_allowed_actions()is a defensive guardrail that constrains LLM output to feasible trades.
Recommendations for hardening (if ever networked)
- [ ] Add authentication (API keys/JWT) to the FastAPI backend.
- [ ] Encrypt or externalize stored provider keys; never store them in plaintext SQLite for shared use.
- [ ] Make CORS origins configurable rather than hardcoded.
- [ ] Add timeouts to all LLM calls.
- [ ] Validate/escape any user-supplied model names and tickers.
14. Operations
Running and observing
- Progress display:
src/utils/progress.pyrenders a live, per-agent status table usingrich.Live. Agents callprogress.update_status(agent_id, ticker, status)throughout. Handlers can be registered (the web app uses this to stream status). - Reasoning visibility:
--show-reasoning(CLI) or the web output panels surface each agent's full reasoning viashow_agent_reasoning()insrc/graph/state.py. - Graph visualization:
--show-agent-graphcallssave_graph_as_png()to render the compiled LangGraph via Mermaid.
Logging
- The core engine prints to stdout (colorized with
colorama/rich). The data client uses Pythonlogging(logger.warningon parse failures). - The FastAPI backend configures
logging.basicConfig(level=logging.INFO)and logs Ollama probe results on startup.
Common operational tasks
| Task | How |
|---|---|
| Run a one-off analysis | poetry run python src/main.py --ticker … --analysts-all --model … |
| Run a backtest | poetry run python src/backtester.py --ticker … --start-date … --end-date … |
| Start the web stack | cd app && ./run.sh (or run.bat) |
| Reset the web DB | Delete the SQLite file; tables are recreated on next backend startup |
| Manage Ollama models | Via the web Ollama routes, or ollama pull/rm directly |
| Interrupt a long run | Ctrl+C (backtester prints partial results) |
Capacity & cost considerations
- Each agent issues at least one LLM call per ticker. Cost scales with
analysts × tickers × backtest_days. - Backtests over long windows with many analysts can produce a very large number of LLM calls. Start small, and prefer cheaper/faster models or Ollama for exploratory runs.
15. Code Quality & Patterns
Patterns worth emulating
- Config-as-data: analysts (
ANALYST_CONFIG) and models (JSON registries) are declared as data, so the graph, menus, and API derive from a single source. Adding an analyst or model needs no orchestration changes. - Deterministic core + LLM narration: agents compute scores deterministically and use the LLM to weigh/explain — keeping output explainable and reducing hallucination surface.
- Defensive LLM handling: centralized retries, multi-strategy JSON extraction, and safe defaults (
create_default_response) incall_llm. - Clean decomposition in
src/backtesting/: small single-responsibility classes (Portfolio,TradeExecutor,PerformanceMetricsCalculator,OutputBuilder) behind typed contracts intypes.py. - Reducer-based parallelism: the
AgentStatereducers (operator.add,merge_dicts) make analyst fan-out concurrency-safe by construction.
Rough edges
sys.pathinjection inapp/backend/__init__.py(self-described as temporary) couples the backend to a relativesrc/layout.blackline-length of 420 (pyproject.toml) means very long lines are normal; readability relies on structure, not wrapping.- In-memory-only cache means no cross-run reuse; long backtests re-fetch nothing within a run but everything across runs.
- No LLM timeouts (known issue #618) can hang runs.
- Two backtester implementations (legacy
src/backtester.pyand modularsrc/backtesting/) coexist intentionally during refactor, but duplicate semantics that must be kept in sync.
16. Testing
Tests use pytest (tool.poetry.group.dev.dependencies). Suites:
| Path | Covers |
|---|---|
tests/test_cache.py |
The in-memory cache merge/dedup logic |
tests/test_api_rate_limiting.py |
The 429 backoff/retry behavior |
tests/test_cli_ticker_alias.py |
--ticker/--tickers aliasing |
tests/backtesting/test_portfolio.py |
Long/short accounting, cost basis, margin |
tests/backtesting/test_execution.py |
TradeExecutor semantics |
tests/backtesting/test_metrics.py |
Sharpe/Sortino/drawdown math |
tests/backtesting/test_controller.py |
Output normalization/coercion |
tests/backtesting/test_valuation.py |
Portfolio value & exposures |
tests/backtesting/test_results.py |
Results-row building |
tests/backtesting/integration/ |
Long-only, long-short, short-only end-to-end runs (with mocks) |
v2/**/test_*.py |
v2 client, backtest, and event-study units |
Fixtures under tests/fixtures/api/ provide canned JSON responses (financial metrics, insider trades, news, prices) for AAPL/MSFT/TSLA so tests don't hit the live API.
poetry run pytest # all tests
poetry run pytest tests/backtesting -v # backtesting suite
Tip
The backtesting integration tests mock the agent and data layer (
tests/backtesting/integration/mocks.py), so they validate the engine's accounting and metrics deterministically without LLM or network calls — a good template for adding new engine tests.
17. Potential Improvements & Tech Debt
Derived from the code and the project's open GitHub issues:
| Area | Observation | Suggested direction |
|---|---|---|
| LLM timeouts (issue #618) | LLM calls have no timeout; runs can hang. | Add per-call timeouts and surface them as retryable errors. |
| Persistent cache | Cache is in-memory only; cross-run re-fetching is wasteful and rate-limit-prone. | Add an optional on-disk cache keyed by ticker/date/endpoint. |
| Data coverage (issues #617, #614) | Some ETFs/symbols aren't found; single data provider. | Pluggable data-provider abstraction behind src/tools/api.py. |
| macOS run friction (issue #635) | Reported failures running on macOS. | Harden setup scripts and document Python 3.11 requirement prominently. |
| Backtester duplication | Legacy + modular engines coexist. | Complete the migration and retire src/backtester.py. |
| Backend auth | No authentication on the API. | Add auth before any non-local deployment. |
sys.path hack |
Backend mutates sys.path to import src. |
Package the engine and depend on it properly. |
| Trade execution (issue #597) | Users ask how to connect output to real execution. | Keep simulation-only by design; document the boundary clearly. |
| Pre-trade governance (issue #643) | Idea for an independent governance gate before risk/portfolio steps. | Consider an optional policy node in the graph. |
18. Contributing
Per the project README and v2/README.md:
- Fork the repository.
- Create a feature branch.
- Commit your changes.
- Push to the branch.
- Open a Pull Request — keep PRs small and focused to ease review.
Feature requests should be opened as GitHub issues tagged enhancement. v2 contributions should start from v2/signals/base.py (the signal interface) and issues tagged v2.
Adding an analyst (worked example)
- Create
src/agents/my_investor.pywith amy_investor_agent(state, agent_id="my_investor_agent")function returning a Pydantic signal{ signal, confidence, reasoning }viacall_llm. - Import it in
src/utils/analysts.pyand add anANALYST_CONFIG["my_investor"]entry (display name, description, style,agent_func,type,order). - That's it — the CLI menus, graph builder, and web
get_agentsendpoint all derive fromANALYST_CONFIG.
Adding a model
Add an entry to src/llm/api_models.json (cloud) or src/llm/ollama_models.json (local). If it's a new provider, extend the ModelProvider enum and add a branch to get_model() in src/llm/models.py.
19. Quick Reference
Entry points
| Command | Purpose |
|---|---|
poetry run python src/main.py … |
One-shot analysis |
poetry run python src/backtester.py … |
Backtest (legacy entry) |
poetry run backtester … |
Backtest (modular engine script) |
python -m v2.backtesting / python -m v2.event_study |
v2 experimental tools |
uvicorn main:app --reload (in app/backend) |
Web backend :8000 |
npm run dev (in app/frontend) |
Web frontend :5173 |
Key files
| File | Role |
|---|---|
src/main.py |
run_hedge_fund, create_workflow |
src/graph/state.py |
AgentState + reducers |
src/utils/analysts.py |
ANALYST_CONFIG registry |
src/utils/llm.py |
call_llm (retries, JSON extraction, defaults) |
src/llm/models.py |
Provider switchboard get_model |
src/tools/api.py |
Financial Datasets client |
src/data/cache.py |
In-memory cache |
src/backtesting/engine.py |
BacktestEngine loop |
app/backend/main.py |
FastAPI app |
pyproject.toml |
Deps, scripts, version |
Performance metric defaults
| Constant | Value | Location |
|---|---|---|
| Annual trading days | 252 |
src/backtesting/metrics.py |
| Annual risk-free rate | 0.0434 (4.34%) |
src/backtesting/metrics.py |
| Default initial cash | 100000.0 |
src/cli/input.py |
| Default margin requirement | 0.0 |
src/cli/input.py |
| Default model | gpt-4.1 / OpenAI |
src/utils/llm.py |
Ports
| Port | Service |
|---|---|
8000 |
FastAPI backend |
5173 |
Vite frontend |
11434 |
Ollama |