# AI Hedge Fund — Administrator & Developer Guide

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](#1-executive-summary)
2. [System Architecture](#2-system-architecture)
3. [Repository Structure](#3-repository-structure)
4. [The Agent Pipeline (Deep Dive)](#4-the-agent-pipeline-deep-dive)
5. [LLM Provider Layer](#5-llm-provider-layer)
6. [Data Layer](#6-data-layer)
7. [The Backtesting Engine](#7-the-backtesting-engine)
8. [Web Application Backend](#8-web-application-backend)
9. [Web Application Frontend](#9-web-application-frontend)
10. [The `v2/` Quantitative Stack](#10-the-v2-quantitative-stack)
11. [Configuration Reference](#11-configuration-reference)
12. [Deployment](#12-deployment)
13. [Security Analysis](#13-security-analysis)
14. [Operations](#14-operations)
15. [Code Quality & Patterns](#15-code-quality--patterns)
16. [Testing](#16-testing)
17. [Potential Improvements & Tech Debt](#17-potential-improvements--tech-debt)
18. [Contributing](#18-contributing)
19. [Quick Reference](#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

```text
┌──────────────────────────────────────────────────────────────────┐
│                         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`:

```python
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

```text
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:

```text
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:

```bash
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:

```bash
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`).

```bash
# 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:

```bash
# 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)).

---

## 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.

```bash
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 |

---

_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. Unofficial; not affiliated with or reviewed by the project._
