Jesse A. Green / notebook

AI Hedge Fund — Administrator & Developer Guide

Generated June 2026 from virattt/ai-hedge-fund at 90351f3

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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

  1. Executive Summary
  2. System Architecture
  3. Repository Structure
  4. The Agent Pipeline (Deep Dive)
  5. LLM Provider Layer
  6. Data Layer
  7. The Backtesting Engine
  8. Web Application Backend
  9. Web Application Frontend
  10. The v2/ Quantitative Stack
  11. Configuration Reference
  12. Deployment
  13. Security Analysis
  14. Operations
  15. Code Quality & Patterns
  16. Testing
  17. Potential Improvements & Tech Debt
  18. Contributing
  19. 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:

  • messages accumulates via operator.add (list concatenation).
  • data and metadata merge via merge_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:

  1. Adds a start_node (identity passthrough).
  2. For each selected analyst key, looks up its (node_name, agent_func) via get_analyst_nodes() and wires start_node → analyst_node.
  3. Always adds risk_management_agent and portfolio_manager.
  4. Wires every analyst → risk_management_agent → portfolio_manager → END.
  5. Sets start_node as 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 by order, 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's get_agents endpoint.

Tip

To add a new analyst, implement an agent function following the standard contract (below), import it into analysts.py, and add one entry to ANALYST_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:

  1. Reads tickers, end_date, and the API key from state.
  2. For each ticker, fetches the data it needs via src/tools/api.py (financial metrics, line items, market cap, prices, insider trades, news).
  3. Runs deterministic scoring helpers. For example, warren_buffett_agent calls analyze_fundamentals, analyze_consistency, analyze_moat, analyze_pricing_power, analyze_book_value_growth, analyze_management_quality, and calculate_intrinsic_value, summing their sub-scores.
  4. Passes the aggregated analysis to call_llm() with a per-agent Pydantic schema (e.g. WarrenBuffettSignal { signal, confidence, reasoning }).
  5. Writes the resulting signal into state["data"]["analyst_signals"][agent_id] and emits progress updates via the shared progress tracker.

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 into analyst_signals["risk_management_agent"].

The Portfolio Manager

portfolio_management_agent (src/agents/portfolio_manager.py):

  1. Reads every analyst's signal and the risk limits. Converts each risk limit into max_shares[ticker] = position_limit // current_price.
  2. Compresses analyst signals into a compact {sig, conf} form per ticker (excluding the risk manager's own entry).
  3. Calls compute_allowed_actions() — a deterministic guardrail that, given current long/short positions and max_shares, computes the maximum permissible quantity for each action (buy, sell, short, cover, hold) and prunes zero-capacity actions to save tokens.
  4. Calls generate_trading_decision() (an LLM call) constrained to those allowed actions, producing PortfolioDecision { 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 into AVAILABLE_MODELS and LLM_ORDER.
  • src/llm/ollama_models.json → loaded into OLLAMA_MODELS and OLLAMA_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 ModelProvider enum has 14 members, but get_model() instantiates clients for 11 distinct cloud providers plus Ollama. Alibaba, Meta, and Mistral appear 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
Google 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 / OPENAI in 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 via extract_json_from_response().
  • Retries up to max_retries=3 times, updating agent progress on each failure.
  • On total failure, returns a safe default via default_factory or create_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 returns True for 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:

  1. Legacy entry — src/backtester.py, which constructs a BacktestEngine and runs it with graceful Ctrl+C handling.
  2. Modular engine — src/backtesting/, a cleanly decomposed package (also exposed as the backtester Poetry script via src/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():

  1. _prefetch_data() warms the cache: one year of prices, 10 periods of financial metrics, and up to 1000 insider trades/news items per ticker, plus SPY prices for benchmarking.
  2. Iterates pd.date_range(start, end, freq="B") (business days).
  3. 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 with TradeExecutor.
  4. Recomputes portfolio value and exposures, appends a PortfolioValuePoint, builds the day's table rows, and prints the full history (latest day first).
  5. 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.py semantics exactly while allowing incremental refactoring (see the docstrings in src/backtesting/__init__.py). The interfaces (Protocols/TypedDicts in types.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:5173 and http://127.0.0.1:5173 with 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__.py injects src/ onto sys.path so 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 request object into the graph's metadata, exposing api_keys and per-agent model config. src/utils/api_key.py::get_api_key_from_state() and src/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 including api-keys, appearance, and models (with cloud and ollama sub-panels).
  • src/services/ — typed clients to the backend: api.ts, api-keys-api.ts, backtest-api.ts, flow-service.ts, plus tab-service.ts and sidebar-storage.ts for 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 Google 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
  • [ ] .env present with data key + one LLM key
  • [ ] poetry install completed
  • [ ] Network egress to api.financialdatasets.ai and 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.py and 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 :8000 can 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_keys routes/repository) so the UI can manage them. Inspect app/backend/repositories/api_key_repository.py for storage details before treating this DB as sensitive — it should be protected like a secrets store and never committed.
  • .env is intended to be git-ignored; never commit real keys. The repository's .env.example carries only placeholders.

Network egress

  • Outbound calls go to api.financialdatasets.ai and the selected LLM provider(s). When using OpenRouter, the app sends YOUR_SITE_URL/YOUR_SITE_NAME attribution headers.
  • The data client retries on 429 with 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! in app/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.py renders a live, per-agent status table using rich.Live. Agents call progress.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 via show_agent_reasoning() in src/graph/state.py.
  • Graph visualization: --show-agent-graph calls save_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 Python logging (logger.warning on 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) in call_llm.
  • Clean decomposition in src/backtesting/: small single-responsibility classes (Portfolio, TradeExecutor, PerformanceMetricsCalculator, OutputBuilder) behind typed contracts in types.py.
  • Reducer-based parallelism: the AgentState reducers (operator.add, merge_dicts) make analyst fan-out concurrency-safe by construction.

Rough edges

  • sys.path injection in app/backend/__init__.py (self-described as temporary) couples the backend to a relative src/ layout.
  • black line-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.py and modular src/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:

  1. Fork the repository.
  2. Create a feature branch.
  3. Commit your changes.
  4. Push to the branch.
  5. 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)

  1. Create src/agents/my_investor.py with a my_investor_agent(state, agent_id="my_investor_agent") function returning a Pydantic signal { signal, confidence, reasoning } via call_llm.
  2. Import it in src/utils/analysts.py and add an ANALYST_CONFIG["my_investor"] entry (display name, description, style, agent_func, type, order).
  3. That's it — the CLI menus, graph builder, and web get_agents endpoint all derive from ANALYST_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