Full Code of virattt/ai-hedge-fund for AI

main 359cce737377 cached
258 files
1.5 MB
386.7k tokens
854 symbols
1 requests
Download .txt
Showing preview only (1,613K chars total). Download the full file or copy to clipboard to get everything.
Repository: virattt/ai-hedge-fund
Branch: main
Commit: 359cce737377
Files: 258
Total size: 1.5 MB

Directory structure:
gitextract_0t_9v5yp/

├── .dockerignore
├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── README.md
├── app/
│   ├── README.md
│   ├── backend/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── alembic/
│   │   │   ├── README
│   │   │   ├── env.py
│   │   │   ├── script.py.mako
│   │   │   └── versions/
│   │   │       ├── 1b1feba3d897_add_data_column_to_hedge_fund_flows.py
│   │   │       ├── 2f8c5d9e4b1a_add_hedgefundflowrun_table.py
│   │   │       ├── 3f9a6b7c8d2e_add_hedgefundflowruncycle_table.py
│   │   │       ├── 5274886e5bee_add_hedgefundflow_table.py
│   │   │       └── add_api_keys_table.py
│   │   ├── alembic.ini
│   │   ├── database/
│   │   │   ├── __init__.py
│   │   │   ├── connection.py
│   │   │   └── models.py
│   │   ├── main.py
│   │   ├── models/
│   │   │   ├── __init__.py
│   │   │   ├── events.py
│   │   │   └── schemas.py
│   │   ├── repositories/
│   │   │   ├── __init__.py
│   │   │   ├── api_key_repository.py
│   │   │   ├── flow_repository.py
│   │   │   └── flow_run_repository.py
│   │   ├── routes/
│   │   │   ├── __init__.py
│   │   │   ├── api_keys.py
│   │   │   ├── flow_runs.py
│   │   │   ├── flows.py
│   │   │   ├── health.py
│   │   │   ├── hedge_fund.py
│   │   │   ├── language_models.py
│   │   │   ├── ollama.py
│   │   │   └── storage.py
│   │   └── services/
│   │       ├── __init__.py
│   │       ├── agent_service.py
│   │       ├── api_key_service.py
│   │       ├── backtest_service.py
│   │       ├── graph.py
│   │       ├── ollama_service.py
│   │       └── portfolio.py
│   ├── frontend/
│   │   ├── .eslintrc.cjs
│   │   ├── .github/
│   │   │   └── dependabot.yml
│   │   ├── .gitignore
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── components.json
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   ├── components/
│   │   │   │   ├── Flow.tsx
│   │   │   │   ├── Layout.tsx
│   │   │   │   ├── custom-controls.tsx
│   │   │   │   ├── layout/
│   │   │   │   │   └── top-bar.tsx
│   │   │   │   ├── panels/
│   │   │   │   │   ├── bottom/
│   │   │   │   │   │   ├── bottom-panel.tsx
│   │   │   │   │   │   └── tabs/
│   │   │   │   │   │       ├── backtest-output.tsx
│   │   │   │   │   │       ├── debug-console-tab.tsx
│   │   │   │   │   │       ├── index.ts
│   │   │   │   │   │       ├── output-tab-utils.ts
│   │   │   │   │   │       ├── output-tab.tsx
│   │   │   │   │   │       ├── problems-tab.tsx
│   │   │   │   │   │       ├── reasoning-content.tsx
│   │   │   │   │   │       ├── regular-output.tsx
│   │   │   │   │   │       └── terminal-tab.tsx
│   │   │   │   │   ├── left/
│   │   │   │   │   │   ├── flow-actions.tsx
│   │   │   │   │   │   ├── flow-context-menu.tsx
│   │   │   │   │   │   ├── flow-create-dialog.tsx
│   │   │   │   │   │   ├── flow-edit-dialog.tsx
│   │   │   │   │   │   ├── flow-item-group.tsx
│   │   │   │   │   │   ├── flow-item.tsx
│   │   │   │   │   │   ├── flow-list.tsx
│   │   │   │   │   │   └── left-sidebar.tsx
│   │   │   │   │   ├── right/
│   │   │   │   │   │   ├── component-actions.tsx
│   │   │   │   │   │   ├── component-item-group.tsx
│   │   │   │   │   │   ├── component-item.tsx
│   │   │   │   │   │   ├── component-list.tsx
│   │   │   │   │   │   └── right-sidebar.tsx
│   │   │   │   │   └── search-box.tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── api-keys.tsx
│   │   │   │   │   ├── appearance.tsx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── models/
│   │   │   │   │   │   ├── cloud.tsx
│   │   │   │   │   │   └── ollama.tsx
│   │   │   │   │   ├── models.tsx
│   │   │   │   │   └── settings.tsx
│   │   │   │   ├── tabs/
│   │   │   │   │   ├── flow-tab-content.tsx
│   │   │   │   │   ├── tab-bar.tsx
│   │   │   │   │   └── tab-content.tsx
│   │   │   │   └── ui/
│   │   │   │       ├── accordion.tsx
│   │   │   │       ├── badge.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── checkbox.tsx
│   │   │   │       ├── command.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── llm-selector.tsx
│   │   │   │       ├── popover.tsx
│   │   │   │       ├── resizable.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       ├── sheet.tsx
│   │   │   │       ├── sidebar.tsx
│   │   │   │       ├── skeleton.tsx
│   │   │   │       ├── sonner.tsx
│   │   │   │       ├── table.tsx
│   │   │   │       ├── tabs.tsx
│   │   │   │       └── tooltip.tsx
│   │   │   ├── contexts/
│   │   │   │   ├── flow-context.tsx
│   │   │   │   ├── layout-context.tsx
│   │   │   │   ├── node-context.tsx
│   │   │   │   └── tabs-context.tsx
│   │   │   ├── data/
│   │   │   │   ├── agents.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── multi-node-mappings.ts
│   │   │   │   ├── node-mappings.ts
│   │   │   │   └── sidebar-components.ts
│   │   │   ├── edges/
│   │   │   │   └── index.ts
│   │   │   ├── hooks/
│   │   │   │   ├── use-component-groups.ts
│   │   │   │   ├── use-enhanced-flow-actions.ts
│   │   │   │   ├── use-flow-connection.ts
│   │   │   │   ├── use-flow-history.ts
│   │   │   │   ├── use-flow-management-tabs.ts
│   │   │   │   ├── use-flow-management.ts
│   │   │   │   ├── use-keyboard-shortcuts.ts
│   │   │   │   ├── use-mobile.tsx
│   │   │   │   ├── use-node-state.ts
│   │   │   │   ├── use-output-node-connection.ts
│   │   │   │   ├── use-resizable.ts
│   │   │   │   └── use-toast-manager.ts
│   │   │   ├── index.css
│   │   │   ├── lib/
│   │   │   │   └── utils.ts
│   │   │   ├── main.tsx
│   │   │   ├── nodes/
│   │   │   │   ├── components/
│   │   │   │   │   ├── agent-node.tsx
│   │   │   │   │   ├── agent-output-dialog.tsx
│   │   │   │   │   ├── investment-report-dialog.tsx
│   │   │   │   │   ├── investment-report-node.tsx
│   │   │   │   │   ├── json-output-dialog.tsx
│   │   │   │   │   ├── json-output-node.tsx
│   │   │   │   │   ├── node-shell.tsx
│   │   │   │   │   ├── output-node-status.tsx
│   │   │   │   │   ├── portfolio-manager-node.tsx
│   │   │   │   │   ├── portfolio-start-node.tsx
│   │   │   │   │   └── stock-analyzer-node.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── utils.ts
│   │   │   ├── providers/
│   │   │   │   └── theme-provider.tsx
│   │   │   ├── services/
│   │   │   │   ├── api-keys-api.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── backtest-api.ts
│   │   │   │   ├── flow-service.ts
│   │   │   │   ├── sidebar-storage.ts
│   │   │   │   ├── tab-service.ts
│   │   │   │   └── types.ts
│   │   │   ├── types/
│   │   │   │   └── flow.ts
│   │   │   ├── utils/
│   │   │   │   ├── date-utils.ts
│   │   │   │   └── text-utils.ts
│   │   │   └── vite-env.d.ts
│   │   ├── tailwind.config.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── run.bat
│   └── run.sh
├── docker/
│   ├── .dockerignore
│   ├── Dockerfile
│   ├── README.md
│   ├── docker-compose.yml
│   ├── run.bat
│   └── run.sh
├── pyproject.toml
├── src/
│   ├── __init__.py
│   ├── agents/
│   │   ├── __init__.py
│   │   ├── aswath_damodaran.py
│   │   ├── ben_graham.py
│   │   ├── bill_ackman.py
│   │   ├── cathie_wood.py
│   │   ├── charlie_munger.py
│   │   ├── fundamentals.py
│   │   ├── growth_agent.py
│   │   ├── michael_burry.py
│   │   ├── mohnish_pabrai.py
│   │   ├── news_sentiment.py
│   │   ├── peter_lynch.py
│   │   ├── phil_fisher.py
│   │   ├── portfolio_manager.py
│   │   ├── rakesh_jhunjhunwala.py
│   │   ├── risk_manager.py
│   │   ├── sentiment.py
│   │   ├── stanley_druckenmiller.py
│   │   ├── technicals.py
│   │   ├── valuation.py
│   │   └── warren_buffett.py
│   ├── backtester.py
│   ├── backtesting/
│   │   ├── __init__.py
│   │   ├── benchmarks.py
│   │   ├── cli.py
│   │   ├── controller.py
│   │   ├── engine.py
│   │   ├── metrics.py
│   │   ├── output.py
│   │   ├── portfolio.py
│   │   ├── trader.py
│   │   ├── types.py
│   │   └── valuation.py
│   ├── cli/
│   │   ├── __init__.py
│   │   └── input.py
│   ├── data/
│   │   ├── __init__.py
│   │   ├── cache.py
│   │   └── models.py
│   ├── graph/
│   │   ├── __init__.py
│   │   └── state.py
│   ├── llm/
│   │   ├── __init__.py
│   │   ├── api_models.json
│   │   ├── models.py
│   │   └── ollama_models.json
│   ├── main.py
│   ├── tools/
│   │   ├── __init__.py
│   │   └── api.py
│   └── utils/
│       ├── __init__.py
│       ├── analysts.py
│       ├── api_key.py
│       ├── display.py
│       ├── docker.py
│       ├── llm.py
│       ├── ollama.py
│       ├── progress.py
│       └── visualize.py
└── tests/
    ├── __init__.py
    ├── backtesting/
    │   ├── conftest.py
    │   ├── integration/
    │   │   ├── conftest.py
    │   │   ├── mocks.py
    │   │   ├── test_integration_long_only.py
    │   │   ├── test_integration_long_short.py
    │   │   └── test_integration_short_only.py
    │   ├── test_controller.py
    │   ├── test_execution.py
    │   ├── test_metrics.py
    │   ├── test_portfolio.py
    │   ├── test_results.py
    │   └── test_valuation.py
    ├── fixtures/
    │   └── api/
    │       ├── financial_metrics/
    │       │   ├── AAPL_2024-03-01_2024-03-08.json
    │       │   ├── MSFT_2024-03-01_2024-03-08.json
    │       │   └── TSLA_2024-03-01_2024-03-08.json
    │       ├── insider_trades/
    │       │   ├── AAPL_2024-03-01_2024-03-08.json
    │       │   ├── MSFT_2024-03-01_2024-03-08.json
    │       │   └── TSLA_2024-03-01_2024-03-08.json
    │       ├── news/
    │       │   ├── AAPL_2024-03-01_2024-03-08.json
    │       │   ├── MSFT_2024-03-01_2024-03-08.json
    │       │   └── TSLA_2024-03-01_2024-03-08.json
    │       └── prices/
    │           ├── AAPL_2024-03-01_2024-03-08.json
    │           ├── MSFT_2024-03-01_2024-03-08.json
    │           └── TSLA_2024-03-01_2024-03-08.json
    └── test_api_rate_limiting.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
# Git
.git
.gitignore

# Poetry
.venv
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/

# Environment
.env

# IDEs and editors
.idea/
.vscode/
*.swp
*.swo

# Logs and data
logs/
data/
*.log

# OS specific
.DS_Store
Thumbs.db 


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**Screenshot**
Add a screenshot of the bug to help explain your problem.

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

**Describe the feature you'd like**
A clear and concise description of what you want to happen.


================================================
FILE: .gitignore
================================================
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual Environment
venv/
ENV/

# Environment Variables
.env

# IDE
.idea/
.vscode/
*.swp
*.swo
.cursorrules
.cursorignore
.cursorindexingignore

# OS
.DS_Store
Thumbs.db

# graph
*.png

# Txt files
*.txt

# PDF files
*.pdf

# Frontend
node_modules

# Outputs
outputs/

# Database files (users will have their own local databases)
*.db
*.db-journal
*.db-wal
*.db-shm
*.sqlite
*.sqlite3

# Alembic (keep migration files, but ignore generated/cache files)
app/backend/alembic/versions/__pycache__/

================================================
FILE: README.md
================================================
# AI Hedge Fund

This is a proof of concept for an AI-powered hedge fund.  The goal of this project is to explore the use of AI to make trading decisions.  This project is for **educational** purposes only and is not intended for real trading or investment.

This system employs several agents working together:

1. Aswath Damodaran Agent - The Dean of Valuation, focuses on story, numbers, and disciplined valuation
2. Ben Graham Agent - The godfather of value investing, only buys hidden gems with a margin of safety
3. Bill Ackman Agent - An activist investor, takes bold positions and pushes for change
4. Cathie Wood Agent - The queen of growth investing, believes in the power of innovation and disruption
5. Charlie Munger Agent - Warren Buffett's partner, only buys wonderful businesses at fair prices
6. Michael Burry Agent - The Big Short contrarian who hunts for deep value
7. Mohnish Pabrai Agent - The Dhandho investor, who looks for doubles at low risk
8. Peter Lynch Agent - Practical investor who seeks "ten-baggers" in everyday businesses
9. Phil Fisher Agent - Meticulous growth investor who uses deep "scuttlebutt" research 
10. Rakesh Jhunjhunwala Agent - The Big Bull of India
11. Stanley Druckenmiller Agent - Macro legend who hunts for asymmetric opportunities with growth potential
12. Warren Buffett Agent - The oracle of Omaha, seeks wonderful companies at a fair price
13. Valuation Agent - Calculates the intrinsic value of a stock and generates trading signals
14. Sentiment Agent - Analyzes market sentiment and generates trading signals
15. Fundamentals Agent - Analyzes fundamental data and generates trading signals
16. Technicals Agent - Analyzes technical indicators and generates trading signals
17. Risk Manager - Calculates risk metrics and sets position limits
18. Portfolio Manager - Makes final trading decisions and generates orders

<img width="1042" alt="Screenshot 2025-03-22 at 6 19 07 PM" src="https://github.com/user-attachments/assets/cbae3dcf-b571-490d-b0ad-3f0f035ac0d4" />

Note: the system does not actually make any trades.

[![Twitter Follow](https://img.shields.io/twitter/follow/virattt?style=social)](https://twitter.com/virattt)

## Disclaimer

This project is for **educational and research purposes only**.

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

By using this software, you agree to use it solely for learning purposes.

## Table of Contents
- [How to Install](#how-to-install)
- [How to Run](#how-to-run)
  - [⌨️ Command Line Interface](#️-command-line-interface)
  - [🖥️ Web Application](#️-web-application)
- [How to Contribute](#how-to-contribute)
- [Feature Requests](#feature-requests)
- [License](#license)

## How to Install

Before you can run the AI Hedge Fund, you'll need to install it and set up your API keys. These steps are common to both the full-stack web application and command line interface.

### 1. Clone the Repository

```bash
git clone https://github.com/virattt/ai-hedge-fund.git
cd ai-hedge-fund
```

### 2. Set up API keys

Create a `.env` file for your API keys:
```bash
# Create .env file for your API keys (in the root directory)
cp .env.example .env
```

Open and edit the `.env` file to add your API keys:
```bash
# For running LLMs hosted by openai (gpt-4o, gpt-4o-mini, etc.)
OPENAI_API_KEY=your-openai-api-key

# For getting financial data to power the hedge fund
FINANCIAL_DATASETS_API_KEY=your-financial-datasets-api-key
```

**Important**: You must set at least one LLM API key (e.g. `OPENAI_API_KEY`, `GROQ_API_KEY`, `ANTHROPIC_API_KEY`, or `DEEPSEEK_API_KEY`) for the hedge fund to work. 

**Financial Data**: Data for AAPL, GOOGL, MSFT, NVDA, and TSLA is free and does not require an API key. For any other ticker, you will need to set the `FINANCIAL_DATASETS_API_KEY` in the .env file.

## How to Run

### ⌨️ Command Line Interface

You can run the AI Hedge Fund directly via terminal. This approach offers more granular control and is useful for automation, scripting, and integration purposes.

<img width="992" alt="Screenshot 2025-01-06 at 5 50 17 PM" src="https://github.com/user-attachments/assets/e8ca04bf-9989-4a7d-a8b4-34e04666663b" />

#### Quick Start

1. Install Poetry (if not already installed):
```bash
curl -sSL https://install.python-poetry.org | python3 -
```

2. Install dependencies:
```bash
poetry install
```

#### Run the AI Hedge Fund
```bash
poetry run python src/main.py --ticker AAPL,MSFT,NVDA
```

You can also specify a `--ollama` flag to run the AI hedge fund using local LLMs.

```bash
poetry run python src/main.py --ticker AAPL,MSFT,NVDA --ollama
```

You can optionally specify the start and end dates to make decisions over a specific time period.

```bash
poetry run python src/main.py --ticker AAPL,MSFT,NVDA --start-date 2024-01-01 --end-date 2024-03-01
```

#### Run the Backtester
```bash
poetry run python src/backtester.py --ticker AAPL,MSFT,NVDA
```

**Example Output:**
<img width="941" alt="Screenshot 2025-01-06 at 5 47 52 PM" src="https://github.com/user-attachments/assets/00e794ea-8628-44e6-9a84-8f8a31ad3b47" />


Note: The `--ollama`, `--start-date`, and `--end-date` flags work for the backtester, as well!

### 🖥️ Web Application

The new way to run the AI Hedge Fund is through our web application that provides a user-friendly interface. This is recommended for users who prefer visual interfaces over command line tools.

Please see detailed instructions on how to install and run the web application [here](https://github.com/virattt/ai-hedge-fund/tree/main/app).

<img width="1721" alt="Screenshot 2025-06-28 at 6 41 03 PM" src="https://github.com/user-attachments/assets/b95ab696-c9f4-416c-9ad1-51feb1f5374b" />


## How to Contribute

1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request

**Important**: Please keep your pull requests small and focused.  This will make it easier to review and merge.

## Feature Requests

If you have a feature request, please open an [issue](https://github.com/virattt/ai-hedge-fund/issues) and make sure it is tagged with `enhancement`.

## License

This project is licensed under the MIT License - see the LICENSE file for details.


================================================
FILE: app/README.md
================================================
# Web Application
The AI Hedge Fund app is a complete system with both frontend and backend components that enables you to run an AI-powered hedge fund trading system through a web interface on your own computer.

<img width="1721" alt="Screenshot 2025-06-28 at 6 41 03 PM" src="https://github.com/user-attachments/assets/b95ab696-c9f4-416c-9ad1-51feb1f5374b" />


## Overview

The AI Hedge Fund consists of:

- **Backend**: A FastAPI application that provides a REST API to run the hedge fund trading system and backtester
- **Frontend**: A React/Vite application that offers a user-friendly interface to visualize and control the hedge fund operations

## Table of Contents

- [🚀 Quick Start (For Non-Technical Users)](#-quick-start-for-non-technical-users)
  - [Option 1: Using 1-Line Shell Script (Recommended)](#option-1-using-1-line-shell-script-recommended)
  - [Option 2: Using npm (Alternative)](#option-2-using-npm-alternative)
- [🛠️ Manual Setup (For Developers)](#️-manual-setup-for-developers)
  - [Prerequisites](#prerequisites)
  - [Installation](#installation)
  - [Running the Application](#running-the-application)
- [Detailed Documentation](#detailed-documentation)
- [Disclaimer](#disclaimer)
- [Troubleshooting](#troubleshooting])

## 🚀 Quick Start (For Non-Technical Users)

**One-line setup and run command:**

### Option 1: Using 1-Line Shell Script (Recommended)

#### For Mac/Linux:
```bash
./run.sh
```

If you get a "permission denied" error, run this first:
```bash
chmod +x run.sh && ./run.sh
```

Or alternatively, you can run:
```bash
bash run.sh
```

#### For Windows:
```cmd
run.bat
```

### Option 2: Using npm (Alternative)
```bash
cd app && npm install && npm run setup
```

**That's it!** These scripts will:
1. Check for required dependencies (Node.js, Python, Poetry)
2. Install all dependencies automatically
3. Start both frontend and backend services
4. **Automatically open your web browser** to the application

**Requirements:**
- [Node.js](https://nodejs.org/) (includes npm)
- [Python 3](https://python.org/)
- [Poetry](https://python-poetry.org/)

**After running, you can access:**
- Frontend (Web Interface): http://localhost:5173
- Backend API: http://localhost:8000
- API Documentation: http://localhost:8000/docs

---

## 🛠️ Manual Setup (For Developers)

If you prefer to set up each component manually or need more control:

### Prerequisites

- Node.js and npm for the frontend
- Python 3.8+ and Poetry for the backend

### Installation

1. Clone the repository:
```bash
git clone https://github.com/virattt/ai-hedge-fund.git
cd ai-hedge-fund
```

2. Set up your environment variables:
```bash
# Create .env file for your API keys (in the root directory)
cp .env.example .env
```

3. Edit the .env file to add your API keys:
```bash
# For running LLMs hosted by openai (gpt-4o, gpt-4o-mini, etc.)
OPENAI_API_KEY=your-openai-api-key

# For running LLMs hosted by groq (deepseek, llama3, etc.)
GROQ_API_KEY=your-groq-api-key

# For getting financial data to power the hedge fund
FINANCIAL_DATASETS_API_KEY=your-financial-datasets-api-key
```

4. Install Poetry (if not already installed):
```bash
curl -sSL https://install.python-poetry.org | python3 -
```

5. Install root project dependencies:
```bash
# From the root directory
poetry install
```

6. Install backend app dependencies:
```bash
# Navigate to the backend directory
cd app/backend
pip install -r requirements.txt  # If there's a requirements.txt file
# OR
poetry install  # If there's a pyproject.toml in the backend directory
```

7. Install frontend app dependencies:
```bash
cd app/frontend
npm install  # or pnpm install or yarn install
```

### Running the Application

1. Start the backend server:
```bash
# In one terminal, from the backend directory
cd app/backend
poetry run uvicorn main:app --reload
```

2. Start the frontend application:
```bash
# In another terminal, from the frontend directory
cd app/frontend
npm run dev
```

You can now access:
- Frontend application: http://localhost:5173
- Backend API: http://localhost:8000
- API Documentation: http://localhost:8000/docs

## Detailed Documentation

For more detailed information:
- [Backend Documentation](./backend/README.md)
- [Frontend Documentation](./frontend/README.md)

## Disclaimer

This project is for **educational and research purposes only**.

- Not intended for real trading or investment
- No warranties or guarantees provided
- Creator assumes no liability for financial losses
- Consult a financial advisor for investment decisions

By using this software, you agree to use it solely for learning purposes.

## Troubleshooting

### Common Issues

#### "Command not found: uvicorn" Error
If you see this error when running the setup script:

```bash
[ERROR] Backend failed to start. Check the logs:
Command not found: uvicorn
```

**Solution:**
1. **Clean Poetry environment:**
   ```bash
   cd app/backend
   poetry env remove --all
   poetry install
   ```

2. **Or force reinstall:**
   ```bash
   cd app/backend
   poetry install --sync
   ```

3. **Verify installation:**
   ```bash
   cd app/backend
   poetry run python -c "import uvicorn; import fastapi"
   ```

#### Python Version Issues
- **Use Python 3.11**: Python 3.13+ may have compatibility issues
- **Check your Python version:** `python --version`
- **Switch Python versions if needed** (using pyenv, conda, etc.)

#### Environment Variable Issues
- **Ensure .env file exists** in the project root directory
- **Copy from template:** `cp .env.example .env`
- **Add your API keys** to the .env file

#### Permission Issues (Mac/Linux)
If you get "permission denied":
```bash
chmod +x run.sh
./run.sh
```

#### Port Already in Use
If ports 8000 or 5173 are in use:
- **Kill existing processes:** `pkill -f "uvicorn\|vite"`
- **Or use different ports** by modifying the scripts

### Getting Help
- Check the [GitHub Issues](https://github.com/virattt/ai-hedge-fund/issues)
- Follow updates on [Twitter](https://x.com/virattt) 


================================================
FILE: app/backend/README.md
================================================
# AI Hedge Fund - Backend [WIP] 🚧
This project is currently a work in progress.  To track progress, please get updates [here](https://x.com/virattt).

This is the backend server for the AI Hedge Fund project. It provides a simple REST API to interact with the AI Hedge Fund system, allowing you to run the hedge fund through a web interface.

## Overview

This backend project is a FastAPI application that serves as the server-side component of the AI Hedge Fund system. It exposes endpoints for running the hedge fund trading system and backtester.

This backend is designed to work with a future frontend application that will allow users to interact with the AI Hedge Fund system through their browser.

## Installation

### Using Poetry

1. Clone the repository:
```bash
git clone https://github.com/virattt/ai-hedge-fund.git
cd ai-hedge-fund
```

2. Install Poetry (if not already installed):
```bash
curl -sSL https://install.python-poetry.org | python3 -
```

3. Install dependencies:
```bash
# From the root directory
poetry install
```

4. Set up your environment variables:
```bash
# Create .env file for your API keys (in the root directory)
cp .env.example .env
```

5. Edit the .env file to add your API keys:
```bash
# For running LLMs hosted by openai (gpt-4o, gpt-4o-mini, etc.)
OPENAI_API_KEY=your-openai-api-key

# For running LLMs hosted by groq (deepseek, llama3, etc.)
GROQ_API_KEY=your-groq-api-key

# For getting financial data to power the hedge fund
FINANCIAL_DATASETS_API_KEY=your-financial-datasets-api-key
```

## Running the Server

To run the development server:

```bash
# Navigate to the backend directory
cd app/backend

# Start the FastAPI server with uvicorn
poetry run uvicorn main:app --reload
```

This will start the FastAPI server with hot-reloading enabled.

The API will be available at:
- API Endpoint: http://localhost:8000
- API Documentation: http://localhost:8000/docs

## API Endpoints

- `POST /hedge-fund/run`: Run the AI Hedge Fund with specified parameters
- `GET /ping`: Simple endpoint to test server connectivity

## Project Structure

```
app/backend/
├── api/                      # API layer (future expansion)
├── models/                   # Domain models
│   ├── __init__.py
│   └── schemas.py            # Pydantic schema definitions
├── routes/                   # API routes
│   ├── __init__.py           # Router registry
│   ├── hedge_fund.py         # Hedge fund endpoints
│   └── health.py             # Health check endpoints
├── services/                 # Business logic
│   ├── graph.py              # Agent graph functionality
│   └── portfolio.py          # Portfolio management
├── __init__.py               # Package initialization
└── main.py                   # FastAPI application entry point
```

## Disclaimer

This project is for **educational and research purposes only**.

- Not intended for real trading or investment
- No warranties or guarantees provided
- Creator assumes no liability for financial losses
- Consult a financial advisor for investment decisions

By using this software, you agree to use it solely for learning purposes.

================================================
FILE: app/backend/__init__.py
================================================
import sys
from pathlib import Path

# Add the src directory to Python path for imports
# This is a temporary solution while we develop the backend
src_path = str(Path(__file__).parent.parent.parent / "src")
if src_path not in sys.path:
    sys.path.append(src_path)


================================================
FILE: app/backend/alembic/README
================================================
Generic single-database configuration.

================================================
FILE: app/backend/alembic/env.py
================================================
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
from app.backend.database.models import Base
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline() -> None:
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online() -> None:
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()


================================================
FILE: app/backend/alembic/script.py.mako
================================================
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
    """Upgrade schema."""
    ${upgrades if upgrades else "pass"}


def downgrade() -> None:
    """Downgrade schema."""
    ${downgrades if downgrades else "pass"}


================================================
FILE: app/backend/alembic/versions/1b1feba3d897_add_data_column_to_hedge_fund_flows.py
================================================
"""add_data_column_to_hedge_fund_flows

Revision ID: 1b1feba3d897
Revises: 5274886e5bee
Create Date: 2025-06-22 17:30:50.992184

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '1b1feba3d897'
down_revision: Union[str, None] = '5274886e5bee'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    """Upgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('hedge_fund_flows', sa.Column('data', sa.JSON(), nullable=True))
    # ### end Alembic commands ###


def downgrade() -> None:
    """Downgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('hedge_fund_flows', 'data')
    # ### end Alembic commands ###


================================================
FILE: app/backend/alembic/versions/2f8c5d9e4b1a_add_hedgefundflowrun_table.py
================================================
"""Add HedgeFundFlowRun table

Revision ID: 2f8c5d9e4b1a
Revises: 1b1feba3d897
Create Date: 2025-01-01 12:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '2f8c5d9e4b1a'
down_revision: Union[str, None] = '1b1feba3d897'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    """Upgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('hedge_fund_flow_runs',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('flow_id', sa.Integer(), nullable=False),
    sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
    sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
    sa.Column('status', sa.String(length=50), nullable=False, server_default='IDLE'),
    sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
    sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
    sa.Column('request_data', sa.JSON(), nullable=True),
    sa.Column('results', sa.JSON(), nullable=True),
    sa.Column('error_message', sa.Text(), nullable=True),
    sa.Column('run_number', sa.Integer(), nullable=False, server_default='1'),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_hedge_fund_flow_runs_id'), 'hedge_fund_flow_runs', ['id'], unique=False)
    op.create_index(op.f('ix_hedge_fund_flow_runs_flow_id'), 'hedge_fund_flow_runs', ['flow_id'], unique=False)
    # ### end Alembic commands ###


def downgrade() -> None:
    """Downgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_index(op.f('ix_hedge_fund_flow_runs_flow_id'), table_name='hedge_fund_flow_runs')
    op.drop_index(op.f('ix_hedge_fund_flow_runs_id'), table_name='hedge_fund_flow_runs')
    op.drop_table('hedge_fund_flow_runs')
    # ### end Alembic commands ### 

================================================
FILE: app/backend/alembic/versions/3f9a6b7c8d2e_add_hedgefundflowruncycle_table.py
================================================
"""Add HedgeFundFlowRunCycle table and update HedgeFundFlowRun

Revision ID: 3f9a6b7c8d2e
Revises: 2f8c5d9e4b1a
Create Date: 2024-11-27 10:00:00.000000

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '3f9a6b7c8d2e'
down_revision = '2f8c5d9e4b1a'
branch_labels = None
depends_on = None


def upgrade():
    # Get the database connection to check existing columns
    conn = op.get_bind()
    
    # Check if columns already exist before adding them
    inspector = sa.inspect(conn)
    existing_columns = [col['name'] for col in inspector.get_columns('hedge_fund_flow_runs')]
    
    # Add new columns to hedge_fund_flow_runs table only if they don't exist
    if 'trading_mode' not in existing_columns:
        op.add_column('hedge_fund_flow_runs', sa.Column('trading_mode', sa.String(50), nullable=False, server_default='one-time'))
    if 'schedule' not in existing_columns:
        op.add_column('hedge_fund_flow_runs', sa.Column('schedule', sa.String(50), nullable=True))
    if 'duration' not in existing_columns:
        op.add_column('hedge_fund_flow_runs', sa.Column('duration', sa.String(50), nullable=True))
    if 'initial_portfolio' not in existing_columns:
        op.add_column('hedge_fund_flow_runs', sa.Column('initial_portfolio', sa.JSON, nullable=True))
    if 'final_portfolio' not in existing_columns:
        op.add_column('hedge_fund_flow_runs', sa.Column('final_portfolio', sa.JSON, nullable=True))
    
    # Create hedge_fund_flow_run_cycles table only if it doesn't exist
    existing_tables = inspector.get_table_names()
    if 'hedge_fund_flow_run_cycles' not in existing_tables:
        op.create_table(
            'hedge_fund_flow_run_cycles',
            sa.Column('id', sa.Integer, primary_key=True, index=True),
            sa.Column('flow_run_id', sa.Integer, nullable=False, index=True),
            sa.Column('cycle_number', sa.Integer, nullable=False),
            sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
            sa.Column('started_at', sa.DateTime(timezone=True), nullable=False),
            sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
            sa.Column('analyst_signals', sa.JSON, nullable=True),
            sa.Column('trading_decisions', sa.JSON, nullable=True),
            sa.Column('executed_trades', sa.JSON, nullable=True),
            sa.Column('portfolio_snapshot', sa.JSON, nullable=True),
            sa.Column('performance_metrics', sa.JSON, nullable=True),
            sa.Column('status', sa.String(50), nullable=False, server_default='IN_PROGRESS'),
            sa.Column('error_message', sa.Text, nullable=True),
            sa.Column('llm_calls_count', sa.Integer, nullable=True, server_default='0'),
            sa.Column('api_calls_count', sa.Integer, nullable=True, server_default='0'),
            sa.Column('estimated_cost', sa.String(20), nullable=True),
            sa.Column('trigger_reason', sa.String(100), nullable=True),
            sa.Column('market_conditions', sa.JSON, nullable=True),
        )
        
        # Create indexes for the new table
        op.create_index('ix_hedge_fund_flow_run_cycles_flow_run_id', 'hedge_fund_flow_run_cycles', ['flow_run_id'])
        op.create_index('ix_hedge_fund_flow_run_cycles_cycle_number', 'hedge_fund_flow_run_cycles', ['cycle_number'])
        op.create_index('ix_hedge_fund_flow_run_cycles_status', 'hedge_fund_flow_run_cycles', ['status'])
        op.create_index('ix_hedge_fund_flow_run_cycles_started_at', 'hedge_fund_flow_run_cycles', ['started_at'])


def downgrade():
    # Check if table exists before dropping
    conn = op.get_bind()
    inspector = sa.inspect(conn)
    existing_tables = inspector.get_table_names()
    
    if 'hedge_fund_flow_run_cycles' in existing_tables:
        # Drop indexes if they exist
        try:
            op.drop_index('ix_hedge_fund_flow_run_cycles_started_at', 'hedge_fund_flow_run_cycles')
            op.drop_index('ix_hedge_fund_flow_run_cycles_status', 'hedge_fund_flow_run_cycles')
            op.drop_index('ix_hedge_fund_flow_run_cycles_cycle_number', 'hedge_fund_flow_run_cycles')
            op.drop_index('ix_hedge_fund_flow_run_cycles_flow_run_id', 'hedge_fund_flow_run_cycles')
        except:
            pass  # Index may not exist
        
        # Drop hedge_fund_flow_run_cycles table
        op.drop_table('hedge_fund_flow_run_cycles')
    
    # Check existing columns before dropping
    existing_columns = [col['name'] for col in inspector.get_columns('hedge_fund_flow_runs')]
    
    # Remove columns from hedge_fund_flow_runs table only if they exist
    if 'final_portfolio' in existing_columns:
        op.drop_column('hedge_fund_flow_runs', 'final_portfolio')
    if 'initial_portfolio' in existing_columns:
        op.drop_column('hedge_fund_flow_runs', 'initial_portfolio')
    if 'duration' in existing_columns:
        op.drop_column('hedge_fund_flow_runs', 'duration')
    if 'schedule' in existing_columns:
        op.drop_column('hedge_fund_flow_runs', 'schedule')
    if 'trading_mode' in existing_columns:
        op.drop_column('hedge_fund_flow_runs', 'trading_mode') 

================================================
FILE: app/backend/alembic/versions/5274886e5bee_add_hedgefundflow_table.py
================================================
"""Add HedgeFundFlow table

Revision ID: 5274886e5bee
Revises: 
Create Date: 2025-06-20 14:50:24.736989

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '5274886e5bee'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    """Upgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('hedge_fund_flows',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
    sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
    sa.Column('name', sa.String(length=200), nullable=False),
    sa.Column('description', sa.Text(), nullable=True),
    sa.Column('nodes', sa.JSON(), nullable=False),
    sa.Column('edges', sa.JSON(), nullable=False),
    sa.Column('viewport', sa.JSON(), nullable=True),
    sa.Column('is_template', sa.Boolean(), nullable=True),
    sa.Column('tags', sa.JSON(), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_hedge_fund_flows_id'), 'hedge_fund_flows', ['id'], unique=False)
    # ### end Alembic commands ###


def downgrade() -> None:
    """Downgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_index(op.f('ix_hedge_fund_flows_id'), table_name='hedge_fund_flows')
    op.drop_table('hedge_fund_flows')
    # ### end Alembic commands ###


================================================
FILE: app/backend/alembic/versions/add_api_keys_table.py
================================================
"""add_api_keys_table

Revision ID: d5e78f9a1b2c
Revises: 3f9a6b7c8d2e
Create Date: 2025-07-27 15:20:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'd5e78f9a1b2c'
down_revision: Union[str, None] = '3f9a6b7c8d2e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    """Upgrade schema."""
    # Create API keys table
    op.create_table('api_keys',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
        sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
        sa.Column('provider', sa.String(length=100), nullable=False),
        sa.Column('key_value', sa.Text(), nullable=False),
        sa.Column('is_active', sa.Boolean(), nullable=True),
        sa.Column('description', sa.Text(), nullable=True),
        sa.Column('last_used', sa.DateTime(timezone=True), nullable=True),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('provider')
    )
    op.create_index(op.f('ix_api_keys_id'), 'api_keys', ['id'], unique=False)
    op.create_index(op.f('ix_api_keys_provider'), 'api_keys', ['provider'], unique=False)


def downgrade() -> None:
    """Downgrade schema."""
    op.drop_index(op.f('ix_api_keys_provider'), table_name='api_keys')
    op.drop_index(op.f('ix_api_keys_id'), table_name='api_keys')
    op.drop_table('api_keys') 

================================================
FILE: app/backend/alembic.ini
================================================
# A generic, single database configuration.

[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = alembic

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .

# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to alembic/versions.  When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions

# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
version_path_separator = os

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = sqlite:///./hedge_fund.db


[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts.  See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARNING
handlers = console
qualname =

[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S


================================================
FILE: app/backend/database/__init__.py
================================================
from .connection import get_db, engine, SessionLocal
from .models import Base

__all__ = ["get_db", "engine", "SessionLocal", "Base"] 

================================================
FILE: app/backend/database/connection.py
================================================
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
from pathlib import Path

# Get the backend directory path
BACKEND_DIR = Path(__file__).parent.parent
DATABASE_PATH = BACKEND_DIR / "hedge_fund.db"

# Database configuration - use absolute path
DATABASE_URL = f"sqlite:///{DATABASE_PATH}"

# Create SQLAlchemy engine
engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False}  # Needed for SQLite
)

# Create SessionLocal class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Create Base class for models
Base = declarative_base()

# Dependency for FastAPI
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close() 

================================================
FILE: app/backend/database/models.py
================================================
from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, JSON, ForeignKey
from sqlalchemy.sql import func
from .connection import Base


class HedgeFundFlow(Base):
    """Table to store React Flow configurations (nodes, edges, viewport)"""
    __tablename__ = "hedge_fund_flows"
    
    id = Column(Integer, primary_key=True, index=True)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
    
    # Flow metadata
    name = Column(String(200), nullable=False)
    description = Column(Text, nullable=True)
    
    # React Flow state
    nodes = Column(JSON, nullable=False)  # Store React Flow nodes as JSON
    edges = Column(JSON, nullable=False)  # Store React Flow edges as JSON
    viewport = Column(JSON, nullable=True)  # Store viewport state (zoom, x, y)
    data = Column(JSON, nullable=True)  # Store node internal states (tickers, models, etc.)
    
    # Additional metadata
    is_template = Column(Boolean, default=False)  # Mark as template for reuse
    tags = Column(JSON, nullable=True)  # Store tags for categorization


class HedgeFundFlowRun(Base):
    """Table to track individual execution runs of a hedge fund flow"""
    __tablename__ = "hedge_fund_flow_runs"
    
    id = Column(Integer, primary_key=True, index=True)
    flow_id = Column(Integer, ForeignKey("hedge_fund_flows.id"), nullable=False, index=True)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
    
    # Run execution tracking
    status = Column(String(50), nullable=False, default="IDLE")  # IDLE, IN_PROGRESS, COMPLETE, ERROR
    started_at = Column(DateTime(timezone=True), nullable=True)
    completed_at = Column(DateTime(timezone=True), nullable=True)
    
    # Run configuration
    trading_mode = Column(String(50), nullable=False, default="one-time")  # one-time, continuous, advisory
    schedule = Column(String(50), nullable=True)  # hourly, daily, weekly (for continuous mode)
    duration = Column(String(50), nullable=True)  # 1day, 1week, 1month (for continuous mode)
    
    # Run data
    request_data = Column(JSON, nullable=True)  # Store the request parameters (tickers, agents, models, etc.)
    initial_portfolio = Column(JSON, nullable=True)  # Store initial portfolio state
    final_portfolio = Column(JSON, nullable=True)  # Store final portfolio state
    results = Column(JSON, nullable=True)  # Store the output/results from the run
    error_message = Column(Text, nullable=True)  # Store error details if run failed
    
    # Metadata
    run_number = Column(Integer, nullable=False, default=1)  # Sequential run number for this flow


class HedgeFundFlowRunCycle(Base):
    """Individual analysis cycles within a trading session"""
    __tablename__ = "hedge_fund_flow_run_cycles"
    
    id = Column(Integer, primary_key=True, index=True)
    flow_run_id = Column(Integer, ForeignKey("hedge_fund_flow_runs.id"), nullable=False, index=True)
    cycle_number = Column(Integer, nullable=False)  # 1, 2, 3, etc. within the run
    
    # Timing
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    started_at = Column(DateTime(timezone=True), nullable=False)
    completed_at = Column(DateTime(timezone=True), nullable=True)
    
    # Analysis results
    analyst_signals = Column(JSON, nullable=True)  # All agent decisions/signals
    trading_decisions = Column(JSON, nullable=True)  # Portfolio manager decisions
    executed_trades = Column(JSON, nullable=True)  # Actual trades executed (paper trading)
    
    # Portfolio state after this cycle
    portfolio_snapshot = Column(JSON, nullable=True)  # Cash, positions, performance metrics
    
    # Performance metrics for this cycle
    performance_metrics = Column(JSON, nullable=True)  # Returns, sharpe ratio, etc.
    
    # Execution tracking
    status = Column(String(50), nullable=False, default="IN_PROGRESS")  # IN_PROGRESS, COMPLETED, ERROR
    error_message = Column(Text, nullable=True)  # Store error details if cycle failed
    
    # Cost tracking
    llm_calls_count = Column(Integer, nullable=True, default=0)  # Number of LLM calls made
    api_calls_count = Column(Integer, nullable=True, default=0)  # Number of financial API calls made
    estimated_cost = Column(String(20), nullable=True)  # Estimated cost in USD
    
    # Metadata
    trigger_reason = Column(String(100), nullable=True)  # scheduled, manual, market_event, etc.
    market_conditions = Column(JSON, nullable=True)  # Market data snapshot at cycle start


class ApiKey(Base):
    """Table to store API keys for various services"""
    __tablename__ = "api_keys"
    
    id = Column(Integer, primary_key=True, index=True)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
    
    # API key details
    provider = Column(String(100), nullable=False, unique=True, index=True)  # e.g., "ANTHROPIC_API_KEY"
    key_value = Column(Text, nullable=False)  # The actual API key (encrypted in production)
    is_active = Column(Boolean, default=True)  # Enable/disable without deletion
    
    # Optional metadata
    description = Column(Text, nullable=True)  # Human-readable description
    last_used = Column(DateTime(timezone=True), nullable=True)  # Track usage


 

================================================
FILE: app/backend/main.py
================================================
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import logging
import asyncio

from app.backend.routes import api_router
from app.backend.database.connection import engine
from app.backend.database.models import Base
from app.backend.services.ollama_service import ollama_service

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="AI Hedge Fund API", description="Backend API for AI Hedge Fund", version="0.1.0")

# Initialize database tables (this is safe to run multiple times)
Base.metadata.create_all(bind=engine)

# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],  # Frontend URLs
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include all routes
app.include_router(api_router)

@app.on_event("startup")
async def startup_event():
    """Startup event to check Ollama availability."""
    try:
        logger.info("Checking Ollama availability...")
        status = await ollama_service.check_ollama_status()
        
        if status["installed"]:
            if status["running"]:
                logger.info(f"✓ Ollama is installed and running at {status['server_url']}")
                if status["available_models"]:
                    logger.info(f"✓ Available models: {', '.join(status['available_models'])}")
                else:
                    logger.info("ℹ No models are currently downloaded")
            else:
                logger.info("ℹ Ollama is installed but not running")
                logger.info("ℹ You can start it from the Settings page or manually with 'ollama serve'")
        else:
            logger.info("ℹ Ollama is not installed. Install it to use local models.")
            logger.info("ℹ Visit https://ollama.com to download and install Ollama")
            
    except Exception as e:
        logger.warning(f"Could not check Ollama status: {e}")
        logger.info("ℹ Ollama integration is available if you install it later")


================================================
FILE: app/backend/models/__init__.py
================================================


================================================
FILE: app/backend/models/events.py
================================================
from typing import Dict, Optional, Any, Literal
from pydantic import BaseModel


class BaseEvent(BaseModel):
    """Base class for all Server-Sent Event events"""

    type: str

    def to_sse(self) -> str:
        """Convert to Server-Sent Event format"""
        event_type = self.type.lower()
        return f"event: {event_type}\ndata: {self.model_dump_json()}\n\n"


class StartEvent(BaseEvent):
    """Event indicating the start of processing"""

    type: Literal["start"] = "start"
    timestamp: Optional[str] = None

class ProgressUpdateEvent(BaseEvent):
    """Event containing an agent's progress update"""

    type: Literal["progress"] = "progress"
    agent: str
    ticker: Optional[str] = None
    status: str
    timestamp: Optional[str] = None
    analysis: Optional[str] = None

class ErrorEvent(BaseEvent):
    """Event indicating an error occurred"""

    type: Literal["error"] = "error"
    message: str
    timestamp: Optional[str] = None


class CompleteEvent(BaseEvent):
    """Event indicating successful completion with results"""

    type: Literal["complete"] = "complete"
    data: Dict[str, Any]
    timestamp: Optional[str] = None


================================================
FILE: app/backend/models/schemas.py
================================================
from datetime import datetime, timedelta
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional, Dict, Any
from src.llm.models import ModelProvider
from enum import Enum
from app.backend.services.graph import extract_base_agent_key


class FlowRunStatus(str, Enum):
    IDLE = "IDLE"
    IN_PROGRESS = "IN_PROGRESS"
    COMPLETE = "COMPLETE"
    ERROR = "ERROR"


class AgentModelConfig(BaseModel):
    agent_id: str
    model_name: Optional[str] = None
    model_provider: Optional[ModelProvider] = None


class PortfolioPosition(BaseModel):
    ticker: str
    quantity: float
    trade_price: float

    @field_validator('trade_price')
    @classmethod
    def price_must_be_positive(cls, v: float) -> float:
        if v <= 0:
            raise ValueError('Trade price must be positive!')
        return v


class GraphNode(BaseModel):
    id: str
    type: Optional[str] = None
    data: Optional[Dict[str, Any]] = None
    position: Optional[Dict[str, Any]] = None


class GraphEdge(BaseModel):
    id: str
    source: str
    target: str
    type: Optional[str] = None
    data: Optional[Dict[str, Any]] = None


class HedgeFundResponse(BaseModel):
    decisions: dict
    analyst_signals: dict


class ErrorResponse(BaseModel):
    message: str
    error: str | None = None


# Base class for shared fields between HedgeFundRequest and BacktestRequest
class BaseHedgeFundRequest(BaseModel):
    tickers: List[str]
    graph_nodes: List[GraphNode]
    graph_edges: List[GraphEdge]
    agent_models: Optional[List[AgentModelConfig]] = None
    model_name: Optional[str] = "gpt-4.1"
    model_provider: Optional[ModelProvider] = ModelProvider.OPENAI
    margin_requirement: float = 0.0
    portfolio_positions: Optional[List[PortfolioPosition]] = None
    api_keys: Optional[Dict[str, str]] = None

    def get_agent_ids(self) -> List[str]:
        """Extract agent IDs from graph structure"""
        return [node.id for node in self.graph_nodes]

    def get_agent_model_config(self, agent_id: str) -> tuple[str, ModelProvider]:
        """Get model configuration for a specific agent"""
        if self.agent_models:
            # Extract base agent key from unique node ID for matching
            base_agent_key = extract_base_agent_key(agent_id)
            
            for config in self.agent_models:
                # Check both unique node ID and base agent key for matches
                config_base_key = extract_base_agent_key(config.agent_id)
                if config.agent_id == agent_id or config_base_key == base_agent_key:
                    return (
                        config.model_name or self.model_name,
                        config.model_provider or self.model_provider
                    )
        # Fallback to global model settings
        return self.model_name, self.model_provider


class BacktestRequest(BaseHedgeFundRequest):
    start_date: str
    end_date: str
    initial_capital: float = 100000.0


class BacktestDayResult(BaseModel):
    date: str
    portfolio_value: float
    cash: float
    decisions: Dict[str, Any]
    executed_trades: Dict[str, int]
    analyst_signals: Dict[str, Any]
    current_prices: Dict[str, float]
    long_exposure: float
    short_exposure: float
    gross_exposure: float
    net_exposure: float
    long_short_ratio: Optional[float] = None


class BacktestPerformanceMetrics(BaseModel):
    sharpe_ratio: Optional[float] = None
    sortino_ratio: Optional[float] = None
    max_drawdown: Optional[float] = None
    max_drawdown_date: Optional[str] = None
    long_short_ratio: Optional[float] = None
    gross_exposure: Optional[float] = None
    net_exposure: Optional[float] = None


class BacktestResponse(BaseModel):
    results: List[BacktestDayResult]
    performance_metrics: BacktestPerformanceMetrics
    final_portfolio: Dict[str, Any]


class HedgeFundRequest(BaseHedgeFundRequest):
    end_date: Optional[str] = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d"))
    start_date: Optional[str] = None
    initial_cash: float = 100000.0

    def get_start_date(self) -> str:
        """Calculate start date if not provided"""
        if self.start_date:
            return self.start_date
        return (datetime.strptime(self.end_date, "%Y-%m-%d") - timedelta(days=90)).strftime("%Y-%m-%d")


# Flow-related schemas
class FlowCreateRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    description: Optional[str] = None
    nodes: List[Dict[str, Any]]
    edges: List[Dict[str, Any]]
    viewport: Optional[Dict[str, Any]] = None
    data: Optional[Dict[str, Any]] = None
    is_template: bool = False
    tags: Optional[List[str]] = None


class FlowUpdateRequest(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=200)
    description: Optional[str] = None
    nodes: Optional[List[Dict[str, Any]]] = None
    edges: Optional[List[Dict[str, Any]]] = None
    viewport: Optional[Dict[str, Any]] = None
    data: Optional[Dict[str, Any]] = None
    is_template: Optional[bool] = None
    tags: Optional[List[str]] = None


class FlowResponse(BaseModel):
    id: int
    name: str
    description: Optional[str]
    nodes: List[Dict[str, Any]]
    edges: List[Dict[str, Any]]
    viewport: Optional[Dict[str, Any]]
    data: Optional[Dict[str, Any]]
    is_template: bool
    tags: Optional[List[str]]
    created_at: datetime
    updated_at: Optional[datetime]

    class Config:
        from_attributes = True


class FlowSummaryResponse(BaseModel):
    """Lightweight flow response without nodes/edges for listing"""
    id: int
    name: str
    description: Optional[str]
    is_template: bool
    tags: Optional[List[str]]
    created_at: datetime
    updated_at: Optional[datetime]

    class Config:
        from_attributes = True


# Flow Run schemas
class FlowRunCreateRequest(BaseModel):
    """Request to create a new flow run"""
    request_data: Optional[Dict[str, Any]] = None


class FlowRunUpdateRequest(BaseModel):
    """Request to update an existing flow run"""
    status: Optional[FlowRunStatus] = None
    results: Optional[Dict[str, Any]] = None
    error_message: Optional[str] = None


class FlowRunResponse(BaseModel):
    """Complete flow run response"""
    id: int
    flow_id: int
    status: FlowRunStatus
    run_number: int
    created_at: datetime
    updated_at: Optional[datetime]
    started_at: Optional[datetime]
    completed_at: Optional[datetime]
    request_data: Optional[Dict[str, Any]]
    results: Optional[Dict[str, Any]]
    error_message: Optional[str]

    class Config:
        from_attributes = True


class FlowRunSummaryResponse(BaseModel):
    """Lightweight flow run response for listing"""
    id: int
    flow_id: int
    status: FlowRunStatus
    run_number: int
    created_at: datetime
    started_at: Optional[datetime]
    completed_at: Optional[datetime]
    error_message: Optional[str]

    class Config:
        from_attributes = True


# API Key schemas
class ApiKeyCreateRequest(BaseModel):
    """Request to create or update an API key"""
    provider: str = Field(..., min_length=1, max_length=100)
    key_value: str = Field(..., min_length=1)
    description: Optional[str] = None
    is_active: bool = True


class ApiKeyUpdateRequest(BaseModel):
    """Request to update an existing API key"""
    key_value: Optional[str] = Field(None, min_length=1)
    description: Optional[str] = None
    is_active: Optional[bool] = None


class ApiKeyResponse(BaseModel):
    """Complete API key response"""
    id: int
    provider: str
    key_value: str
    is_active: bool
    description: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]
    last_used: Optional[datetime]

    class Config:
        from_attributes = True


class ApiKeySummaryResponse(BaseModel):
    """API key response without the actual key value"""
    id: int
    provider: str
    is_active: bool
    description: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]
    last_used: Optional[datetime]
    has_key: bool = True  # Indicates if a key is set

    class Config:
        from_attributes = True


class ApiKeyBulkUpdateRequest(BaseModel):
    """Request to update multiple API keys at once"""
    api_keys: List[ApiKeyCreateRequest]


================================================
FILE: app/backend/repositories/__init__.py
================================================
from .flow_repository import FlowRepository

__all__ = ["FlowRepository"] 

================================================
FILE: app/backend/repositories/api_key_repository.py
================================================
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Optional
from datetime import datetime

from app.backend.database.models import ApiKey


class ApiKeyRepository:
    """Repository for API key database operations"""
    
    def __init__(self, db: Session):
        self.db = db

    def create_or_update_api_key(
        self, 
        provider: str, 
        key_value: str, 
        description: str = None, 
        is_active: bool = True
    ) -> ApiKey:
        """Create a new API key or update existing one"""
        # Check if API key already exists for this provider
        existing_key = self.db.query(ApiKey).filter(ApiKey.provider == provider).first()
        
        if existing_key:
            # Update existing key
            existing_key.key_value = key_value
            existing_key.description = description
            existing_key.is_active = is_active
            existing_key.updated_at = func.now()
            self.db.commit()
            self.db.refresh(existing_key)
            return existing_key
        else:
            # Create new key
            api_key = ApiKey(
                provider=provider,
                key_value=key_value,
                description=description,
                is_active=is_active
            )
            self.db.add(api_key)
            self.db.commit()
            self.db.refresh(api_key)
            return api_key

    def get_api_key_by_provider(self, provider: str) -> Optional[ApiKey]:
        """Get API key by provider name"""
        return self.db.query(ApiKey).filter(
            ApiKey.provider == provider,
            ApiKey.is_active == True
        ).first()

    def get_all_api_keys(self, include_inactive: bool = False) -> List[ApiKey]:
        """Get all API keys"""
        query = self.db.query(ApiKey)
        if not include_inactive:
            query = query.filter(ApiKey.is_active == True)
        return query.order_by(ApiKey.provider).all()

    def update_api_key(
        self, 
        provider: str, 
        key_value: str = None, 
        description: str = None, 
        is_active: bool = None
    ) -> Optional[ApiKey]:
        """Update an existing API key"""
        api_key = self.db.query(ApiKey).filter(ApiKey.provider == provider).first()
        if not api_key:
            return None

        if key_value is not None:
            api_key.key_value = key_value
        if description is not None:
            api_key.description = description
        if is_active is not None:
            api_key.is_active = is_active
        
        api_key.updated_at = func.now()
        self.db.commit()
        self.db.refresh(api_key)
        return api_key

    def delete_api_key(self, provider: str) -> bool:
        """Delete an API key by provider"""
        api_key = self.db.query(ApiKey).filter(ApiKey.provider == provider).first()
        if not api_key:
            return False
        
        self.db.delete(api_key)
        self.db.commit()
        return True

    def deactivate_api_key(self, provider: str) -> bool:
        """Deactivate an API key instead of deleting it"""
        api_key = self.db.query(ApiKey).filter(ApiKey.provider == provider).first()
        if not api_key:
            return False
        
        api_key.is_active = False
        api_key.updated_at = func.now()
        self.db.commit()
        return True

    def update_last_used(self, provider: str) -> bool:
        """Update the last_used timestamp for an API key"""
        api_key = self.db.query(ApiKey).filter(
            ApiKey.provider == provider,
            ApiKey.is_active == True
        ).first()
        if not api_key:
            return False
        
        api_key.last_used = func.now()
        self.db.commit()
        return True

    def bulk_create_or_update(self, api_keys_data: List[dict]) -> List[ApiKey]:
        """Bulk create or update multiple API keys"""
        results = []
        for data in api_keys_data:
            api_key = self.create_or_update_api_key(
                provider=data['provider'],
                key_value=data['key_value'],
                description=data.get('description'),
                is_active=data.get('is_active', True)
            )
            results.append(api_key)
        return results 

================================================
FILE: app/backend/repositories/flow_repository.py
================================================
from typing import List, Optional
from sqlalchemy.orm import Session
from app.backend.database.models import HedgeFundFlow


class FlowRepository:
    """Repository for HedgeFundFlow CRUD operations"""
    
    def __init__(self, db: Session):
        self.db = db
    
    def create_flow(self, name: str, nodes: dict, edges: dict, description: str = None, 
                   viewport: dict = None, data: dict = None, is_template: bool = False, tags: List[str] = None) -> HedgeFundFlow:
        """Create a new hedge fund flow"""
        flow = HedgeFundFlow(
            name=name,
            description=description,
            nodes=nodes,
            edges=edges,
            viewport=viewport,
            data=data,
            is_template=is_template,
            tags=tags or []
        )
        self.db.add(flow)
        self.db.commit()
        self.db.refresh(flow)
        return flow
    
    def get_flow_by_id(self, flow_id: int) -> Optional[HedgeFundFlow]:
        """Get a flow by its ID"""
        return self.db.query(HedgeFundFlow).filter(HedgeFundFlow.id == flow_id).first()
    
    def get_all_flows(self, include_templates: bool = True) -> List[HedgeFundFlow]:
        """Get all flows, optionally excluding templates"""
        query = self.db.query(HedgeFundFlow)
        if not include_templates:
            query = query.filter(HedgeFundFlow.is_template == False)
        return query.order_by(HedgeFundFlow.updated_at.desc()).all()
    
    def get_flows_by_name(self, name: str) -> List[HedgeFundFlow]:
        """Search flows by name (case-insensitive partial match)"""
        return self.db.query(HedgeFundFlow).filter(
            HedgeFundFlow.name.ilike(f"%{name}%")
        ).order_by(HedgeFundFlow.updated_at.desc()).all()
    
    def update_flow(self, flow_id: int, name: str = None, description: str = None,
                   nodes: dict = None, edges: dict = None, viewport: dict = None, data: dict = None,
                   is_template: bool = None, tags: List[str] = None) -> Optional[HedgeFundFlow]:
        """Update an existing flow"""
        flow = self.get_flow_by_id(flow_id)
        if not flow:
            return None
        
        if name is not None:
            flow.name = name
        if description is not None:
            flow.description = description
        if nodes is not None:
            flow.nodes = nodes
        if edges is not None:
            flow.edges = edges
        if viewport is not None:
            flow.viewport = viewport
        if data is not None:
            flow.data = data
        if is_template is not None:
            flow.is_template = is_template
        if tags is not None:
            flow.tags = tags
        
        self.db.commit()
        self.db.refresh(flow)
        return flow
    
    def delete_flow(self, flow_id: int) -> bool:
        """Delete a flow by ID"""
        flow = self.get_flow_by_id(flow_id)
        if not flow:
            return False
        
        self.db.delete(flow)
        self.db.commit()
        return True
    
    def duplicate_flow(self, flow_id: int, new_name: str = None) -> Optional[HedgeFundFlow]:
        """Create a copy of an existing flow"""
        original = self.get_flow_by_id(flow_id)
        if not original:
            return None
        
        copy_name = new_name or f"{original.name} (Copy)"
        
        return self.create_flow(
            name=copy_name,
            description=original.description,
            nodes=original.nodes,
            edges=original.edges,
            viewport=original.viewport,
            data=original.data,
            is_template=False,  # Copies are not templates by default
            tags=original.tags
        ) 

================================================
FILE: app/backend/repositories/flow_run_repository.py
================================================
from typing import List, Optional, Dict, Any
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy import desc, func
from app.backend.database.models import HedgeFundFlowRun
from app.backend.models.schemas import FlowRunStatus


class FlowRunRepository:
    """Repository for HedgeFundFlowRun CRUD operations"""
    
    def __init__(self, db: Session):
        self.db = db
    
    def create_flow_run(self, flow_id: int, request_data: Dict[str, Any] = None) -> HedgeFundFlowRun:
        """Create a new flow run"""
        # Get the next run number for this flow
        run_number = self._get_next_run_number(flow_id)
        
        flow_run = HedgeFundFlowRun(
            flow_id=flow_id,
            request_data=request_data,
            run_number=run_number,
            status=FlowRunStatus.IDLE.value
        )
        self.db.add(flow_run)
        self.db.commit()
        self.db.refresh(flow_run)
        return flow_run
    
    def get_flow_run_by_id(self, run_id: int) -> Optional[HedgeFundFlowRun]:
        """Get a flow run by its ID"""
        return self.db.query(HedgeFundFlowRun).filter(HedgeFundFlowRun.id == run_id).first()
    
    def get_flow_runs_by_flow_id(self, flow_id: int, limit: int = 50, offset: int = 0) -> List[HedgeFundFlowRun]:
        """Get all runs for a specific flow, ordered by most recent first"""
        return (
            self.db.query(HedgeFundFlowRun)
            .filter(HedgeFundFlowRun.flow_id == flow_id)
            .order_by(desc(HedgeFundFlowRun.created_at))
            .limit(limit)
            .offset(offset)
            .all()
        )
    
    def get_active_flow_run(self, flow_id: int) -> Optional[HedgeFundFlowRun]:
        """Get the current active (IN_PROGRESS) run for a flow"""
        return (
            self.db.query(HedgeFundFlowRun)
            .filter(
                HedgeFundFlowRun.flow_id == flow_id,
                HedgeFundFlowRun.status == FlowRunStatus.IN_PROGRESS.value
            )
            .first()
        )
    
    def get_latest_flow_run(self, flow_id: int) -> Optional[HedgeFundFlowRun]:
        """Get the most recent run for a flow"""
        return (
            self.db.query(HedgeFundFlowRun)
            .filter(HedgeFundFlowRun.flow_id == flow_id)
            .order_by(desc(HedgeFundFlowRun.created_at))
            .first()
        )
    
    def update_flow_run(
        self,
        run_id: int,
        status: Optional[FlowRunStatus] = None,
        results: Optional[Dict[str, Any]] = None,
        error_message: Optional[str] = None
    ) -> Optional[HedgeFundFlowRun]:
        """Update an existing flow run"""
        flow_run = self.get_flow_run_by_id(run_id)
        if not flow_run:
            return None
        
        # Update status and timing
        if status is not None:
            flow_run.status = status.value
            
            # Update timing based on status
            if status == FlowRunStatus.IN_PROGRESS and not flow_run.started_at:
                flow_run.started_at = datetime.utcnow()
            elif status in [FlowRunStatus.COMPLETE, FlowRunStatus.ERROR] and not flow_run.completed_at:
                flow_run.completed_at = datetime.utcnow()
        
        # Update results and error message
        if results is not None:
            flow_run.results = results
        if error_message is not None:
            flow_run.error_message = error_message
        
        self.db.commit()
        self.db.refresh(flow_run)
        return flow_run
    
    def delete_flow_run(self, run_id: int) -> bool:
        """Delete a flow run by ID"""
        flow_run = self.get_flow_run_by_id(run_id)
        if not flow_run:
            return False
        
        self.db.delete(flow_run)
        self.db.commit()
        return True
    
    def delete_flow_runs_by_flow_id(self, flow_id: int) -> int:
        """Delete all runs for a specific flow. Returns count of deleted runs."""
        deleted_count = (
            self.db.query(HedgeFundFlowRun)
            .filter(HedgeFundFlowRun.flow_id == flow_id)
            .delete()
        )
        self.db.commit()
        return deleted_count
    
    def get_flow_run_count(self, flow_id: int) -> int:
        """Get total count of runs for a flow"""
        return (
            self.db.query(HedgeFundFlowRun)
            .filter(HedgeFundFlowRun.flow_id == flow_id)
            .count()
        )
    
    def _get_next_run_number(self, flow_id: int) -> int:
        """Get the next run number for a flow"""
        max_run_number = (
            self.db.query(func.max(HedgeFundFlowRun.run_number))
            .filter(HedgeFundFlowRun.flow_id == flow_id)
            .scalar()
        )
        return (max_run_number or 0) + 1 

================================================
FILE: app/backend/routes/__init__.py
================================================
from fastapi import APIRouter

from app.backend.routes.hedge_fund import router as hedge_fund_router
from app.backend.routes.health import router as health_router
from app.backend.routes.storage import router as storage_router
from app.backend.routes.flows import router as flows_router
from app.backend.routes.flow_runs import router as flow_runs_router
from app.backend.routes.ollama import router as ollama_router
from app.backend.routes.language_models import router as language_models_router
from app.backend.routes.api_keys import router as api_keys_router

# Main API router
api_router = APIRouter()

# Include sub-routers
api_router.include_router(health_router, tags=["health"])
api_router.include_router(hedge_fund_router, tags=["hedge-fund"])
api_router.include_router(storage_router, tags=["storage"])
api_router.include_router(flows_router, tags=["flows"])
api_router.include_router(flow_runs_router, tags=["flow-runs"])
api_router.include_router(ollama_router, tags=["ollama"])
api_router.include_router(language_models_router, tags=["language-models"])
api_router.include_router(api_keys_router, tags=["api-keys"])


================================================
FILE: app/backend/routes/api_keys.py
================================================
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from typing import List

from app.backend.database import get_db
from app.backend.repositories.api_key_repository import ApiKeyRepository
from app.backend.models.schemas import (
    ApiKeyCreateRequest,
    ApiKeyUpdateRequest,
    ApiKeyResponse,
    ApiKeySummaryResponse,
    ApiKeyBulkUpdateRequest,
    ErrorResponse
)

router = APIRouter(prefix="/api-keys", tags=["api-keys"])


@router.post(
    "/",
    response_model=ApiKeyResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Invalid request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def create_or_update_api_key(request: ApiKeyCreateRequest, db: Session = Depends(get_db)):
    """Create a new API key or update existing one"""
    try:
        repo = ApiKeyRepository(db)
        api_key = repo.create_or_update_api_key(
            provider=request.provider,
            key_value=request.key_value,
            description=request.description,
            is_active=request.is_active
        )
        return ApiKeyResponse.from_orm(api_key)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to create/update API key: {str(e)}")


@router.get(
    "/",
    response_model=List[ApiKeySummaryResponse],
    responses={
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_api_keys(include_inactive: bool = False, db: Session = Depends(get_db)):
    """Get all API keys (without actual key values for security)"""
    try:
        repo = ApiKeyRepository(db)
        api_keys = repo.get_all_api_keys(include_inactive=include_inactive)
        return [ApiKeySummaryResponse.from_orm(key) for key in api_keys]
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve API keys: {str(e)}")


@router.get(
    "/{provider}",
    response_model=ApiKeyResponse,
    responses={
        404: {"model": ErrorResponse, "description": "API key not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_api_key(provider: str, db: Session = Depends(get_db)):
    """Get a specific API key by provider"""
    try:
        repo = ApiKeyRepository(db)
        api_key = repo.get_api_key_by_provider(provider)
        if not api_key:
            raise HTTPException(status_code=404, detail="API key not found")
        return ApiKeyResponse.from_orm(api_key)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve API key: {str(e)}")


@router.put(
    "/{provider}",
    response_model=ApiKeyResponse,
    responses={
        404: {"model": ErrorResponse, "description": "API key not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def update_api_key(provider: str, request: ApiKeyUpdateRequest, db: Session = Depends(get_db)):
    """Update an existing API key"""
    try:
        repo = ApiKeyRepository(db)
        api_key = repo.update_api_key(
            provider=provider,
            key_value=request.key_value,
            description=request.description,
            is_active=request.is_active
        )
        if not api_key:
            raise HTTPException(status_code=404, detail="API key not found")
        return ApiKeyResponse.from_orm(api_key)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to update API key: {str(e)}")


@router.delete(
    "/{provider}",
    responses={
        204: {"description": "API key deleted successfully"},
        404: {"model": ErrorResponse, "description": "API key not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def delete_api_key(provider: str, db: Session = Depends(get_db)):
    """Delete an API key"""
    try:
        repo = ApiKeyRepository(db)
        success = repo.delete_api_key(provider)
        if not success:
            raise HTTPException(status_code=404, detail="API key not found")
        return {"message": "API key deleted successfully"}
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to delete API key: {str(e)}")


@router.patch(
    "/{provider}/deactivate",
    response_model=ApiKeySummaryResponse,
    responses={
        404: {"model": ErrorResponse, "description": "API key not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def deactivate_api_key(provider: str, db: Session = Depends(get_db)):
    """Deactivate an API key without deleting it"""
    try:
        repo = ApiKeyRepository(db)
        success = repo.deactivate_api_key(provider)
        if not success:
            raise HTTPException(status_code=404, detail="API key not found")
        
        # Return the updated key
        api_key = repo.get_api_key_by_provider(provider)
        return ApiKeySummaryResponse.from_orm(api_key)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to deactivate API key: {str(e)}")


@router.post(
    "/bulk",
    response_model=List[ApiKeyResponse],
    responses={
        400: {"model": ErrorResponse, "description": "Invalid request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def bulk_update_api_keys(request: ApiKeyBulkUpdateRequest, db: Session = Depends(get_db)):
    """Bulk create or update multiple API keys"""
    try:
        repo = ApiKeyRepository(db)
        api_keys_data = [
            {
                'provider': key.provider,
                'key_value': key.key_value,
                'description': key.description,
                'is_active': key.is_active
            }
            for key in request.api_keys
        ]
        api_keys = repo.bulk_create_or_update(api_keys_data)
        return [ApiKeyResponse.from_orm(key) for key in api_keys]
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to bulk update API keys: {str(e)}")


@router.patch(
    "/{provider}/last-used",
    responses={
        200: {"description": "Last used timestamp updated"},
        404: {"model": ErrorResponse, "description": "API key not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def update_last_used(provider: str, db: Session = Depends(get_db)):
    """Update the last used timestamp for an API key"""
    try:
        repo = ApiKeyRepository(db)
        success = repo.update_last_used(provider)
        if not success:
            raise HTTPException(status_code=404, detail="API key not found")
        return {"message": "Last used timestamp updated"}
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to update last used timestamp: {str(e)}") 

================================================
FILE: app/backend/routes/flow_runs.py
================================================
from fastapi import APIRouter, HTTPException, Depends, Query
from sqlalchemy.orm import Session
from typing import List, Optional

from app.backend.database import get_db
from app.backend.repositories.flow_run_repository import FlowRunRepository
from app.backend.repositories.flow_repository import FlowRepository
from app.backend.models.schemas import (
    FlowRunCreateRequest,
    FlowRunUpdateRequest,
    FlowRunResponse,
    FlowRunSummaryResponse,
    FlowRunStatus,
    ErrorResponse
)

router = APIRouter(prefix="/flows/{flow_id}/runs", tags=["flow-runs"])


@router.post(
    "/",
    response_model=FlowRunResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def create_flow_run(
    flow_id: int, 
    request: FlowRunCreateRequest, 
    db: Session = Depends(get_db)
):
    """Create a new flow run for the specified flow"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Create the flow run
        run_repo = FlowRunRepository(db)
        flow_run = run_repo.create_flow_run(
            flow_id=flow_id,
            request_data=request.request_data
        )
        return FlowRunResponse.from_orm(flow_run)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to create flow run: {str(e)}")


@router.get(
    "/",
    response_model=List[FlowRunSummaryResponse],
    responses={
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_flow_runs(
    flow_id: int,
    limit: int = Query(50, ge=1, le=100, description="Maximum number of runs to return"),
    offset: int = Query(0, ge=0, description="Number of runs to skip"),
    db: Session = Depends(get_db)
):
    """Get all runs for the specified flow"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Get flow runs
        run_repo = FlowRunRepository(db)
        flow_runs = run_repo.get_flow_runs_by_flow_id(flow_id, limit=limit, offset=offset)
        return [FlowRunSummaryResponse.from_orm(run) for run in flow_runs]
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve flow runs: {str(e)}")


@router.get(
    "/active",
    response_model=Optional[FlowRunResponse],
    responses={
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_active_flow_run(flow_id: int, db: Session = Depends(get_db)):
    """Get the current active (IN_PROGRESS) run for the specified flow"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Get active flow run
        run_repo = FlowRunRepository(db)
        active_run = run_repo.get_active_flow_run(flow_id)
        return FlowRunResponse.from_orm(active_run) if active_run else None
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve active flow run: {str(e)}")


@router.get(
    "/latest",
    response_model=Optional[FlowRunResponse],
    responses={
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_latest_flow_run(flow_id: int, db: Session = Depends(get_db)):
    """Get the most recent run for the specified flow"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Get latest flow run
        run_repo = FlowRunRepository(db)
        latest_run = run_repo.get_latest_flow_run(flow_id)
        return FlowRunResponse.from_orm(latest_run) if latest_run else None
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve latest flow run: {str(e)}")


@router.get(
    "/{run_id}",
    response_model=FlowRunResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Flow or run not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_flow_run(flow_id: int, run_id: int, db: Session = Depends(get_db)):
    """Get a specific flow run by ID"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Get flow run
        run_repo = FlowRunRepository(db)
        flow_run = run_repo.get_flow_run_by_id(run_id)
        if not flow_run or flow_run.flow_id != flow_id:
            raise HTTPException(status_code=404, detail="Flow run not found")
        
        return FlowRunResponse.from_orm(flow_run)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve flow run: {str(e)}")


@router.put(
    "/{run_id}",
    response_model=FlowRunResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Flow or run not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def update_flow_run(
    flow_id: int, 
    run_id: int, 
    request: FlowRunUpdateRequest, 
    db: Session = Depends(get_db)
):
    """Update an existing flow run"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Update flow run
        run_repo = FlowRunRepository(db)
        # First verify the run exists and belongs to this flow
        existing_run = run_repo.get_flow_run_by_id(run_id)
        if not existing_run or existing_run.flow_id != flow_id:
            raise HTTPException(status_code=404, detail="Flow run not found")
        
        flow_run = run_repo.update_flow_run(
            run_id=run_id,
            status=request.status,
            results=request.results,
            error_message=request.error_message
        )
        
        if not flow_run:
            raise HTTPException(status_code=404, detail="Flow run not found")
        
        return FlowRunResponse.from_orm(flow_run)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to update flow run: {str(e)}")


@router.delete(
    "/{run_id}",
    responses={
        204: {"description": "Flow run deleted successfully"},
        404: {"model": ErrorResponse, "description": "Flow or run not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def delete_flow_run(flow_id: int, run_id: int, db: Session = Depends(get_db)):
    """Delete a flow run"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Verify run exists and belongs to this flow
        run_repo = FlowRunRepository(db)
        existing_run = run_repo.get_flow_run_by_id(run_id)
        if not existing_run or existing_run.flow_id != flow_id:
            raise HTTPException(status_code=404, detail="Flow run not found")
        
        success = run_repo.delete_flow_run(run_id)
        if not success:
            raise HTTPException(status_code=404, detail="Flow run not found")
        
        return {"message": "Flow run deleted successfully"}
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to delete flow run: {str(e)}")


@router.delete(
    "/",
    responses={
        204: {"description": "All flow runs deleted successfully"},
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def delete_all_flow_runs(flow_id: int, db: Session = Depends(get_db)):
    """Delete all runs for the specified flow"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Delete all flow runs
        run_repo = FlowRunRepository(db)
        deleted_count = run_repo.delete_flow_runs_by_flow_id(flow_id)
        
        return {"message": f"Deleted {deleted_count} flow runs successfully"}
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to delete flow runs: {str(e)}")


@router.get(
    "/count",
    responses={
        200: {"description": "Flow run count"},
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_flow_run_count(flow_id: int, db: Session = Depends(get_db)):
    """Get the total count of runs for the specified flow"""
    try:
        # Verify flow exists
        flow_repo = FlowRepository(db)
        flow = flow_repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        
        # Get run count
        run_repo = FlowRunRepository(db)
        count = run_repo.get_flow_run_count(flow_id)
        
        return {"flow_id": flow_id, "total_runs": count}
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to get flow run count: {str(e)}") 

================================================
FILE: app/backend/routes/flows.py
================================================
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from typing import List

from app.backend.database import get_db
from app.backend.repositories.flow_repository import FlowRepository
from app.backend.models.schemas import (
    FlowCreateRequest, 
    FlowUpdateRequest, 
    FlowResponse, 
    FlowSummaryResponse,
    ErrorResponse
)

router = APIRouter(prefix="/flows", tags=["flows"])


@router.post(
    "/",
    response_model=FlowResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Invalid request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def create_flow(request: FlowCreateRequest, db: Session = Depends(get_db)):
    """Create a new hedge fund flow"""
    try:
        repo = FlowRepository(db)
        flow = repo.create_flow(
            name=request.name,
            description=request.description,
            nodes=request.nodes,
            edges=request.edges,
            viewport=request.viewport,
            data=request.data,
            is_template=request.is_template,
            tags=request.tags
        )
        return FlowResponse.from_orm(flow)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to create flow: {str(e)}")


@router.get(
    "/",
    response_model=List[FlowSummaryResponse],
    responses={
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_flows(include_templates: bool = True, db: Session = Depends(get_db)):
    """Get all flows (summary view)"""
    try:
        repo = FlowRepository(db)
        flows = repo.get_all_flows(include_templates=include_templates)
        return [FlowSummaryResponse.from_orm(flow) for flow in flows]
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve flows: {str(e)}")


@router.get(
    "/{flow_id}",
    response_model=FlowResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_flow(flow_id: int, db: Session = Depends(get_db)):
    """Get a specific flow by ID"""
    try:
        repo = FlowRepository(db)
        flow = repo.get_flow_by_id(flow_id)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        return FlowResponse.from_orm(flow)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve flow: {str(e)}")


@router.put(
    "/{flow_id}",
    response_model=FlowResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def update_flow(flow_id: int, request: FlowUpdateRequest, db: Session = Depends(get_db)):
    """Update an existing flow"""
    try:
        repo = FlowRepository(db)
        flow = repo.update_flow(
            flow_id=flow_id,
            name=request.name,
            description=request.description,
            nodes=request.nodes,
            edges=request.edges,
            viewport=request.viewport,
            data=request.data,
            is_template=request.is_template,
            tags=request.tags
        )
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        return FlowResponse.from_orm(flow)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to update flow: {str(e)}")


@router.delete(
    "/{flow_id}",
    responses={
        204: {"description": "Flow deleted successfully"},
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def delete_flow(flow_id: int, db: Session = Depends(get_db)):
    """Delete a flow"""
    try:
        repo = FlowRepository(db)
        success = repo.delete_flow(flow_id)
        if not success:
            raise HTTPException(status_code=404, detail="Flow not found")
        return {"message": "Flow deleted successfully"}
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to delete flow: {str(e)}")


@router.post(
    "/{flow_id}/duplicate",
    response_model=FlowResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Flow not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def duplicate_flow(flow_id: int, new_name: str = None, db: Session = Depends(get_db)):
    """Create a copy of an existing flow"""
    try:
        repo = FlowRepository(db)
        flow = repo.duplicate_flow(flow_id, new_name)
        if not flow:
            raise HTTPException(status_code=404, detail="Flow not found")
        return FlowResponse.from_orm(flow)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to duplicate flow: {str(e)}")


@router.get(
    "/search/{name}",
    response_model=List[FlowSummaryResponse],
    responses={
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def search_flows(name: str, db: Session = Depends(get_db)):
    """Search flows by name"""
    try:
        repo = FlowRepository(db)
        flows = repo.get_flows_by_name(name)
        return [FlowSummaryResponse.from_orm(flow) for flow in flows]
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to search flows: {str(e)}") 

================================================
FILE: app/backend/routes/health.py
================================================
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
import asyncio
import json

router = APIRouter()


@router.get("/")
async def root():
    return {"message": "Welcome to AI Hedge Fund API"}


@router.get("/ping")
async def ping():
    async def event_generator():
        for i in range(5):
            # Create a JSON object for each ping
            data = {"ping": f"ping {i+1}/5", "timestamp": i + 1}

            # Format as SSE
            yield f"data: {json.dumps(data)}\n\n"

            # Wait 1 second
            await asyncio.sleep(1)

    return StreamingResponse(event_generator(), media_type="text/event-stream")


================================================
FILE: app/backend/routes/hedge_fund.py
================================================
from fastapi import APIRouter, HTTPException, Request, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
import asyncio

from app.backend.database import get_db
from app.backend.models.schemas import ErrorResponse, HedgeFundRequest, BacktestRequest, BacktestDayResult, BacktestPerformanceMetrics
from app.backend.models.events import StartEvent, ProgressUpdateEvent, ErrorEvent, CompleteEvent
from app.backend.services.graph import create_graph, parse_hedge_fund_response, run_graph_async
from app.backend.services.portfolio import create_portfolio
from app.backend.services.backtest_service import BacktestService
from app.backend.services.api_key_service import ApiKeyService
from src.utils.progress import progress
from src.utils.analysts import get_agents_list

router = APIRouter(prefix="/hedge-fund")

@router.post(
    path="/run",
    responses={
        200: {"description": "Successful response with streaming updates"},
        400: {"model": ErrorResponse, "description": "Invalid request parameters"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def run(request_data: HedgeFundRequest, request: Request, db: Session = Depends(get_db)):
    try:
        # Hydrate API keys from database if not provided
        if not request_data.api_keys:
            api_key_service = ApiKeyService(db)
            request_data.api_keys = api_key_service.get_api_keys_dict()

        # Create the portfolio
        portfolio = create_portfolio(request_data.initial_cash, request_data.margin_requirement, request_data.tickers, request_data.portfolio_positions)

        # Construct agent graph using the React Flow graph structure
        graph = create_graph(
            graph_nodes=request_data.graph_nodes,
            graph_edges=request_data.graph_edges
        )
        graph = graph.compile()

        # Log a test progress update for debugging
        progress.update_status("system", None, "Preparing hedge fund run")

        # Convert model_provider to string if it's an enum
        model_provider = request_data.model_provider
        if hasattr(model_provider, "value"):
            model_provider = model_provider.value

        # Function to detect client disconnection
        async def wait_for_disconnect():
            """Wait for client disconnect and return True when it happens"""
            try:
                while True:
                    message = await request.receive()
                    if message["type"] == "http.disconnect":
                        return True
            except Exception:
                return True

        # Set up streaming response
        async def event_generator():
            # Queue for progress updates
            progress_queue = asyncio.Queue()
            run_task = None
            disconnect_task = None

            # Simple handler to add updates to the queue
            def progress_handler(agent_name, ticker, status, analysis, timestamp):
                event = ProgressUpdateEvent(agent=agent_name, ticker=ticker, status=status, timestamp=timestamp, analysis=analysis)
                progress_queue.put_nowait(event)

            # Register our handler with the progress tracker
            progress.register_handler(progress_handler)

            try:
                # Start the graph execution in a background task
                run_task = asyncio.create_task(
                    run_graph_async(
                        graph=graph,
                        portfolio=portfolio,
                        tickers=request_data.tickers,
                        start_date=request_data.start_date,
                        end_date=request_data.end_date,
                        model_name=request_data.model_name,
                        model_provider=model_provider,
                        request=request_data,  # Pass the full request for agent-specific model access
                    )
                )
                
                # Start the disconnect detection task
                disconnect_task = asyncio.create_task(wait_for_disconnect())
                
                # Send initial message
                yield StartEvent().to_sse()

                # Stream progress updates until run_task completes or client disconnects
                while not run_task.done():
                    # Check if client disconnected
                    if disconnect_task.done():
                        print("Client disconnected, cancelling hedge fund execution")
                        run_task.cancel()
                        try:
                            await run_task
                        except asyncio.CancelledError:
                            pass
                        return

                    # Either get a progress update or wait a bit
                    try:
                        event = await asyncio.wait_for(progress_queue.get(), timeout=1.0)
                        yield event.to_sse()
                    except asyncio.TimeoutError:
                        # Just continue the loop
                        pass

                # Get the final result
                try:
                    result = await run_task
                except asyncio.CancelledError:
                    print("Task was cancelled")
                    return

                if not result or not result.get("messages"):
                    yield ErrorEvent(message="Failed to generate hedge fund decisions").to_sse()
                    return

                # Send the final result
                final_data = CompleteEvent(
                    data={
                        "decisions": parse_hedge_fund_response(result.get("messages", [])[-1].content),
                        "analyst_signals": result.get("data", {}).get("analyst_signals", {}),
                        "current_prices": result.get("data", {}).get("current_prices", {}),
                    }
                )
                yield final_data.to_sse()

            except asyncio.CancelledError:
                print("Event generator cancelled")
                return
            finally:
                # Clean up
                progress.unregister_handler(progress_handler)
                if run_task and not run_task.done():
                    run_task.cancel()
                    try:
                        await run_task
                    except asyncio.CancelledError:
                        pass
                if disconnect_task and not disconnect_task.done():
                    disconnect_task.cancel()

        # Return a streaming response
        return StreamingResponse(event_generator(), media_type="text/event-stream")

    except HTTPException as e:
        raise e
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"An error occurred while processing the request: {str(e)}")

@router.post(
    path="/backtest",
    responses={
        200: {"description": "Successful response with streaming backtest updates"},
        400: {"model": ErrorResponse, "description": "Invalid request parameters"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def backtest(request_data: BacktestRequest, request: Request, db: Session = Depends(get_db)):
    """Run a continuous backtest over a time period with streaming updates."""
    try:
        # Hydrate API keys from database if not provided
        if not request_data.api_keys:
            api_key_service = ApiKeyService(db)
            request_data.api_keys = api_key_service.get_api_keys_dict()

        # Convert model_provider to string if it's an enum
        model_provider = request_data.model_provider
        if hasattr(model_provider, "value"):
            model_provider = model_provider.value

        # Create the portfolio (same as /run endpoint)
        portfolio = create_portfolio(
            request_data.initial_capital, 
            request_data.margin_requirement, 
            request_data.tickers, 
            request_data.portfolio_positions
        )

        # Construct agent graph using the React Flow graph structure (same as /run endpoint)
        graph = create_graph(graph_nodes=request_data.graph_nodes, graph_edges=request_data.graph_edges)
        graph = graph.compile()

        # Create backtest service with the compiled graph
        backtest_service = BacktestService(
            graph=graph,
            portfolio=portfolio,
            tickers=request_data.tickers,
            start_date=request_data.start_date,
            end_date=request_data.end_date,
            initial_capital=request_data.initial_capital,
            model_name=request_data.model_name,
            model_provider=model_provider,
            request=request_data,  # Pass the full request for agent-specific model access
        )

        # Function to detect client disconnection
        async def wait_for_disconnect():
            """Wait for client disconnect and return True when it happens"""
            try:
                while True:
                    message = await request.receive()
                    if message["type"] == "http.disconnect":
                        return True
            except Exception:
                return True

        # Set up streaming response
        async def event_generator():
            progress_queue = asyncio.Queue()
            backtest_task = None
            disconnect_task = None

            # Global progress handler to capture individual agent updates during backtest
            def progress_handler(agent_name, ticker, status, analysis, timestamp):
                event = ProgressUpdateEvent(agent=agent_name, ticker=ticker, status=status, timestamp=timestamp, analysis=analysis)
                progress_queue.put_nowait(event)

            # Progress callback to handle backtest-specific updates
            def progress_callback(update):
                if update["type"] == "progress":
                    event = ProgressUpdateEvent(
                        agent="backtest",
                        ticker=None,
                        status=f"Processing {update['current_date']} ({update['current_step']}/{update['total_dates']})",
                        timestamp=None,
                        analysis=None
                    )
                    progress_queue.put_nowait(event)
                elif update["type"] == "backtest_result":
                    # Convert day result to a streaming event
                    backtest_result = BacktestDayResult(**update["data"])
                    
                    # Send the full day result data as JSON in the analysis field
                    import json
                    analysis_data = json.dumps(update["data"])
                    
                    event = ProgressUpdateEvent(
                        agent="backtest",
                        ticker=None,
                        status=f"Completed {backtest_result.date} - Portfolio: ${backtest_result.portfolio_value:,.2f}",
                        timestamp=None,
                        analysis=analysis_data
                    )
                    progress_queue.put_nowait(event)

            # Register our handler with the progress tracker to capture agent updates
            progress.register_handler(progress_handler)
            
            try:
                # Start the backtest in a background task
                backtest_task = asyncio.create_task(
                    backtest_service.run_backtest_async(progress_callback=progress_callback)
                )
                
                # Start the disconnect detection task
                disconnect_task = asyncio.create_task(wait_for_disconnect())
                
                # Send initial message
                yield StartEvent().to_sse()

                # Stream progress updates until backtest_task completes or client disconnects
                while not backtest_task.done():
                    # Check if client disconnected
                    if disconnect_task.done():
                        print("Client disconnected, cancelling backtest execution")
                        backtest_task.cancel()
                        try:
                            await backtest_task
                        except asyncio.CancelledError:
                            pass
                        return

                    # Either get a progress update or wait a bit
                    try:
                        event = await asyncio.wait_for(progress_queue.get(), timeout=1.0)
                        yield event.to_sse()
                    except asyncio.TimeoutError:
                        # Just continue the loop
                        pass

                # Get the final result
                try:
                    result = await backtest_task
                except asyncio.CancelledError:
                    print("Backtest task was cancelled")
                    return

                if not result:
                    yield ErrorEvent(message="Failed to complete backtest").to_sse()
                    return

                # Send the final result
                performance_metrics = BacktestPerformanceMetrics(**result["performance_metrics"])
                final_data = CompleteEvent(
                    data={
                        "performance_metrics": performance_metrics.model_dump(),
                        "final_portfolio": result["final_portfolio"],
                        "total_days": len(result["results"]),
                    }
                )
                yield final_data.to_sse()

            except asyncio.CancelledError:
                print("Backtest event generator cancelled")
                return
            finally:
                # Clean up
                progress.unregister_handler(progress_handler)
                if backtest_task and not backtest_task.done():
                    backtest_task.cancel()
                    try:
                        await backtest_task
                    except asyncio.CancelledError:
                        pass
                if disconnect_task and not disconnect_task.done():
                    disconnect_task.cancel()

        # Return a streaming response
        return StreamingResponse(event_generator(), media_type="text/event-stream")

    except HTTPException as e:
        raise e
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"An error occurred while processing the backtest request: {str(e)}")


@router.get(
    path="/agents",
    responses={
        200: {"description": "List of available agents"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_agents():
    """Get the list of available agents."""
    try:
        return {"agents": get_agents_list()}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve agents: {str(e)}")



================================================
FILE: app/backend/routes/language_models.py
================================================
from fastapi import APIRouter, HTTPException
from typing import List, Dict, Any

from app.backend.models.schemas import ErrorResponse
from app.backend.services.ollama_service import OllamaService
from src.llm.models import get_models_list

router = APIRouter(prefix="/language-models")

# Initialize Ollama service
ollama_service = OllamaService()

@router.get(
    path="/",
    responses={
        200: {"description": "List of available language models"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_language_models():
    """Get the list of available cloud-based and Ollama language models."""
    try:
        # Start with cloud models
        models = get_models_list()
        
        # Add available Ollama models (handles all checking internally)
        ollama_models = await ollama_service.get_available_models()
        models.extend(ollama_models)
        
        return {"models": models}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve models: {str(e)}")

@router.get(
    path="/providers",
    responses={
        200: {"description": "List of available model providers"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_language_model_providers():
    """Get the list of available model providers with their models grouped."""
    try:
        models = get_models_list()
        
        # Group models by provider
        providers = {}
        for model in models:
            provider_name = model["provider"]
            if provider_name not in providers:
                providers[provider_name] = {
                    "name": provider_name,
                    "models": []
                }
            providers[provider_name]["models"].append({
                "display_name": model["display_name"],
                "model_name": model["model_name"]
            })
        
        return {"providers": list(providers.values())}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to retrieve providers: {str(e)}") 

================================================
FILE: app/backend/routes/ollama.py
================================================
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List, Dict, Any
import logging

from app.backend.models.schemas import ErrorResponse
from app.backend.services.ollama_service import ollama_service

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/ollama")

class ModelRequest(BaseModel):
    model_name: str

class OllamaStatusResponse(BaseModel):
    installed: bool
    running: bool
    available_models: List[str]
    server_url: str
    error: str | None = None

class ActionResponse(BaseModel):
    success: bool
    message: str

class RecommendedModel(BaseModel):
    display_name: str
    model_name: str
    provider: str

class ProgressResponse(BaseModel):
    status: str
    percentage: float | None = None
    message: str | None = None
    phase: str | None = None
    bytes_downloaded: int | None = None
    total_bytes: int | None = None

@router.get(
    "/status",
    response_model=OllamaStatusResponse,
    responses={
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_ollama_status():
    """Get Ollama installation and server status."""
    try:
        status = await ollama_service.check_ollama_status()
        return OllamaStatusResponse(**status)
    except Exception as e:
        logger.error(f"Failed to check Ollama status: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to check Ollama status: {str(e)}")

@router.post(
    "/start",
    response_model=ActionResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Bad request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def start_ollama_server():
    """Start the Ollama server."""
    try:
        # First check if it's already running
        status = await ollama_service.check_ollama_status()
        if not status["installed"]:
            raise HTTPException(status_code=400, detail="Ollama is not installed on this system")
        
        if status["running"]:
            return ActionResponse(success=True, message="Ollama server is already running")
        
        result = await ollama_service.start_server()
        
        if not result["success"]:
            logger.error(f"Failed to start Ollama server: {result['message']}")
            raise HTTPException(status_code=500, detail=result["message"])
        
        return ActionResponse(**result)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error starting Ollama server: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to start Ollama server: {str(e)}")

@router.post(
    "/stop",
    response_model=ActionResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Bad request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def stop_ollama_server():
    """Stop the Ollama server."""
    try:
        # First check if it's installed
        status = await ollama_service.check_ollama_status()
        if not status["installed"]:
            raise HTTPException(status_code=400, detail="Ollama is not installed on this system")
        
        if not status["running"]:
            return ActionResponse(success=True, message="Ollama server is already stopped")
        
        result = await ollama_service.stop_server()
        
        if not result["success"]:
            logger.error(f"Failed to stop Ollama server: {result['message']}")
            raise HTTPException(status_code=500, detail=result["message"])
        
        return ActionResponse(**result)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error stopping Ollama server: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to stop Ollama server: {str(e)}")

@router.post(
    "/models/download",
    response_model=ActionResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Bad request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def download_model(request: ModelRequest):
    """Download an Ollama model (legacy endpoint)."""
    try:
        logger.info(f"Download request for model: {request.model_name}")
        
        # Check current status
        status = await ollama_service.check_ollama_status()
        logger.debug(f"Current Ollama status: installed={status['installed']}, running={status['running']}")
        
        if not status["installed"]:
            raise HTTPException(status_code=400, detail="Ollama is not installed on this system")
        
        if not status["running"]:
            raise HTTPException(status_code=400, detail="Ollama server is not running. Please start it first.")
        
        result = await ollama_service.download_model(request.model_name)
        
        if not result["success"]:
            logger.error(f"Failed to download model {request.model_name}: {result['message']}")
            raise HTTPException(status_code=500, detail=result["message"])
        
        logger.info(f"Successfully downloaded model: {request.model_name}")
        return ActionResponse(**result)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error downloading model {request.model_name}: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to download model: {str(e)}")

@router.post(
    "/models/download/progress",
    responses={
        400: {"model": ErrorResponse, "description": "Bad request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def download_model_with_progress(request: ModelRequest):
    """Download an Ollama model with real-time progress updates via Server-Sent Events."""
    try:
        logger.info(f"Progress download request for model: {request.model_name}")
        
        # Check current status
        status = await ollama_service.check_ollama_status()
        logger.debug(f"Current Ollama status: installed={status['installed']}, running={status['running']}")
        
        if not status["installed"]:
            raise HTTPException(status_code=400, detail="Ollama is not installed on this system")
        
        if not status["running"]:
            raise HTTPException(status_code=400, detail="Ollama server is not running. Please start it first.")
        
        # Return Server-Sent Events stream
        return StreamingResponse(
            ollama_service.download_model_with_progress(request.model_name),
            media_type="text/event-stream",
            headers={
                "Cache-Control": "no-cache",
                "Connection": "keep-alive",
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Headers": "*",
            }
        )
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error setting up progress download for {request.model_name}: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to start progress download: {str(e)}")

@router.get(
    "/models/download/progress/{model_name}",
    response_model=ProgressResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Model download not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_download_progress(model_name: str):
    """Get current download progress for a specific model."""
    try:
        progress = ollama_service.get_download_progress(model_name)
        if progress is None:
            raise HTTPException(status_code=404, detail=f"No active download found for model: {model_name}")
        
        return ProgressResponse(**progress)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error getting download progress for {model_name}: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to get download progress: {str(e)}")

@router.get(
    "/models/downloads/active",
    response_model=Dict[str, ProgressResponse],
    responses={
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_active_downloads():
    """Get all currently active model downloads."""
    try:
        active_downloads = {}
        all_progress = ollama_service.get_all_download_progress()
        
        # Only return downloads that are actually active (not completed, error, or cancelled)
        for model_name, progress in all_progress.items():
            if progress.get("status") in ["starting", "downloading"]:
                active_downloads[model_name] = ProgressResponse(**progress)
        
        return active_downloads
    except Exception as e:
        logger.error(f"Error getting active downloads: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to get active downloads: {str(e)}")

@router.delete(
    "/models/{model_name}",
    response_model=ActionResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Bad request"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def delete_model(model_name: str):
    """Delete an Ollama model."""
    try:
        logger.info(f"Delete request for model: {model_name}")
        
        # Check current status
        status = await ollama_service.check_ollama_status()
        logger.debug(f"Current Ollama status: installed={status['installed']}, running={status['running']}")
        
        if not status["installed"]:
            raise HTTPException(status_code=400, detail="Ollama is not installed on this system")
        
        if not status["running"]:
            raise HTTPException(status_code=400, detail="Ollama server is not running. Please start it first.")
        
        result = await ollama_service.delete_model(model_name)
        
        if not result["success"]:
            logger.error(f"Failed to delete model {model_name}: {result['message']}")
            raise HTTPException(status_code=500, detail=result["message"])
        
        logger.info(f"Successfully deleted model: {model_name}")
        return ActionResponse(**result)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error deleting model {model_name}: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to delete model: {str(e)}")

@router.get(
    "/models/recommended",
    response_model=List[RecommendedModel],
    responses={
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def get_recommended_models():
    """Get list of recommended Ollama models."""
    try:
        models = await ollama_service.get_recommended_models()
        return [RecommendedModel(**model) for model in models]
    except Exception as e:
        logger.error(f"Failed to get recommended models: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to get recommended models: {str(e)}")

@router.delete(
    "/models/download/{model_name}",
    response_model=ActionResponse,
    responses={
        404: {"model": ErrorResponse, "description": "Download not found"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def cancel_download(model_name: str):
    """Cancel an active model download."""
    try:
        logger.info(f"Cancel download request for model: {model_name}")
        
        success = ollama_service.cancel_download(model_name)
        
        if success:
            return ActionResponse(success=True, message=f"Download cancelled for {model_name}")
        else:
            raise HTTPException(status_code=404, detail=f"No active download found for model: {model_name}")
            
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error cancelling download for {model_name}: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to cancel download: {str(e)}") 

================================================
FILE: app/backend/routes/storage.py
================================================
from fastapi import APIRouter, HTTPException
import json
from pathlib import Path
from pydantic import BaseModel

from app.backend.models.schemas import ErrorResponse

router = APIRouter(prefix="/storage")

class SaveJsonRequest(BaseModel):
    filename: str
    data: dict

@router.post(
    path="/save-json",
    responses={
        200: {"description": "File saved successfully"},
        400: {"model": ErrorResponse, "description": "Invalid request parameters"},
        500: {"model": ErrorResponse, "description": "Internal server error"},
    },
)
async def save_json_file(request: SaveJsonRequest):
    """Save JSON data to the project's /outputs directory."""
    try:
        # Create outputs directory if it doesn't exist
        project_root = Path(__file__).parent.parent.parent.parent  # Navigate to project root
        outputs_dir = project_root / "outputs"
        outputs_dir.mkdir(exist_ok=True)
        
        # Construct file path
        file_path = outputs_dir / request.filename
        
        # Save JSON data to file
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(request.data, f, indent=2, ensure_ascii=False)
        
        return {
            "success": True,
            "message": f"File saved successfully to {file_path}",
            "filename": request.filename
        }
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}") 

================================================
FILE: app/backend/services/__init__.py
================================================


================================================
FILE: app/backend/services/agent_service.py
================================================
from functools import partial
from typing import Callable
from src.graph.state import AgentState

def create_agent_function(agent_function: Callable, agent_id: str) -> Callable[[AgentState], dict]:
    """
    Creates a new function from an agent function that accepts an agent_id.

    :param agent_function: The agent function to wrap.
    :param agent_id: The ID to be passed to the agent.
    :return: A new function that can be called by LangGraph.
    """
    return partial(agent_function, agent_id=agent_id) 

================================================
FILE: app/backend/services/api_key_service.py
================================================
from sqlalchemy.orm import Session
from typing import Dict, Optional
from app.backend.repositories.api_key_repository import ApiKeyRepository


class ApiKeyService:
    """Simple service to load API keys for requests"""
    
    def __init__(self, db: Session):
        self.repository = ApiKeyRepository(db)
    
    def get_api_keys_dict(self) -> Dict[str, str]:
        """
        Load all active API keys from database and return as a dictionary
        suitable for injecting into requests
        """
        api_keys = self.repository.get_all_api_keys(include_inactive=False)
        return {key.provider: key.key_value for key in api_keys}
    
    def get_api_key(self, provider: str) -> Optional[str]:
        """Get a specific API key by provider"""
        api_key = self.repository.get_api_key_by_provider(provider)
        return api_key.key_value if api_key else None 

================================================
FILE: app/backend/services/backtest_service.py
================================================
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import pandas as pd
import numpy as np
from typing import Callable, Dict, List, Optional, Any
import asyncio

from src.tools.api import (
    get_company_news,
    get_price_data,
    get_prices,
    get_financial_metrics,
    get_insider_trades,
)
from app.backend.services.graph import run_graph_async, parse_hedge_fund_response
from app.backend.services.portfolio import create_portfolio

class BacktestService:
    """
    Core backtesting service that focuses purely on backtesting logic.
    Uses a pre-compiled graph and portfolio for trading decisions.
    """

    def __init__(
        self,
        graph,
        portfolio: dict,
        tickers: List[str],
        start_date: str,
        end_date: str,
        initial_capital: float,
        model_name: str = "gpt-4.1",
        model_provider: str = "OpenAI",
        request: dict = {},
    ):
        """
        Initialize the backtest service.
        
        :param graph: Pre-compiled LangGraph graph for trading decisions.
        :param portfolio: Initial portfolio state.
        :param tickers: List of tickers to backtest.
        :param start_date: Start date string (YYYY-MM-DD).
        :param end_date: End date string (YYYY-MM-DD).
        :param initial_capital: Starting portfolio cash.
        :param model_name: Which LLM model name to use.
        :param model_provider: Which LLM provider.
        :param request: Request object containing API keys and other metadata.
        """
        self.graph = graph
        self.portfolio = portfolio
        self.tickers = tickers
        self.start_date = start_date
        self.end_date = end_date
        self.initial_capital = initial_capital
        self.model_name = model_name
        self.model_provider = model_provider
        self.request = request
        self.portfolio_values = []

    def execute_trade(self, ticker: str, action: str, quantity: float, current_price: float) -> int:
        """
        Execute trades with support for both long and short positions.
        Returns the actual quantity traded.
        """
        if quantity <= 0:
            return 0

        quantity = int(quantity)  # force integer shares
        position = self.portfolio["positions"][ticker]

        if action == "buy":
            cost = quantity * current_price
            if cost <= self.portfolio["cash"]:
                # Weighted average cost basis for the new total
                old_shares = position["long"]
                old_cost_basis = position["long_cost_basis"]
                new_shares = quantity
                total_shares = old_shares + new_shares

                if total_shares > 0:
                    total_old_cost = old_cost_basis * old_shares
                    total_new_cost = cost
                    position["long_cost_basis"] = (total_old_cost + total_new_cost) / total_shares

                position["long"] += quantity
                self.portfolio["cash"] -= cost
                return quantity
            else:
                # Calculate maximum affordable quantity
                max_quantity = int(self.portfolio["cash"] / current_price)
                if max_quantity > 0:
                    cost = max_quantity * current_price
                    old_shares = position["long"]
                    old_cost_basis = position["long_cost_basis"]
                    total_shares = old_shares + max_quantity

                    if total_shares > 0:
                        total_old_cost = old_cost_basis * old_shares
                        total_new_cost = cost
                        position["long_cost_basis"] = (total_old_cost + total_new_cost) / total_shares

                    position["long"] += max_quantity
                    self.portfolio["cash"] -= cost
                    return max_quantity
                return 0

        elif action == "sell":
            quantity = min(quantity, position["long"])
            if quantity > 0:
                avg_cost_per_share = position["long_cost_basis"] if position["long"] > 0 else 0
                realized_gain = (current_price - avg_cost_per_share) * quantity
                self.portfolio["realized_gains"][ticker]["long"] += realized_gain

                position["long"] -= quantity
                self.portfolio["cash"] += quantity * current_price

                if position["long"] == 0:
                    position["long_cost_basis"] = 0.0

                return quantity

        elif action == "short":
            proceeds = current_price * quantity
            margin_required = proceeds * self.portfolio["margin_requirement"]
            available_cash = max(
                0.0, self.portfolio["cash"] - self.portfolio["margin_used"]
            )
            if margin_required <= available_cash:
                # Weighted average short cost basis
                old_short_shares = position["short"]
                old_cost_basis = position["short_cost_basis"]
                new_shares = quantity
                total_shares = old_short_shares + new_shares

                if total_shares > 0:
                    total_old_cost = old_cost_basis * old_short_shares
                    total_new_cost = current_price * new_shares
                    position["short_cost_basis"] = (total_old_cost + total_new_cost) / total_shares

                position["short"] += quantity
                position["short_margin_used"] += margin_required
                self.portfolio["margin_used"] += margin_required

                self.portfolio["cash"] += proceeds
                self.portfolio["cash"] -= margin_required
                return quantity
            else:
                margin_ratio = self.portfolio["margin_requirement"]
                if margin_ratio > 0:
                    max_quantity = int(available_cash / (current_price * margin_ratio))
                else:
                    max_quantity = 0

                if max_quantity > 0:
                    proceeds = current_price * max_quantity
                    margin_required = proceeds * margin_ratio

                    old_short_shares = position["short"]
                    old_cost_basis = position["short_cost_basis"]
                    total_shares = old_short_shares + max_quantity

                    if total_shares > 0:
                        total_old_cost = old_cost_basis * old_short_shares
                        total_new_cost = current_price * max_quantity
                        position["short_cost_basis"] = (total_old_cost + total_new_cost) / total_shares

                    position["short"] += max_quantity
                    position["short_margin_used"] += margin_required
                    self.portfolio["margin_used"] += margin_required

                    self.portfolio["cash"] += proceeds
                    self.portfolio["cash"] -= margin_required
                    return max_quantity
                return 0

        elif action == "cover":
            quantity = min(quantity, position["short"])
            if quantity > 0:
                cover_cost = quantity * current_price
                avg_short_price = position["short_cost_basis"] if position["short"] > 0 else 0
                realized_gain = (avg_short_price - current_price) * quantity

                if position["short"] > 0:
                    portion = quantity / position["short"]
                else:
                    portion = 1.0

                margin_to_release = portion * position["short_margin_used"]

                position["short"] -= quantity
                position["short_margin_used"] -= margin_to_release
                self.portfolio["margin_used"] -= margin_to_release

                self.portfolio["cash"] += margin_to_release
                self.portfolio["cash"] -= cover_cost

                self.portfolio["realized_gains"][ticker]["short"] += realized_gain

                if position["short"] == 0:
                    position["short_cost_basis"] = 0.0
                    position["short_margin_used"] = 0.0

                return quantity

        return 0

    def calculate_portfolio_value(self, current_prices: Dict[str, float]) -> float:
        """Calculate total portfolio value."""
        total_value = self.portfolio["cash"]

        for ticker in self.tickers:
            position = self.portfolio["positions"][ticker]
            price = current_prices[ticker]

            # Long position value
            long_value = position["long"] * price
            total_value += long_value

            # Short position unrealized PnL
            if position["short"] > 0:
                total_value -= position["short"] * price

        return total_value

    def prefetch_data(self):
        """Pre-fetch all data needed for the backtest period."""
        end_date_dt = datetime.strptime(self.end_date, "%Y-%m-%d")
        start_date_dt = end_date_dt - relativedelta(years=1)
        start_date_str = start_date_dt.strftime("%Y-%m-%d")
        api_key = self.request.api_keys.get("FINANCIAL_DATASETS_API_KEY")

        for ticker in self.tickers:
            get_prices(ticker, start_date_str, self.end_date, api_key=api_key)
            get_financial_metrics(ticker, self.end_date, limit=10, api_key=api_key)
            get_insider_trades(ticker, self.end_date, start_date=self.start_date, limit=1000, api_key=api_key)
            get_company_news(ticker, self.end_date, start_date=self.start_date, limit=1000, api_key=api_key)

    def _update_performance_metrics(self, performance_metrics: Dict[str, Any]):
        """Update performance metrics using daily returns."""
        values_df = pd.DataFrame(self.portfolio_values).set_index("Date")
        values_df["Daily Return"] = values_df["Portfolio Value"].pct_change()
        clean_returns = values_df["Daily Return"].dropna()

        if len(clean_returns) < 2:
            return

        daily_risk_free_rate = 0.0434 / 252
        excess_returns = clean_returns - daily_risk_free_rate
        mean_excess_return = excess_returns.mean()
        std_excess_return = excess_returns.std()

        # Sharpe ratio
        if std_excess_return > 1e-12:
            performance_metrics["sharpe_ratio"] = np.sqrt(252) * (mean_excess_return / std_excess_return)
        else:
            performance_metrics["sharpe_ratio"] = 0.0

        # Sortino ratio
        negative_returns = excess_returns[excess_returns < 0]
        if len(negative_returns) > 0:
            downside_std = negative_returns.std()
            if downside_std > 1e-12:
                performance_metrics["sortino_ratio"] = np.sqrt(252) * (mean_excess_return / downside_std)
            else:
                performance_metrics["sortino_ratio"] = None if mean_excess_return > 0 else 0
        else:
            performance_metrics["sortino_ratio"] = None if mean_excess_return > 0 else 0

        # Maximum drawdown
        rolling_max = values_df["Portfolio Value"].cummax()
        drawdown = (values_df["Portfolio Value"] - rolling_max) / rolling_max

        if len(drawdown) > 0:
            min_drawdown = drawdown.min()
            performance_metrics["max_drawdown"] = min_drawdown * 100

            if min_drawdown < 0:
                performance_metrics["max_drawdown_date"] = drawdown.idxmin().strftime("%Y-%m-%d")
            else:
                performance_metrics["max_drawdown_date"] = None
        else:
            performance_metrics["max_drawdown"] = 0.0
            performance_metrics["max_drawdown_date"] = None

    async def run_backtest_async(self, progress_callback: Optional[Callable] = None) -> Dict[str, Any]:
        """
        Run the backtest asynchronously with optional progress callbacks.
        Uses the pre-compiled graph for trading decisions.
        """
        # Pre-fetch all data at the start
        self.prefetch_data()

        dates = pd.date_range(self.start_date, self.end_date, freq="B")
        performance_metrics = {
            "sharpe_ratio": 0.0,
            "sortino_ratio": 0.0,
            "max_drawdown": 0.0,
            "long_short_ratio": 0.0,
            "gross_exposure": 0.0,
            "net_exposure": 0.0,
        }

        # Initialize portfolio values
        if len(dates) > 0:
            self.portfolio_values = [{"Date": dates[0], "Portfolio Value": self.initial_capital}]
        else:
            self.portfolio_values = []

        backtest_results = []

        for i, current_date in enumerate(dates):
            # Allow other async operations to run
            await asyncio.sleep(0)

            lookback_start = (current_date - timedelta(days=30)).strftime("%Y-%m-%d")
            current_date_str = current_date.strftime("%Y-%m-%d")
            previous_date_str = (current_date - timedelta(days=1)).strftime("%Y-%m-%d")

            if lookback_start == current_date_str:
                continue

            # Send progress update if callback provided
            if progress_callback:
                progress_callback({
                    "type": "progress",
                    "current_date": current_date_str,
                    "progress": (i + 1) / len(dates),
                    "total_dates": len(dates),
                    "current_step": i + 1,
                })

            # Get current prices
            try:
                current_prices = {}
                missing_data = False

                for ticker in self.tickers:
                    try:
                        price_data = get_price_data(ticker, previous_date_str, current_date_str)
                        if price_data.empty:
                            missing_data = True
                            break
                        current_prices[ticker] = price_data.iloc[-1]["close"]
                    except Exception as e:
                        missing_data = True
                        break

                if missing_data:
                    continue

            except Exception:
                continue

            # Create portfolio for this iteration
            portfolio_for_graph = create_portfolio(
                initial_cash=self.portfolio["cash"],
                margin_requirement=self.portfolio["margin_requirement"],
                tickers=self.tickers,
                portfolio_positions=[]  # We'll handle positions manually
            )
            
            # Copy current portfolio state to the graph portfolio
            portfolio_for_graph.update(self.portfolio)

            # Execute graph-based agent decisions
            try:
                result = await run_graph_async(
                    graph=self.graph,
                    portfolio=portfolio_for_graph,
                    tickers=self.tickers,
                    start_date=lookback_start,
                    end_date=current_date_str,
                    model_name=self.model_name,
                    model_provider=self.model_provider,
                    request=self.request,
                )
                
                # Parse the decisions from the graph result
                if result and result.get("messages"):
                    decisions = parse_hedge_fund_response(result["messages"][-1].content)
                    analyst_signals = result.get("data", {}).get("analyst_signals", {})
                else:
                    decisions = {}
                    analyst_signals = {}
                    
            except Exception as e:
                print(f"Error running graph for {current_date_str}: {e}")
                decisions = {}
                analyst_signals = {}

            # Execute trades based on decisions
            executed_trades = {}
            for ticker in self.tickers:
                decision = decisions.get(ticker, {"action": "hold", "quantity": 0})
                action, quantity = decision.get("action", "hold"), decision.get("quantity", 0)
                executed_quantity = self.execute_trade(ticker, action, quantity, current_prices[ticker])
                executed_trades[ticker] = executed_quantity

            # Calculate portfolio value
            total_value = self.calculate_portfolio_value(current_prices)

            # Calculate exposures
            long_exposure = sum(self.portfolio["positions"][t]["long"] * current_prices[t] for t in self.tickers)
            short_exposure = sum(self.portfolio["positions"][t]["short"] * current_prices[t] for t in self.tickers)
            gross_exposure = long_exposure + short_exposure
            net_exposure = long_exposure - short_exposure
            long_short_ratio = long_exposure / short_exposure if short_exposure > 1e-9 else None

            # Track portfolio value
            self.portfolio_values.append({
                "Date": current_date,
                "Portfolio Value": total_value,
                "Long Exposure": long_exposure,
                "Short Exposure": short_exposure,
                "Gross Exposure": gross_exposure,
                "Net Exposure": net_exposure,
                "Long/Short Ratio": long_short_ratio,
            })

            # Calculate performance metrics for this day
            portfolio_return = (total_value / self.initial_capital - 1) * 100
            
            # Update performance metrics if we have enough data
            if len(self.portfolio_values) > 2:
                self._update_performance_metrics(performance_metrics)

            # Build detailed result for this date (similar to CLI format)
            date_result = {
                "date": current_date_str,
                "portfolio_value": total_value,
                "cash": self.portfolio["cash"],
                "decisions": decisions,
                "executed_trades": executed_trades,
                "analyst_signals": analyst_signals,
                "current_prices": current_prices,
                "long_exposure": long_exposure,
                "short_exposure": short_exposure,
                "gross_exposure": gross_exposure,
                "net_exposure": net_exposure,
                "long_short_ratio": long_short_ratio,
                "portfolio_return": portfolio_return,
                "performance_metrics": performance_metrics.copy(),
                # Add detailed trading information for each ticker
                "ticker_details": []
            }

            # Build ticker details (similar to CLI format_backtest_row)
            for ticker in self.tickers:
                ticker_signals = {}
                for agent_name, signals in analyst_signals.items():
                    if ticker in signals:
                        ticker_signals[agent_name] = signals[ticker]

                bullish_count = len([s for s in ticker_signals.values() if s.get("signal", "").lower() == "bullish"])
                bearish_count = len([s for s in ticker_signals.values() if s.get("signal", "").lower() == "bearish"])
                neutral_count = len([s for s in ticker_signals.values() if s.get("signal", "").lower() == "neutral"])

                # Calculate net position value
                pos = self.portfolio["positions"][ticker]
                long_val = pos["long"] * current_prices[ticker]
                short_val = pos["short"] * current_prices[ticker]
                net_position_value = long_val - short_val

                # Get the action and quantity from the decisions
                action = decisions.get(ticker, {}).get("action", "hold")
                quantity = executed_trades.get(ticker, 0)

                ticker_detail = {
                    "ticker": ticker,
                    "action": action,
                    "quantity": quantity,
                    "price": current_prices[ticker],
                    "shares_owned": pos["long"] - pos["short"],  # net shares
                    "long_shares": pos["long"],
                    "short_shares": pos["short"],
                    "position_value": net_position_value,
                    "bullish_count": bullish_count,
                    "bearish_count": bearish_count,
                    "neutral_count": neutral_count,
                }
                
                date_result["ticker_details"].append(ticker_detail)

            backtest_results.append(date_result)

            # Send intermediate result if callback provided
            if progress_callback:
                progress_callback({
                    "type": "backtest_result",
                    "data": date_result,
                })

        # Ensure final performance metrics are calculated
        if len(self.portfolio_values) > 1:
            self._update_performance_metrics(performance_metrics)

        # Calculate final exposures if we have results
        if backtest_results:
            final_result = backtest_results[-1]
            performance_metrics["gross_exposure"] = final_result["gross_exposure"]
            performance_metrics["net_exposure"] = final_result["net_exposure"]
            performance_metrics["long_short_ratio"] = final_result["long_short_ratio"]

        # Store final performance metrics
        self.performance_metrics = performance_metrics

        return {
            "results": backtest_results,
            "performance_metrics": performance_metrics,
            "portfolio_values": self.portfolio_values,
            "final_portfolio": self.portfolio,
        }

    def run_backtest_sync(self) -> Dict[str, Any]:
        """
        Run the backtest synchronously.
        This version can be used by the CLI.
        """
        # Use asyncio to run the async version
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        try:
            return loop.run_until_complete(self.run_backtest_async())
        finally:
            loop.close()

    def analyze_performance(self) -> pd.DataFrame:
        """Analyze performance and return DataFrame with metrics."""
        if not self.portfolio_values:
            return pd.DataFrame()

        performance_df = pd.DataFrame(self.portfolio_values).set_index("Date")
        if performance_df.empty:
            return performance_df

        # Calculate additional metrics
        performance_df["Daily Return"] = performance_df["Portfolio Value"].pct_change().fillna(0)
        
        return performance_df 

================================================
FILE: app/backend/services/graph.py
================================================
import asyncio
import json
import re
from langchain_core.messages import HumanMessage
from langgraph.graph import END, StateGraph

from app.backend.services.agent_service import create_agent_function
from src.agents.portfolio_manager import portfolio_management_agent
from src.agents.risk_manager import risk_management_agent
from src.main import start
from src.utils.analysts import ANALYST_CONFIG
from src.graph.state import AgentState


def extract_base_agent_key(unique_id: str) -> str:
    """
    Extract the base agent key from a unique node ID.
    
    Args:
        unique_id: The unique node ID with suffix (e.g., "warren_buffett_abc123")
    
    Returns:
        The base agent key (e.g., "warren_buffett")
    """
    # For agent nodes, remove the last underscore and 6-character suffix
    parts = unique_id.split('_')
    if len(parts) >= 2:
        last_part = parts[-1]
        # If the last part is a 6-character alphanumeric string, it's likely our suffix
        if len(last_part) == 6 and re.match(r'^[a-z0-9]+$', last_part):
            return '_'.join(parts[:-1])
    return unique_id  # Return original if no suffix pattern found


# Helper function to create the agent graph
def create_graph(graph_nodes: list, graph_edges: list) -> StateGraph:
    """Create the workflow based on the React Flow graph structure."""
    graph = StateGraph(AgentState)
    graph.add_node("start_node", start)

    # Get analyst nodes from the configuration
    analyst_nodes = {key: (f"{key}_agent", config["agent_func"]) for key, config in ANALYST_CONFIG.items()}
    
    # Extract agent IDs from graph structure
    agent_ids = [node.id for node in graph_nodes]
    agent_ids_set = set(agent_ids)
    
    # Track which nodes are portfolio managers for special handling
    portfolio_manager_nodes = set()
    
    # Add agent nodes
    for unique_agent_id in agent_ids:
        base_agent_key = extract_base_agent_key(unique_agent_id)
        
        # Track portfolio manager nodes for special handling (before ANALYST_CONFIG check)
        if base_agent_key == "portfolio_manager":
            portfolio_manager_nodes.add(unique_agent_id)
            continue
            
        # Skip if the base agent key is not in our analyst configuration
        if base_agent_key not in ANALYST_CONFIG:
            continue
            
        node_name, node_func = analyst_nodes[base_agent_key]
        agent_function = create_agent_function(node_func, unique_agent_id)
        graph.add_node(unique_agent_id, agent_function)
    
    # Add portfolio manager nodes and their corresponding risk managers
    risk_manager_nodes = {}  # Map portfolio manager ID to risk manager ID
    for portfolio_manager_id in portfolio_manager_nodes:
        portfolio_manager_function = create_agent_function(portfolio_management_agent, portfolio_manager_id)
        graph.add_node(portfolio_manager_id, portfolio_manager_function)
        
        # Create unique risk manager for this portfolio manager
        suffix = portfolio_manager_id.split('_')[-1]
        risk_manager_id = f"risk_management_agent_{suffix}"
        risk_manager_nodes[portfolio_manager_id] = risk_manager_id
        
        # Add the risk manager node
        risk_manager_function = create_agent_function(risk_management_agent, risk_manager_id)
        graph.add_node(risk_manager_id, risk_manager_function)

    # Build connections based on React Flow graph structure
    nodes_with_incoming_edges = set()
    nodes_with_outgoing_edges = set()
    direct_to_portfolio_managers = {}  # Map analyst ID to portfolio manager ID for direct connections
    
    for edge in graph_edges:
        # Only consider edges between agent nodes (not from stock tickers)
        if edge.source in agent_ids_set and edge.target in agent_ids_set:
            source_base_key = extract_base_agent_key(edge.source)
            target_base_key = extract_base_agent_key(edge.target)
            
            nodes_with_incoming_edges.add(edge.target)
            nodes_with_outgoing_edges.add(edge.source)
            
            # Check if this is a direct connection from analyst to portfolio manager
            if (source_base_key in ANALYST_CONFIG and 
                source_base_key != "portfolio_manager" and 
                target_base_key == "portfolio_manager"):
                # Don't add direct edge to portfolio manager - we'll route through risk manager
                direct_to_portfolio_managers[edge.source] = edge.target
            else:
                # Add edge between agent nodes (but not direct to portfolio managers)
                graph.add_edge(edge.source, edge.target)
    
    # Connect start_node to nodes that don't have incoming edges from other agents
    for agent_id in agent_ids:
        if agent_id not in nodes_with_incoming_edges:
            base_agent_key = extract_base_agent_key(agent_id)
            if base_agent_key in ANALYST_CONFIG and base_agent_key != "portfolio_manager":
                graph.add_edge("start_node", agent_id)
    
    # Connect analysts that have direct connections to portfolio managers to their corresponding risk managers
    for analyst_id, portfolio_manager_id in direct_to_portfolio_managers.items():
        risk_manager_id = risk_manager_nodes[portfolio_manager_id]
        graph.add_edge(analyst_id, risk_manager_id)
    
    # Connect each risk manager to its corresponding portfolio manager
    for portfolio_manager_id, risk_manager_id in risk_manager_nodes.items():
        graph.add_edge(risk_manager_id, portfolio_manager_id)
    
    # Connect portfolio managers to END
    for portfolio_manager_id in portfolio_manager_nodes:
        graph.add_edge(portfolio_manager_id, END)

    # Set the entry point to the start node
    graph.set_entry_point("start_node")
    return graph


async def run_graph_async(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, request=None):
    """Async wrapper for run_graph to work with asyncio."""
    # Use run_in_executor to run the synchronous function in a separate thread
    # so it doesn't block the event loop
    loop = asyncio.get_running_loop()
    result = await loop.run_in_executor(None, lambda: run_graph(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, request))  # Use default executor
    return result


def run_graph(
    graph: StateGraph,
    portfolio: dict,
    tickers: list[str],
    start_date: str,
    end_date: str,
    model_name: str,
    model_provider: str,
    request=None,
) -> dict:
    """
    Run the graph with the given portfolio, tickers,
    start date, end date, show reasoning, model name,
    and model provider.
    """
    return graph.invoke(
        {
            "messages": [
                HumanMessage(
                    content="Make trading decisions based on the provided data.",
                )
            ],
            "data": {
                "tickers": tickers,
                "portfolio": portfolio,
                "start_date": start_date,
                "end_date": end_date,
                "analyst_signals": {},
            },
            "metadata": {
                "show_reasoning": False,
                "model_name": model_name,
                "model_provider": model_provider,
                "request": request,  # Pass the request for agent-specific model access
            },
        },
    )


def parse_hedge_fund_response(response):
    """Parses a JSON string and returns a dictionary."""
    try:
        return json.loads(response)
    except json.JSONDecodeError as e:
        print(f"JSON decoding error: {e}\nResponse: {repr(response)}")
        return None
    except TypeError as e:
        print(f"Invalid response type (expected string, got {type(response).__name__}): {e}")
        return None
    except Exception as e:
        print(f"Unexpected error while parsing response: {e}\nResponse: {repr(response)}")
        return None


================================================
FILE: app/backend/services/ollama_service.py
================================================
import asyncio
import os
import sys
import platform
import subprocess
import time
import re
import json
import queue
import threading
from pathlib import Path
from typing import Dict, List, Optional, AsyncGenerator
import logging
import signal
import ollama

logger = logging.getLogger(__name__)

class OllamaService:
    """Service for managing Ollama integration in the backend."""
    
    def __init__(self):
        self._download_progress = {}
        self._download_processes = {}
        
        # Initialize async client
        self._async_client = ollama.AsyncClient()
        self._sync_client = ollama.Client()
    
    # =============================================================================
    # PUBLIC API METHODS
    # =============================================================================
    
    async def check_ollama_status(self) -> Dict[str, any]:
        """Check Ollama installation and server status."""
        try:
            is_installed = await self._check_installation()
            is_running = await self._check_server_running()
            models, server_url = await self._get_server_info(is_running)
            
            status = {
                "installed": is_installed,
                "running": is_running,
                "server_running": is_running,  # Backward compatibility
                "available_models": models,
                "server_url": server_url,
                "error": None
            }
            
            logger.debug(f"Ollama status: installed={is_installed}, running={is_running}, models={len(models)}")
            return status
            
        except Exception as e:
            logger.error(f"Error checking Ollama status: {e}")
            return self._create_error_status(str(e))
    
    async def start_server(self) -> Dict[str, any]:
        """Start the Ollama server."""
        try:
            success = await self._execute_server_start()
            
            message = "Ollama server started successfully" if success else "Failed to start Ollama server"
            return {"success": success, "message": message}
                
        except Exception as e:
            logger.error(f"Error starting Ollama server: {e}")
            return {"success": False, "message": f"Error starting server: {str(e)}"}
    
    async def stop_server(self) -> Dict[str, any]:
        """Stop the Ollama server."""
        try:
            success = await self._execute_server_stop()
            
            message = "Ollama server stopped successfully" if success else "Failed to stop Ollama server"
            return {"success": success, "message": message}
                
        except Exception as e:
            logger.error(f"Error stopping Ollama server: {e}")
            return {"success": False, "message": f"Error stopping server: {str(e)}"}
    
    async def download_model(self, model_name: str) -> Dict[str, any]:
        """Download an Ollama model."""
        try:
            success = await self._execute_model_download(model_name)
            
            message = f"Model {model_name} downloaded successfully" if success else f"Failed to download model {model_name}"
            return {"success": success, "message": message}
                
        except Exception as e:
            logger.error(f"Error downloading model {model_name}: {e}")
            return {"success": False, "message": f"Error downloading model: {str(e)}"}
    
    async def download_model_with_progress(self, model_name: str) -> AsyncGenerator[str, None]:
        """Download an Ollama model with progress streaming."""
        async for progress_data in self._stream_model_download(model_name):
            yield progress_data
    
    async def delete_model(self, model_name: str) -> Dict[str, any]:
        """Delete an Ollama model."""
        try:
            success = await self._execute_model_deletion(model_name)
            
            message = f"Model {model_name} deleted successfully" if success else f"Failed to delete model {model_name}"
            return {"success": success, "message": message}
                
        except Exception as e:
            logger.error(f"Error deleting model {model_name}: {e}")
            return {"success": False, "message": f"Error deleting model: {str(e)}"}
    
    async def get_recommended_models(self) -> List[Dict[str, str]]:
        """Get list of recommended Ollama models."""
        try:
            models_path = self._get_ollama_models_path()
            
            if models_path.exists():
                return self._load_models_from_file(models_path)
            else:
                return self._get_fallback_models()
                
        except Exception as e:
            logger.error(f"Error loading recommended models: {e}")
            return []
    
    async def get_available_models(self) -> List[Dict[str, str]]:
        """Get available Ollama models formatted for the language models API.
        
        Returns only models that are:
        1. Server is running
        2. Model is downloaded locally  
        3. Model is in our recommended list (OLLAMA_MODELS)
        """
        try:
            status = await self.check_ollama_status()
            
            if not status.get("server_running", False):
                logger.debug("Ollama server not running, returning no models for API")
                return []
            
            downloaded_models = status.get("available_models", [])
            if not downloaded_models:
                logger.debug("No Ollama models downloaded, returning empty list for API")
                return []
            
            api_models = self._format_models_for_api(downloaded_models)
            logger.debug(f"Returning {len(api_models)} Ollama models for language models API")
            return api_models
            
        except Exception as e:
            logger.error(f"Error getting available models for API: {e}")
            return []  # Return empty list on error to not break the API
    
    def get_download_progress(self, model_name: str) -> Optional[Dict[str, any]]:
        """Get current download progress for a model."""
        return self._download_progress.get(model_name)
    
    def get_all_download_progress(self) -> Dict[str, Dict[str, any]]:
        """Get current download progress for all models."""
        return self._download_progress.copy()
    
    def cancel_download(self, model_name: str) -> bool:
        """Cancel an active download."""
        logger.warning(f"Download cancellation not directly supported by ollama client for model: {model_name}")
        
        if model_name in self._download_progress:
            self._download_progress[model_name] = {
                "status": "cancelled",
                "message": f"Download of {model_name} was cancelled",
                "error": "Download cancelled by user"
            }
            return True
        
        return False
    
    # =============================================================================
    # PRIVATE HELPER METHODS
    # =============================================================================
    

    def _create_error_status(self, error: str) -> Dict[str, any]:
        """Create error status response."""
        return {
            "installed": False,
            "running": False,
            "server_running": False,
            "available_models": [],
            "server_url": "",
            "error": error
        }
    
    async def _check_installation(self) -> bool:
        """Check if Ollama CLI is installed."""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, self._is_ollama_installed)
    
    def _is_ollama_installed(self) -> bool:
        """Check if Ollama is installed on the system."""
        system = platform.system().lower()
        command = ["which", "ollama"] if system in ["darwin", "linux"] else ["where", "ollama"]
        shell = system == "windows"
        
        try:
            result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=shell)
            return result.returncode == 0
        except Exception:
            return False
    
    async def _check_server_running(self) -> bool:
        """Check if the Ollama server is running using the ollama client."""
        try:
            await self._async_client.list()
            logger.debug("Ollama server confirmed running via client")
            return True
        except Exception as e:
            logger.debug(f"Ollama server not reachable: {e}")
            return False
    
    async def _get_server_info(self, is_running: bool) -> tuple[List[str], str]:
        """Get server information (models and URL) if server is running."""
        if not is_running:
            return [], ""
        
        try:
            response = await self._async_client.list()
            models = [model.model for model in response.models]
            server_url = getattr(self._async_client, 'host', 'http://localhost:11434')
            logger.debug(f"Found {len(models)} locally available models")
            return models, server_url
        except Exception as e:
            logger.debug(f"Failed to get server info: {e}")
            return [], ""
    
    async def _execute_server_start(self) -> bool:
        """Execute server start operation."""
        # Check if already running
        try:
            self._sync_client.list()
            logger.info("Ollama server is already running")
            return True
        except Exception:
            pass  # Server not running, continue to start it
        
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, self._start_ollama_process)
    
    def _start_ollama_process(self) -> bool:
        """Start the Ollama server process."""
        system = platform.system().lower()
        
        try:
            command = ["ollama", "serve"]
            shell = system == "windows"
            subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
            
            return self._wait_for_server_start()
            
        except Exception as e:
            logger.error(f"Error starting Ollama server: {e}")
            return False
    
    def _wait_for_server_start(self) -> bool:
        """Wait for server to start and become ready."""
        logger.info("Starting Ollama server, waiting for it to become ready...")
        
        for i in range(20):  # Try for 20 seconds
            time.sleep(1)
            try:
                self._sync_client.list()
                logger.info(f"Ollama server started successfully after {i+1} seconds")
                return True
            except Exception:
                logger.debug(f"Waiting for Ollama server... ({i+1}/20)")
                continue
        
        logger.error("Ollama server failed to start within 20 seconds")
        return False
    
    async def _execute_server_stop(self) -> bool:
        """Execute server stop operation."""
        # Check if already stopped
        try:
            self._sync_client.list()
        except Exception:
            logger.info("Ollama server is already stopped")
            return True
        
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, self._stop_ollama_process)
    
    def _stop_ollama_process(self) -> bool:
        """Stop the Ollama server process."""
        system = platform.system().lower()
        
        try:
            if system in ["darwin", "linux"]:
                return self._stop_unix_process()
            elif system == "windows":
                return self._stop_windows_process()
            else:
                return False
                
        except Exception as e:
            logger.error(f"Error stopping Ollama server: {e}")
            return False
    
    def _stop_unix_process(self) -> bool:
        """Stop Ollama on Unix-like systems."""
        try:
            result = subprocess.run(
                ["pgrep", "-f", "ollama serve"], 
                stdout=subprocess.PIPE, 
                stderr=subprocess.PIPE, 
                text=True
            )
            
            if result.returncode == 0:
                pids = [pid for pid in result.stdout.strip().split('\n') if pid]
                self._terminate_processes(pids)
            
            return self._verify_server_stopped()
            
        except Exception as e:
            logger.error(f"Error stopping Unix process: {e}")
            return False
    
    def _stop_windows_process(self) -> bool:
        """Stop Ollama on Windows."""
        try:
            subprocess.run(
                ["taskkill", "/F", "/IM", "ollama.exe"], 
                stdout=subprocess.PIPE, 
                stderr=subprocess.PIPE
            )
            return self._verify_server_stopped()
            
        except Exception as e:
            logger.error(f"Error stopping Windows process: {e}")
            return False
    
    def _terminate_processes(self, pids: List[str]) -> None:
        """Terminate processes gracefully, then forcefully if needed."""
        # Try SIGTERM first
        for pid in pids:
            if pid:
                try:
                    os.kill(int(pid), signal.SIGTERM)
                except (ValueError, ProcessLookupError, PermissionError):
                    continue
        
        # Wait for graceful termination
        for _ in range(5):
            try:
                self._sync_client.list()
                time.sleep(1)
            except Exception:
                return  # Server stopped
        
        # Force kill if still running
        for pid in pids:
            if pid:
                try:
                    os.kill(int(pid), signal.SIGKILL)
                except (ValueError, ProcessLookupError, PermissionError):
                    continue
    
    def _verify_server_stopped(self) -> bool:
        """Verify that the server has stopped."""
        for _ in range(3):
            try:
                self._sync_client.list()
                time.sleep(1)
            except Exception:
                return True
        return False
    
    async def _execute_model_download(self, model_name: str) -> bool:
        """Execute model download operation."""
        if not await self._check_server_running():
            logger.error(f"Cannot download model {model_name}: Ollama server is not running")
            return False
        
        try:
            logger.info(f"Starting download of model: {model_name}")
            await self._async_client.pull(model_name)
            logger.info(f"Successfully downloaded model: {model_name}")
            return True
        except Exception as e:
            logger.error(f"Error downloading model {model_name}: {e}")
            return False
    
    async def _execute_model_deletion(self, model_name: str) -> bool:
        """Execute model deletion operation."""
        if not await self._check_server_running():
            logger.error(f"Cannot delete model {model_name}: Ollama server is not running")
            return False
        
        try:
            logger.info(f"Deleting model: {model_name}")
            await self._async_client.delete(model_name)
            logger.info(f"Successfully deleted model: {model_name}")
            return True
        except Exception as e:
            logger.error(f"Error deleting model {model_name}: {e}")
            return False
    
    async def _stream_model_download(self, model_name: str) -> AsyncGenerator[str, None]:
        """Stream model download with progress updates."""
        try:
            if not await self._check_server_running():
                yield f"data: {json.dumps({'status': 'error', 'error': 'Ollama server is not running'})}\n\n"
                return
            
            logger.info(f"Starting download of model: {model_name}")
            self._download_progress[model_name] = {"status": "starting", "percentage": 0}
            
            yield f"data: {json.dumps({'status': 'starting', 'percentage': 0, 'message': f'Starting download of {model_name}...'})}\n\n"
            
            # Await the pull method to get the async iterator
            pull_stream = await self._async_client.pull(model_name, stream=True)
            async for progress in pull_stream:
                progress_data = self._process_download_progress(progress, model_name)
                if progress_data:
                    yield f"data: {json.dumps(progress_data)}\n\n"
                    
                    if progress_data.get("status") == "completed":
                        logger.info(f"Successfully downloaded model: {model_name}")
                        break
                        
        except Exception as e:
            error_data = {
                "status": "error",
                "message": f"Error downloading model {model_name}",
                "error": str(e)
            }
            self._download_progress[model_name] = error_data
            yield f"data: {json.dumps(error_data)}\n\n"
            logger.error(f"Error downloading model {model_name}: {e}")
        finally:
            await asyncio.sleep(1)
            if model_name in self._download_progress:
                del self._download_progress[model_name]
    
    def _process_download_progress(self, progress, model_name: str) -> Optional[Dict[str, any]]:
        """Process download progress from ollama client."""
        if not hasattr(progress, 'status'):
            return None
        
        progress_data = {
            "status": "downloading",
            "message": progress.status,
            "raw_output": progress.status
        }
        
        # Add completed/total info if available
        if (hasattr(progress, 'completed') and hasattr(progress, 'total') and 
            progress.total is not None and progress.completed is not None and progress.total > 0):
            percentage = (progress.completed / progress.total) * 100
            progress_data.update({
                "percentage": percentage,
                "bytes_downloaded": progress.completed,
                "total_bytes": progress.total
            })
        
        # Add digest info if available
        if hasattr(progress, 'digest'):
            progress_data["digest"] = progress.digest
        
        # Store in cache
        self._download_progress[model_name] = progress_data
        
        # Check if download is complete
        if (progress.status == "success" or 
            (hasattr(progress, 'completed') and hasattr(progress, 'total') and 
             progress.completed is not None and progress.total is not None and
             progress.completed == progress.total)):
            final_data = {
                "status": "completed",
                "percentage": 100,
                "message": f"Model {model_name} downloaded successfully!"
            }
            self._download_progress[model_name] = final_data
            return final_data
        
        return progress_data
    
    def _get_ollama_models_path(self) -> Path:
        """Get path to ollama_models.json file."""
        return Path(__file__).parent.parent.parent.parent / "src" / "llm" / "ollama_models.json"
    
    def _load_models_from_file(self, models_path: Path) -> List[Dict[str, str]]:
        """Load models from JSON file."""
        with open(models_path, 'r') as f:
            return json.load(f)
    
    def _get_fallback_models(self) -> List[Dict[str, str]]:
        """Get fallback models when file is not available."""
        return [
            {"display_name": "[meta] llama3.1 (8B)", "model_name": "llama3.1:latest", "provider": "Ollama"},
            {"display_name": "[google] gemma3 (4B)", "model_name": "gemma3:4b", "provider": "Ollama"},
            {"display_name": "[alibaba] qwen3 (4B)", "model_name": "qwen3:4b", "provider": "Ollama"},
        ]
    
    def _format_models_for_api(self, downloaded_models: List[str]) -> List[Dict[str, str]]:
        """Format downloaded models for API response."""
        # Import OLLAMA_MODELS here to avoid circular imports
        from src.llm.models import OLLAMA_MODELS
        
        api_models = []
        for ollama_model in OLLAMA_MODELS:
            if ollama_model.model_name in downloaded_models:
                api_models.append({
                    "display_name": ollama_model.display_name,
                    "model_name": ollama_model.model_name,
                    "provider": "Ollama"
                })
        
        return api_models

# Global service instance
ollama_service = OllamaService() 

================================================
FILE: app/backend/services/portfolio.py
================================================

from typing import Optional, List
from app.backend.models.schemas import PortfolioPosition


def create_portfolio(initial_cash: float, margin_requirement: float, tickers: list[str], portfolio_positions: Optional[List[PortfolioPosition]] = None) -> dict:
    # Initialize base portfolio structure
    portfolio = {
        "cash": initial_cash,  # Initial cash amount
        "margin_requirement": margin_requirement,  # Initial margin requirement
        "margin_used": 0.0,  # total margin usage across all short positions
        "positions": {
            ticker: {
                "long": 0,  # Number of shares held long
                "short": 0,  # Number of shares held short
                "long_cost_basis": 0.0,  # Average cost basis for long positions
                "short_cost_basis": 0.0,  # Average price at which shares were sold short
                "short_margin_used": 0.0,  # Dollars of margin used for this ticker's short
            }
            for ticker in tickers
        },
        "realized_gains": {
            ticker: {
                "long": 0.0,  # Realized gains from long positions
                "short": 0.0,  # Realized gains from short positions
            }
            for ticker in tickers
        },
    }
    
    # If portfolio positions are provided, populate them
    if portfolio_positions:
        for position in portfolio_positions:
            ticker = position.ticker
            quantity = position.quantity
            trade_price = position.trade_price
            
            # Ensure ticker exists in portfolio (it should from tickers list)
            if ticker in portfolio["positions"]:
                if quantity > 0:
                    # Positive quantity means long position
                    portfolio["positions"][ticker]["long"] = quantity
                    portfolio["positions"][ticker]["long_cost_basis"] = trade_price
                elif quantity < 0:
                    # Negative quantity means short position
                    portfolio["positions"][ticker]["short"] = abs(quantity)
                    portfolio["positions"][ticker]["short_cost_basis"] = trade_price
                    # Calculate margin used for short position
                    portfolio["positions"][ticker]["short_margin_used"] = abs(quantity) * trade_price * margin_requirement
                    portfolio["margin_used"] += portfolio["positions"][ticker]["short_margin_used"]
    
    return portfolio

================================================
FILE: app/frontend/.eslintrc.cjs
================================================
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
  },
}


================================================
FILE: app/frontend/.github/dependabot.yml
================================================
# docs:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: 'npm'
    directory: '/'
    schedule:
      interval: 'daily'
    allow:
      - dependency-name: '@xyflow/react'


================================================
FILE: app/frontend/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: app/frontend/LICENSE
================================================
MIT License

Copyright (c) 2023 webkid GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: app/frontend/README.md
================================================
# AI Hedge Fund - Frontend [WIP] 🚧
This project is currently a work in progress.  To track progress, please get updates [here](https://x.com/virattt).

This is the frontend application for the AI Hedge Fund project. It provides a web interface to interact with the AI Hedge Fund system, allowing you to visualize and control the hedge fund operations.

## Overview

This frontend project is built with React and Vite, serving as the client-side component of the AI Hedge Fund system. It connects to the backend API to provide a user-friendly interface for managing the hedge fund trading system and backtester.

## Installation

The project contains the minimum dependencies to get up and running, and includes eslint with additional rules to help write clean React code:

```bash
npm install # or `pnpm install` or `yarn install`
```

## Running the Application

Start the application with:

```bash
npm run dev
```

While the application is running, changes made to the code will be automatically reflected in the browser!

## Disclaimer

This project is for **educational and research purposes only**.

- Not intended for real trading or investment
- No warranties or guarantees provided
- Creator assumes no liability for financial losses
- Consult a financial advisor for investment decisions

By using this software, you agree to use it solely for learning purposes.

================================================
FILE: app/frontend/components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/index.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

================================================
FILE: app/frontend/index.html
================================================
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI Hedge Fund</title>
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

</html>

================================================
FILE: app/frontend/package.json
================================================
{
  "name": "vite-react-flow-template",
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@radix-ui/react-accordion": "^1.2.10",
    "@radix-ui/react-checkbox": "^1.3.2",
    "@radix-ui/react-dialog": "^1.1.13",
    "@radix-ui/react-icons": "^1.3.2",
    "@radix-ui/react-popover": "^1.1.13",
    "@radix-ui/react-separator": "^1.1.6",
    "@radix-ui/react-slot": "^1.2.0",
    "@radix-ui/react-tabs": "^1.1.11",
    "@radix-ui/react-tooltip": "^1.2.6",
    "@types/react-syntax-highlighter": "^15.5.13",
    "@xyflow/react": "^12.5.1",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "lucide-react": "^0.507.0",
    "next-themes": "^0.4.6",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-resizable-panels": "^3.0.1",
    "react-syntax-highlighter": "^15.6.1",
    "shadcn-ui": "^0.9.5",
    "sonner": "^2.0.5",
    "tailwind-merge": "^3.2.0"
  },
  "license": "MIT",
  "devDependencies": {
    "@tailwindcss/typography": "^0.5.16",
    "@types/node": "^22.15.3",
    "@types/react": "^18.2.53",
    "@types/react-dom": "^18.2.18",
    "@typescript-eslint/eslint-plugin": "^6.20.0",
    "@typescript-eslint/parser": "^6.20.0",
    "@vitejs/plugin-react": "^4.2.1",
    "autoprefixer": "^10.4.21",
    "eslint": "^8.56.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "postcss": "^8.5.3",
    "tailwindcss": "^3.4.1",
    "tailwindcss-animate": "^1.0.7",
    "typescript": "^5.3.3",
    "vite": "^5.0.12"
  }
}


================================================
FILE: app/frontend/postcss.config.mjs
================================================
/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

export default config;


================================================
FILE: app/frontend/src/App.tsx
================================================
import { Layout } from './components/layout';
import { Toaster } from './components/ui/sonner';

export default function App() {
  return (
    <>
      <Layout />
      <Toaster />
    </>
  );
}


================================================
FILE: app/frontend/src/components/Flow.tsx
================================================
import {
  Background,
  BackgroundVariant,
  ColorMode,
  Connection,
  Edge,
  EdgeChange,
  MarkerType,
  NodeChange,
  ReactFlow,
  addEdge,
  useEdgesState,
  useNodesState
} from '@xyflow/react';
import { useTheme } from 'next-themes';
import { useCallback, useEffect, useRef, useState } from 'react';

import '@xyflow/react/dist/style.css';

import { useFlowContext } from '@/contexts/flow-context';
import { useEnhancedFlowActions } from '@/hooks/use-enhanced-flow-actions';
import { useFlowHistory } from '@/hooks/use-flow-history';
import { useFlowKeyboardShortcuts, useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts';
import { useToastManager } from '@/hooks/use-toast-manager';
import { AppNode } from '@/nodes/types';
import { edgeTypes } from '../edges';
import { nodeTypes } from '../nodes';
import { TooltipProvider } from './ui/tooltip';

type FlowProps = {
  className?: string;
};

export function Flow({ className = '' }: FlowProps) {
  const { theme, resolvedTheme } = useTheme();
  
  // Use the resolved theme for ReactFlow ColorMode
  const colorMode: ColorMode = resolvedTheme === 'light' ? 'light' : 'dark';
  
  const [nodes, setNodes, onNodesChange] = useNodesState<AppNode>([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
  const [isInitialized, setIsInitialized] = useState(false);
  const proOptions = { hideAttribution: true };
  
  // Get flow context for flow ID
  const { currentFlowId } = useFlowContext();
  
  // Get enhanced flow actions for complete state persistence
  const { saveCurrentFlowWithCompleteState } = useEnhancedFlowActions();
  
  // Get toast manager
  const { success, error } = useToastManager();

  // Initialize flow history (each flow maintains its own separate history)
  const { takeSnapshot, undo, redo, canUndo, canRedo, clearHistory } = useFlowHistory({ flowId: currentFlowId });

  // Create debounced auto-save function
  const autoSaveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const lastSavedFlowIdRef = useRef<number | null>(null);
  
  const autoSave = useCallback(async (flowIdToSave?: number | null) => {
    // Use the provided flowId or fall back to current flow ID
    const targetFlowId = flowIdToSave !== undefined ? flowIdToSave : currentFlowId;
    
    // Clear any existing timeout
    if (autoSaveTimeoutRef.current) {
      clearTimeout(autoSaveTimeoutRef.current);
    }
    
    // Set new timeout for debounced save
    autoSaveTimeoutRef.current = setTimeout(async () => {
      // Double-check that we're still saving to the correct flow
      if (!targetFlowId) {
        return;
      }
      
      // If the current flow has changed since this auto-save was scheduled, skip it
      if (targetFlowId !== currentFlowId) {
        return;
      }
      
      try {
        await saveCurrentFlowWithCompleteState();
        lastSavedFlowIdRef.current = targetFlowId;
      } catch (error) {
        console.error(`[Auto-save] Failed to save flow ${targetFlowId}:`, error);
      }
    }, 1000); // 1 second debounce
  }, [currentFlowId, saveCurrentFlowWithCompleteState]);

  // Enhanced onNodesChange handler with auto-save for specific change types
  const handleNodesChange = useCallback((changes: NodeChange<AppNode>[]) => {
    // Apply the changes first
    onNodesChange(changes);
    
    // Check if any of the changes should trigger auto-save
    const shouldAutoSave = changes.some(change => {
      switch (change.type) {
        case 'add':
          return true;
        case 'remove':
          return true;
        case 'position':
          // Only auto-save position changes when dragging is complete
          if (!change.dragging) {
            return true;
          }
          return false;
        default:
          return false;
      }
    });

    // Trigger auto-save if needed and flow is initialized
    // IMPORTANT: Capture the current flow ID at the time of the change
    if (shouldAutoSave && isInitialized && currentFlowId) {
      const flowIdAtTimeOfChange = currentFlowId;
      autoSave(flowIdAtTimeOfChange);
    }
  }, [onNodesChange, autoSave, isInitialized, currentFlowId]);

  // Enhanced onEdgesChange handler with auto-save for edge removal
  const handleEdgesChange = useCallback((changes: EdgeChange[]) => {
    // Apply the changes first
    onEdgesChange(changes);
    
    // Check if any of the changes should trigger auto-save
    const shouldAutoSave = changes.some(change => {
      switch (change.type) {
        case 'remove':
          return true;
        default:
          return false;
      }
    });

    // Trigger auto-save if needed and flow is initialized
    // IMPORTANT: Capture the current flow ID at the time of the change
    if (shouldAutoSave && isInitialized && currentFlowId) {
      const flowIdAtTimeOfChange = currentFlowId;
      autoSave(flowIdAtTimeOfChange);
    }
  }, [onEdgesChange, autoSave, isInitialized, currentFlowId]);

  // Cleanup timeout on unmount
  useEffect(() => {
    return () => {
      if (autoSaveTimeoutRef.current) {
        clearTimeout(autoSaveTimeoutRef.current);
      }
    };
  }, []);

  // Cancel pending auto-saves when flow changes to prevent cross-flow saves
  useEffect(() => {
    if (autoSaveTimeoutRef.current) {
      clearTimeout(autoSaveTimeoutRef.current);
      autoSaveTimeoutRef.current = null;
    }
  }, [currentFlowId]);

  // Take initial snapshot when flow is initialized
  useEffect(() => {
    if (isInitialized && nodes.length === 0 && edges.length === 0) {
      takeSnapshot();
    }
  }, [isInitialized, takeSnapshot, nodes.length, edges.length]);

  // Take snapshot when nodes or edges change (debounced)
  useEffect(() => {
    if (!isInitialized) return;
    
    const timeoutId = setTimeout(() => {
      takeSnapshot();
    }, 500); // Debounce snapshots by 500ms

    return () => clearTimeout(timeoutId);
  }, [nodes, edges, takeSnapshot, isInitialized]);

  // // Auto-save when nodes or edges change (debounced with longer delay)
  // useEffect(() => {
  //   if (!isInitialized) return;
    
  //   const timeoutId = setTimeout(async () => {
  //     try {
  //       await saveCurrentFlowWithCompleteState();
  //       // Don't show success toast for auto-save to avoid spam
  //     } catch (err) {
  //       // Only show error notifications for auto-save failures
  //       error('Auto-save failed', 'auto-save-error');
  //     }
  //   }, 1000); // Debounce auto-save by 1 second (longer than undo/redo)

  //   return () => clearTimeout(timeoutId);
  // }, [nodes, edges, saveCurrentFlowWithCompleteState, error, isInitialized]);

  // Connect keyboard shortcuts to save flow with toast
  useFlowKeyboardShortcuts(async () => {
    try {
      const savedFlow = await saveCurrentFlowWithCompleteState();
      if (savedFlow) {
        success(`"${savedFlow.name}" saved!`, 'flow-save');
      } else {
        error('Failed to save flow', 'flow-save-error');
      }
    } catch (err) {
      error('Failed to save flow', 'flow-save-error');
    }
  });

  // Add undo/redo keyboard shortcuts
  useKeyboardShortcuts({
    shortcuts: [
      {
        key: 'z',
        ctrlKey: true,
        metaKey: true,
        callback: undo,
        preventDefault: true,
      },
      {
        key: 'z',
        ctrlKey: true,
        metaKey: true,
        shiftKey: true,
        callback: redo,
        preventDefault: true,
      },
    ],
  });
  
  // Initialize the flow when it first renders
  const onInit = useCallback(() => {
    if (!isInitialized) {
      setIsInitialized(true);
    }
  }, [isInitialized]);

  // Connect two nodes with marker
  const onConnect = useCallback(
    (connection: Connection) => {
      // Create a new edge with a marker and unique ID
      const newEdge: Edge = {
        ...connection,
        id: `edge-${Date.now()}`, // Add unique ID
        markerEnd: {
          type: MarkerType.ArrowClosed,
        },
      };
      setEdges((eds) => addEdge(newEdge, eds));
      
      // Auto-save new connections immediately (structural change)
      if (currentFlowId) {
        // IMPORTANT: Capture the current flow ID at the time of the change
        const flowIdAtTimeOfChange = currentFlowId;
        
        // Clear any pending debounced saves and save immediately
        if (autoSaveTimeoutRef.current) {
          clearTimeout(autoSaveTimeoutRef.current);
        }
        
        // Use setTimeout to ensure the edge is added to state first
        setTimeout(async () => {
          // Double-check that we're still saving to the correct flow
   
Download .txt
gitextract_0t_9v5yp/

├── .dockerignore
├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── README.md
├── app/
│   ├── README.md
│   ├── backend/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── alembic/
│   │   │   ├── README
│   │   │   ├── env.py
│   │   │   ├── script.py.mako
│   │   │   └── versions/
│   │   │       ├── 1b1feba3d897_add_data_column_to_hedge_fund_flows.py
│   │   │       ├── 2f8c5d9e4b1a_add_hedgefundflowrun_table.py
│   │   │       ├── 3f9a6b7c8d2e_add_hedgefundflowruncycle_table.py
│   │   │       ├── 5274886e5bee_add_hedgefundflow_table.py
│   │   │       └── add_api_keys_table.py
│   │   ├── alembic.ini
│   │   ├── database/
│   │   │   ├── __init__.py
│   │   │   ├── connection.py
│   │   │   └── models.py
│   │   ├── main.py
│   │   ├── models/
│   │   │   ├── __init__.py
│   │   │   ├── events.py
│   │   │   └── schemas.py
│   │   ├── repositories/
│   │   │   ├── __init__.py
│   │   │   ├── api_key_repository.py
│   │   │   ├── flow_repository.py
│   │   │   └── flow_run_repository.py
│   │   ├── routes/
│   │   │   ├── __init__.py
│   │   │   ├── api_keys.py
│   │   │   ├── flow_runs.py
│   │   │   ├── flows.py
│   │   │   ├── health.py
│   │   │   ├── hedge_fund.py
│   │   │   ├── language_models.py
│   │   │   ├── ollama.py
│   │   │   └── storage.py
│   │   └── services/
│   │       ├── __init__.py
│   │       ├── agent_service.py
│   │       ├── api_key_service.py
│   │       ├── backtest_service.py
│   │       ├── graph.py
│   │       ├── ollama_service.py
│   │       └── portfolio.py
│   ├── frontend/
│   │   ├── .eslintrc.cjs
│   │   ├── .github/
│   │   │   └── dependabot.yml
│   │   ├── .gitignore
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── components.json
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   ├── components/
│   │   │   │   ├── Flow.tsx
│   │   │   │   ├── Layout.tsx
│   │   │   │   ├── custom-controls.tsx
│   │   │   │   ├── layout/
│   │   │   │   │   └── top-bar.tsx
│   │   │   │   ├── panels/
│   │   │   │   │   ├── bottom/
│   │   │   │   │   │   ├── bottom-panel.tsx
│   │   │   │   │   │   └── tabs/
│   │   │   │   │   │       ├── backtest-output.tsx
│   │   │   │   │   │       ├── debug-console-tab.tsx
│   │   │   │   │   │       ├── index.ts
│   │   │   │   │   │       ├── output-tab-utils.ts
│   │   │   │   │   │       ├── output-tab.tsx
│   │   │   │   │   │       ├── problems-tab.tsx
│   │   │   │   │   │       ├── reasoning-content.tsx
│   │   │   │   │   │       ├── regular-output.tsx
│   │   │   │   │   │       └── terminal-tab.tsx
│   │   │   │   │   ├── left/
│   │   │   │   │   │   ├── flow-actions.tsx
│   │   │   │   │   │   ├── flow-context-menu.tsx
│   │   │   │   │   │   ├── flow-create-dialog.tsx
│   │   │   │   │   │   ├── flow-edit-dialog.tsx
│   │   │   │   │   │   ├── flow-item-group.tsx
│   │   │   │   │   │   ├── flow-item.tsx
│   │   │   │   │   │   ├── flow-list.tsx
│   │   │   │   │   │   └── left-sidebar.tsx
│   │   │   │   │   ├── right/
│   │   │   │   │   │   ├── component-actions.tsx
│   │   │   │   │   │   ├── component-item-group.tsx
│   │   │   │   │   │   ├── component-item.tsx
│   │   │   │   │   │   ├── component-list.tsx
│   │   │   │   │   │   └── right-sidebar.tsx
│   │   │   │   │   └── search-box.tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── api-keys.tsx
│   │   │   │   │   ├── appearance.tsx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── models/
│   │   │   │   │   │   ├── cloud.tsx
│   │   │   │   │   │   └── ollama.tsx
│   │   │   │   │   ├── models.tsx
│   │   │   │   │   └── settings.tsx
│   │   │   │   ├── tabs/
│   │   │   │   │   ├── flow-tab-content.tsx
│   │   │   │   │   ├── tab-bar.tsx
│   │   │   │   │   └── tab-content.tsx
│   │   │   │   └── ui/
│   │   │   │       ├── accordion.tsx
│   │   │   │       ├── badge.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── checkbox.tsx
│   │   │   │       ├── command.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── llm-selector.tsx
│   │   │   │       ├── popover.tsx
│   │   │   │       ├── resizable.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       ├── sheet.tsx
│   │   │   │       ├── sidebar.tsx
│   │   │   │       ├── skeleton.tsx
│   │   │   │       ├── sonner.tsx
│   │   │   │       ├── table.tsx
│   │   │   │       ├── tabs.tsx
│   │   │   │       └── tooltip.tsx
│   │   │   ├── contexts/
│   │   │   │   ├── flow-context.tsx
│   │   │   │   ├── layout-context.tsx
│   │   │   │   ├── node-context.tsx
│   │   │   │   └── tabs-context.tsx
│   │   │   ├── data/
│   │   │   │   ├── agents.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── multi-node-mappings.ts
│   │   │   │   ├── node-mappings.ts
│   │   │   │   └── sidebar-components.ts
│   │   │   ├── edges/
│   │   │   │   └── index.ts
│   │   │   ├── hooks/
│   │   │   │   ├── use-component-groups.ts
│   │   │   │   ├── use-enhanced-flow-actions.ts
│   │   │   │   ├── use-flow-connection.ts
│   │   │   │   ├── use-flow-history.ts
│   │   │   │   ├── use-flow-management-tabs.ts
│   │   │   │   ├── use-flow-management.ts
│   │   │   │   ├── use-keyboard-shortcuts.ts
│   │   │   │   ├── use-mobile.tsx
│   │   │   │   ├── use-node-state.ts
│   │   │   │   ├── use-output-node-connection.ts
│   │   │   │   ├── use-resizable.ts
│   │   │   │   └── use-toast-manager.ts
│   │   │   ├── index.css
│   │   │   ├── lib/
│   │   │   │   └── utils.ts
│   │   │   ├── main.tsx
│   │   │   ├── nodes/
│   │   │   │   ├── components/
│   │   │   │   │   ├── agent-node.tsx
│   │   │   │   │   ├── agent-output-dialog.tsx
│   │   │   │   │   ├── investment-report-dialog.tsx
│   │   │   │   │   ├── investment-report-node.tsx
│   │   │   │   │   ├── json-output-dialog.tsx
│   │   │   │   │   ├── json-output-node.tsx
│   │   │   │   │   ├── node-shell.tsx
│   │   │   │   │   ├── output-node-status.tsx
│   │   │   │   │   ├── portfolio-manager-node.tsx
│   │   │   │   │   ├── portfolio-start-node.tsx
│   │   │   │   │   └── stock-analyzer-node.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── utils.ts
│   │   │   ├── providers/
│   │   │   │   └── theme-provider.tsx
│   │   │   ├── services/
│   │   │   │   ├── api-keys-api.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── backtest-api.ts
│   │   │   │   ├── flow-service.ts
│   │   │   │   ├── sidebar-storage.ts
│   │   │   │   ├── tab-service.ts
│   │   │   │   └── types.ts
│   │   │   ├── types/
│   │   │   │   └── flow.ts
│   │   │   ├── utils/
│   │   │   │   ├── date-utils.ts
│   │   │   │   └── text-utils.ts
│   │   │   └── vite-env.d.ts
│   │   ├── tailwind.config.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── run.bat
│   └── run.sh
├── docker/
│   ├── .dockerignore
│   ├── Dockerfile
│   ├── README.md
│   ├── docker-compose.yml
│   ├── run.bat
│   └── run.sh
├── pyproject.toml
├── src/
│   ├── __init__.py
│   ├── agents/
│   │   ├── __init__.py
│   │   ├── aswath_damodaran.py
│   │   ├── ben_graham.py
│   │   ├── bill_ackman.py
│   │   ├── cathie_wood.py
│   │   ├── charlie_munger.py
│   │   ├── fundamentals.py
│   │   ├── growth_agent.py
│   │   ├── michael_burry.py
│   │   ├── mohnish_pabrai.py
│   │   ├── news_sentiment.py
│   │   ├── peter_lynch.py
│   │   ├── phil_fisher.py
│   │   ├── portfolio_manager.py
│   │   ├── rakesh_jhunjhunwala.py
│   │   ├── risk_manager.py
│   │   ├── sentiment.py
│   │   ├── stanley_druckenmiller.py
│   │   ├── technicals.py
│   │   ├── valuation.py
│   │   └── warren_buffett.py
│   ├── backtester.py
│   ├── backtesting/
│   │   ├── __init__.py
│   │   ├── benchmarks.py
│   │   ├── cli.py
│   │   ├── controller.py
│   │   ├── engine.py
│   │   ├── metrics.py
│   │   ├── output.py
│   │   ├── portfolio.py
│   │   ├── trader.py
│   │   ├── types.py
│   │   └── valuation.py
│   ├── cli/
│   │   ├── __init__.py
│   │   └── input.py
│   ├── data/
│   │   ├── __init__.py
│   │   ├── cache.py
│   │   └── models.py
│   ├── graph/
│   │   ├── __init__.py
│   │   └── state.py
│   ├── llm/
│   │   ├── __init__.py
│   │   ├── api_models.json
│   │   ├── models.py
│   │   └── ollama_models.json
│   ├── main.py
│   ├── tools/
│   │   ├── __init__.py
│   │   └── api.py
│   └── utils/
│       ├── __init__.py
│       ├── analysts.py
│       ├── api_key.py
│       ├── display.py
│       ├── docker.py
│       ├── llm.py
│       ├── ollama.py
│       ├── progress.py
│       └── visualize.py
└── tests/
    ├── __init__.py
    ├── backtesting/
    │   ├── conftest.py
    │   ├── integration/
    │   │   ├── conftest.py
    │   │   ├── mocks.py
    │   │   ├── test_integration_long_only.py
    │   │   ├── test_integration_long_short.py
    │   │   └── test_integration_short_only.py
    │   ├── test_controller.py
    │   ├── test_execution.py
    │   ├── test_metrics.py
    │   ├── test_portfolio.py
    │   ├── test_results.py
    │   └── test_valuation.py
    ├── fixtures/
    │   └── api/
    │       ├── financial_metrics/
    │       │   ├── AAPL_2024-03-01_2024-03-08.json
    │       │   ├── MSFT_2024-03-01_2024-03-08.json
    │       │   └── TSLA_2024-03-01_2024-03-08.json
    │       ├── insider_trades/
    │       │   ├── AAPL_2024-03-01_2024-03-08.json
    │       │   ├── MSFT_2024-03-01_2024-03-08.json
    │       │   └── TSLA_2024-03-01_2024-03-08.json
    │       ├── news/
    │       │   ├── AAPL_2024-03-01_2024-03-08.json
    │       │   ├── MSFT_2024-03-01_2024-03-08.json
    │       │   └── TSLA_2024-03-01_2024-03-08.json
    │       └── prices/
    │           ├── AAPL_2024-03-01_2024-03-08.json
    │           ├── MSFT_2024-03-01_2024-03-08.json
    │           └── TSLA_2024-03-01_2024-03-08.json
    └── test_api_rate_limiting.py
Download .txt
SYMBOL INDEX (854 symbols across 177 files)

FILE: app/backend/alembic/env.py
  function run_migrations_offline (line 28) | def run_migrations_offline() -> None:
  function run_migrations_online (line 52) | def run_migrations_online() -> None:

FILE: app/backend/alembic/versions/1b1feba3d897_add_data_column_to_hedge_fund_flows.py
  function upgrade (line 21) | def upgrade() -> None:
  function downgrade (line 28) | def downgrade() -> None:

FILE: app/backend/alembic/versions/2f8c5d9e4b1a_add_hedgefundflowrun_table.py
  function upgrade (line 21) | def upgrade() -> None:
  function downgrade (line 43) | def downgrade() -> None:

FILE: app/backend/alembic/versions/3f9a6b7c8d2e_add_hedgefundflowruncycle_table.py
  function upgrade (line 18) | def upgrade():
  function downgrade (line 70) | def downgrade():

FILE: app/backend/alembic/versions/5274886e5bee_add_hedgefundflow_table.py
  function upgrade (line 21) | def upgrade() -> None:
  function downgrade (line 41) | def downgrade() -> None:

FILE: app/backend/alembic/versions/add_api_keys_table.py
  function upgrade (line 21) | def upgrade() -> None:
  function downgrade (line 40) | def downgrade() -> None:

FILE: app/backend/database/connection.py
  function get_db (line 27) | def get_db():

FILE: app/backend/database/models.py
  class HedgeFundFlow (line 6) | class HedgeFundFlow(Base):
  class HedgeFundFlowRun (line 29) | class HedgeFundFlowRun(Base):
  class HedgeFundFlowRunCycle (line 59) | class HedgeFundFlowRunCycle(Base):
  class ApiKey (line 97) | class ApiKey(Base):

FILE: app/backend/main.py
  function startup_event (line 33) | async def startup_event():

FILE: app/backend/models/events.py
  class BaseEvent (line 5) | class BaseEvent(BaseModel):
    method to_sse (line 10) | def to_sse(self) -> str:
  class StartEvent (line 16) | class StartEvent(BaseEvent):
  class ProgressUpdateEvent (line 22) | class ProgressUpdateEvent(BaseEvent):
  class ErrorEvent (line 32) | class ErrorEvent(BaseEvent):
  class CompleteEvent (line 40) | class CompleteEvent(BaseEvent):

FILE: app/backend/models/schemas.py
  class FlowRunStatus (line 9) | class FlowRunStatus(str, Enum):
  class AgentModelConfig (line 16) | class AgentModelConfig(BaseModel):
  class PortfolioPosition (line 22) | class PortfolioPosition(BaseModel):
    method price_must_be_positive (line 29) | def price_must_be_positive(cls, v: float) -> float:
  class GraphNode (line 35) | class GraphNode(BaseModel):
  class GraphEdge (line 42) | class GraphEdge(BaseModel):
  class HedgeFundResponse (line 50) | class HedgeFundResponse(BaseModel):
  class ErrorResponse (line 55) | class ErrorResponse(BaseModel):
  class BaseHedgeFundRequest (line 61) | class BaseHedgeFundRequest(BaseModel):
    method get_agent_ids (line 72) | def get_agent_ids(self) -> List[str]:
    method get_agent_model_config (line 76) | def get_agent_model_config(self, agent_id: str) -> tuple[str, ModelPro...
  class BacktestRequest (line 94) | class BacktestRequest(BaseHedgeFundRequest):
  class BacktestDayResult (line 100) | class BacktestDayResult(BaseModel):
  class BacktestPerformanceMetrics (line 115) | class BacktestPerformanceMetrics(BaseModel):
  class BacktestResponse (line 125) | class BacktestResponse(BaseModel):
  class HedgeFundRequest (line 131) | class HedgeFundRequest(BaseHedgeFundRequest):
    method get_start_date (line 136) | def get_start_date(self) -> str:
  class FlowCreateRequest (line 144) | class FlowCreateRequest(BaseModel):
  class FlowUpdateRequest (line 155) | class FlowUpdateRequest(BaseModel):
  class FlowResponse (line 166) | class FlowResponse(BaseModel):
    class Config (line 179) | class Config:
  class FlowSummaryResponse (line 183) | class FlowSummaryResponse(BaseModel):
    class Config (line 193) | class Config:
  class FlowRunCreateRequest (line 198) | class FlowRunCreateRequest(BaseModel):
  class FlowRunUpdateRequest (line 203) | class FlowRunUpdateRequest(BaseModel):
  class FlowRunResponse (line 210) | class FlowRunResponse(BaseModel):
    class Config (line 224) | class Config:
  class FlowRunSummaryResponse (line 228) | class FlowRunSummaryResponse(BaseModel):
    class Config (line 239) | class Config:
  class ApiKeyCreateRequest (line 244) | class ApiKeyCreateRequest(BaseModel):
  class ApiKeyUpdateRequest (line 252) | class ApiKeyUpdateRequest(BaseModel):
  class ApiKeyResponse (line 259) | class ApiKeyResponse(BaseModel):
    class Config (line 270) | class Config:
  class ApiKeySummaryResponse (line 274) | class ApiKeySummaryResponse(BaseModel):
    class Config (line 285) | class Config:
  class ApiKeyBulkUpdateRequest (line 289) | class ApiKeyBulkUpdateRequest(BaseModel):

FILE: app/backend/repositories/api_key_repository.py
  class ApiKeyRepository (line 9) | class ApiKeyRepository:
    method __init__ (line 12) | def __init__(self, db: Session):
    method create_or_update_api_key (line 15) | def create_or_update_api_key(
    method get_api_key_by_provider (line 48) | def get_api_key_by_provider(self, provider: str) -> Optional[ApiKey]:
    method get_all_api_keys (line 55) | def get_all_api_keys(self, include_inactive: bool = False) -> List[Api...
    method update_api_key (line 62) | def update_api_key(
    method delete_api_key (line 86) | def delete_api_key(self, provider: str) -> bool:
    method deactivate_api_key (line 96) | def deactivate_api_key(self, provider: str) -> bool:
    method update_last_used (line 107) | def update_last_used(self, provider: str) -> bool:
    method bulk_create_or_update (line 120) | def bulk_create_or_update(self, api_keys_data: List[dict]) -> List[Api...

FILE: app/backend/repositories/flow_repository.py
  class FlowRepository (line 6) | class FlowRepository:
    method __init__ (line 9) | def __init__(self, db: Session):
    method create_flow (line 12) | def create_flow(self, name: str, nodes: dict, edges: dict, description...
    method get_flow_by_id (line 30) | def get_flow_by_id(self, flow_id: int) -> Optional[HedgeFundFlow]:
    method get_all_flows (line 34) | def get_all_flows(self, include_templates: bool = True) -> List[HedgeF...
    method get_flows_by_name (line 41) | def get_flows_by_name(self, name: str) -> List[HedgeFundFlow]:
    method update_flow (line 47) | def update_flow(self, flow_id: int, name: str = None, description: str...
    method delete_flow (line 76) | def delete_flow(self, flow_id: int) -> bool:
    method duplicate_flow (line 86) | def duplicate_flow(self, flow_id: int, new_name: str = None) -> Option...

FILE: app/backend/repositories/flow_run_repository.py
  class FlowRunRepository (line 9) | class FlowRunRepository:
    method __init__ (line 12) | def __init__(self, db: Session):
    method create_flow_run (line 15) | def create_flow_run(self, flow_id: int, request_data: Dict[str, Any] =...
    method get_flow_run_by_id (line 31) | def get_flow_run_by_id(self, run_id: int) -> Optional[HedgeFundFlowRun]:
    method get_flow_runs_by_flow_id (line 35) | def get_flow_runs_by_flow_id(self, flow_id: int, limit: int = 50, offs...
    method get_active_flow_run (line 46) | def get_active_flow_run(self, flow_id: int) -> Optional[HedgeFundFlowR...
    method get_latest_flow_run (line 57) | def get_latest_flow_run(self, flow_id: int) -> Optional[HedgeFundFlowR...
    method update_flow_run (line 66) | def update_flow_run(
    method delete_flow_run (line 98) | def delete_flow_run(self, run_id: int) -> bool:
    method delete_flow_runs_by_flow_id (line 108) | def delete_flow_runs_by_flow_id(self, flow_id: int) -> int:
    method get_flow_run_count (line 118) | def get_flow_run_count(self, flow_id: int) -> int:
    method _get_next_run_number (line 126) | def _get_next_run_number(self, flow_id: int) -> int:

FILE: app/backend/routes/api_keys.py
  function create_or_update_api_key (line 27) | async def create_or_update_api_key(request: ApiKeyCreateRequest, db: Ses...
  function get_api_keys (line 49) | async def get_api_keys(include_inactive: bool = False, db: Session = Dep...
  function get_api_key (line 67) | async def get_api_key(provider: str, db: Session = Depends(get_db)):
  function update_api_key (line 89) | async def update_api_key(provider: str, request: ApiKeyUpdateRequest, db...
  function delete_api_key (line 116) | async def delete_api_key(provider: str, db: Session = Depends(get_db)):
  function deactivate_api_key (line 138) | async def deactivate_api_key(provider: str, db: Session = Depends(get_db)):
  function bulk_update_api_keys (line 163) | async def bulk_update_api_keys(request: ApiKeyBulkUpdateRequest, db: Ses...
  function update_last_used (line 190) | async def update_last_used(provider: str, db: Session = Depends(get_db)):

FILE: app/backend/routes/flow_runs.py
  function create_flow_run (line 28) | async def create_flow_run(
  function get_flow_runs (line 62) | async def get_flow_runs(
  function get_active_flow_run (line 94) | async def get_active_flow_run(flow_id: int, db: Session = Depends(get_db)):
  function get_latest_flow_run (line 121) | async def get_latest_flow_run(flow_id: int, db: Session = Depends(get_db)):
  function get_flow_run (line 148) | async def get_flow_run(flow_id: int, run_id: int, db: Session = Depends(...
  function update_flow_run (line 178) | async def update_flow_run(
  function delete_flow_run (line 224) | async def delete_flow_run(flow_id: int, run_id: int, db: Session = Depen...
  function delete_all_flow_runs (line 258) | async def delete_all_flow_runs(flow_id: int, db: Session = Depends(get_d...
  function get_flow_run_count (line 286) | async def get_flow_run_count(flow_id: int, db: Session = Depends(get_db)):

FILE: app/backend/routes/flows.py
  function create_flow (line 26) | async def create_flow(request: FlowCreateRequest, db: Session = Depends(...
  function get_flows (line 52) | async def get_flows(include_templates: bool = True, db: Session = Depend...
  function get_flow (line 70) | async def get_flow(flow_id: int, db: Session = Depends(get_db)):
  function update_flow (line 92) | async def update_flow(flow_id: int, request: FlowUpdateRequest, db: Sess...
  function delete_flow (line 124) | async def delete_flow(flow_id: int, db: Session = Depends(get_db)):
  function duplicate_flow (line 146) | async def duplicate_flow(flow_id: int, new_name: str = None, db: Session...
  function search_flows (line 167) | async def search_flows(name: str, db: Session = Depends(get_db)):

FILE: app/backend/routes/health.py
  function root (line 10) | async def root():
  function ping (line 15) | async def ping():

FILE: app/backend/routes/hedge_fund.py
  function run (line 26) | async def run(request_data: HedgeFundRequest, request: Request, db: Sess...
  function backtest (line 170) | async def backtest(request_data: BacktestRequest, request: Request, db: ...
  function get_agents (line 346) | async def get_agents():

FILE: app/backend/routes/language_models.py
  function get_language_models (line 20) | async def get_language_models():
  function get_language_model_providers (line 41) | async def get_language_model_providers():

FILE: app/backend/routes/ollama.py
  class ModelRequest (line 14) | class ModelRequest(BaseModel):
  class OllamaStatusResponse (line 17) | class OllamaStatusResponse(BaseModel):
  class ActionResponse (line 24) | class ActionResponse(BaseModel):
  class RecommendedModel (line 28) | class RecommendedModel(BaseModel):
  class ProgressResponse (line 33) | class ProgressResponse(BaseModel):
  function get_ollama_status (line 48) | async def get_ollama_status():
  function start_ollama_server (line 65) | async def start_ollama_server():
  function stop_ollama_server (line 97) | async def stop_ollama_server():
  function download_model (line 129) | async def download_model(request: ModelRequest):
  function download_model_with_progress (line 165) | async def download_model_with_progress(request: ModelRequest):
  function get_download_progress (line 205) | async def get_download_progress(model_name: str):
  function get_active_downloads (line 226) | async def get_active_downloads():
  function delete_model (line 250) | async def delete_model(model_name: str):
  function get_recommended_models (line 286) | async def get_recommended_models():
  function cancel_download (line 303) | async def cancel_download(model_name: str):

FILE: app/backend/routes/storage.py
  class SaveJsonRequest (line 10) | class SaveJsonRequest(BaseModel):
  function save_json_file (line 22) | async def save_json_file(request: SaveJsonRequest):

FILE: app/backend/services/agent_service.py
  function create_agent_function (line 5) | def create_agent_function(agent_function: Callable, agent_id: str) -> Ca...

FILE: app/backend/services/api_key_service.py
  class ApiKeyService (line 6) | class ApiKeyService:
    method __init__ (line 9) | def __init__(self, db: Session):
    method get_api_keys_dict (line 12) | def get_api_keys_dict(self) -> Dict[str, str]:
    method get_api_key (line 20) | def get_api_key(self, provider: str) -> Optional[str]:

FILE: app/backend/services/backtest_service.py
  class BacktestService (line 18) | class BacktestService:
    method __init__ (line 24) | def __init__(
    method execute_trade (line 60) | def execute_trade(self, ticker: str, action: str, quantity: float, cur...
    method calculate_portfolio_value (line 207) | def calculate_portfolio_value(self, current_prices: Dict[str, float]) ...
    method prefetch_data (line 225) | def prefetch_data(self):
    method _update_performance_metrics (line 238) | def _update_performance_metrics(self, performance_metrics: Dict[str, A...
    method run_backtest_async (line 285) | async def run_backtest_async(self, progress_callback: Optional[Callabl...
    method run_backtest_sync (line 514) | def run_backtest_sync(self) -> Dict[str, Any]:
    method analyze_performance (line 527) | def analyze_performance(self) -> pd.DataFrame:

FILE: app/backend/services/graph.py
  function extract_base_agent_key (line 15) | def extract_base_agent_key(unique_id: str) -> str:
  function create_graph (line 36) | def create_graph(graph_nodes: list, graph_edges: list) -> StateGraph:
  function run_graph_async (line 132) | async def run_graph_async(graph, portfolio, tickers, start_date, end_dat...
  function run_graph (line 141) | def run_graph(
  function parse_hedge_fund_response (line 180) | def parse_hedge_fund_response(response):

FILE: app/backend/services/ollama_service.py
  class OllamaService (line 19) | class OllamaService:
    method __init__ (line 22) | def __init__(self):
    method check_ollama_status (line 34) | async def check_ollama_status(self) -> Dict[str, any]:
    method start_server (line 57) | async def start_server(self) -> Dict[str, any]:
    method stop_server (line 69) | async def stop_server(self) -> Dict[str, any]:
    method download_model (line 81) | async def download_model(self, model_name: str) -> Dict[str, any]:
    method download_model_with_progress (line 93) | async def download_model_with_progress(self, model_name: str) -> Async...
    method delete_model (line 98) | async def delete_model(self, model_name: str) -> Dict[str, any]:
    method get_recommended_models (line 110) | async def get_recommended_models(self) -> List[Dict[str, str]]:
    method get_available_models (line 124) | async def get_available_models(self) -> List[Dict[str, str]]:
    method get_download_progress (line 152) | def get_download_progress(self, model_name: str) -> Optional[Dict[str,...
    method get_all_download_progress (line 156) | def get_all_download_progress(self) -> Dict[str, Dict[str, any]]:
    method cancel_download (line 160) | def cancel_download(self, model_name: str) -> bool:
    method _create_error_status (line 179) | def _create_error_status(self, error: str) -> Dict[str, any]:
    method _check_installation (line 190) | async def _check_installation(self) -> bool:
    method _is_ollama_installed (line 195) | def _is_ollama_installed(self) -> bool:
    method _check_server_running (line 207) | async def _check_server_running(self) -> bool:
    method _get_server_info (line 217) | async def _get_server_info(self, is_running: bool) -> tuple[List[str],...
    method _execute_server_start (line 232) | async def _execute_server_start(self) -> bool:
    method _start_ollama_process (line 245) | def _start_ollama_process(self) -> bool:
    method _wait_for_server_start (line 260) | def _wait_for_server_start(self) -> bool:
    method _execute_server_stop (line 277) | async def _execute_server_stop(self) -> bool:
    method _stop_ollama_process (line 289) | def _stop_ollama_process(self) -> bool:
    method _stop_unix_process (line 305) | def _stop_unix_process(self) -> bool:
    method _stop_windows_process (line 325) | def _stop_windows_process(self) -> bool:
    method _terminate_processes (line 339) | def _terminate_processes(self, pids: List[str]) -> None:
    method _verify_server_stopped (line 365) | def _verify_server_stopped(self) -> bool:
    method _execute_model_download (line 375) | async def _execute_model_download(self, model_name: str) -> bool:
    method _execute_model_deletion (line 390) | async def _execute_model_deletion(self, model_name: str) -> bool:
    method _stream_model_download (line 405) | async def _stream_model_download(self, model_name: str) -> AsyncGenera...
    method _process_download_progress (line 442) | def _process_download_progress(self, progress, model_name: str) -> Opt...
    method _get_ollama_models_path (line 485) | def _get_ollama_models_path(self) -> Path:
    method _load_models_from_file (line 489) | def _load_models_from_file(self, models_path: Path) -> List[Dict[str, ...
    method _get_fallback_models (line 494) | def _get_fallback_models(self) -> List[Dict[str, str]]:
    method _format_models_for_api (line 502) | def _format_models_for_api(self, downloaded_models: List[str]) -> List...

FILE: app/backend/services/portfolio.py
  function create_portfolio (line 6) | def create_portfolio(initial_cash: float, margin_requirement: float, tic...

FILE: app/frontend/src/App.tsx
  function App (line 4) | function App() {

FILE: app/frontend/src/components/Flow.tsx
  type FlowProps (line 30) | type FlowProps = {
  function Flow (line 34) | function Flow({ className = '' }: FlowProps) {

FILE: app/frontend/src/components/Layout.tsx
  function LayoutContent (line 19) | function LayoutContent({ children }: { children: ReactNode }) {
  type LayoutProps (line 183) | interface LayoutProps {
  function Layout (line 187) | function Layout({ children }: LayoutProps) {

FILE: app/frontend/src/components/custom-controls.tsx
  type CustomControlsProps (line 4) | type CustomControlsProps = {
  function CustomControls (line 8) | function CustomControls({ onReset }: CustomControlsProps) {

FILE: app/frontend/src/components/layout/top-bar.tsx
  type TopBarProps (line 5) | interface TopBarProps {
  function TopBar (line 15) | function TopBar({

FILE: app/frontend/src/components/panels/bottom/bottom-panel.tsx
  type BottomPanelProps (line 10) | interface BottomPanelProps {
  function BottomPanel (line 19) | function BottomPanel({

FILE: app/frontend/src/components/panels/bottom/tabs/backtest-output.tsx
  function BacktestProgress (line 8) | function BacktestProgress({ agentData }: { agentData: Record<string, any...
  function BacktestTradingTable (line 37) | function BacktestTradingTable({ agentData }: { agentData: Record<string,...
  function BacktestResults (line 154) | function BacktestResults({ outputData }: { outputData: any }) {
  function BacktestPerformanceMetrics (line 301) | function BacktestPerformanceMetrics({ agentData }: { agentData: Record<s...
  function BacktestOutput (line 398) | function BacktestOutput({

FILE: app/frontend/src/components/panels/bottom/tabs/debug-console-tab.tsx
  type DebugConsoleTabProps (line 1) | interface DebugConsoleTabProps {
  function DebugConsoleTab (line 5) | function DebugConsoleTab({ className }: DebugConsoleTabProps) {

FILE: app/frontend/src/components/panels/bottom/tabs/output-tab-utils.ts
  function isJsonString (line 4) | function isJsonString(str: string): boolean {
  function getDisplayName (line 14) | function getDisplayName(agentName: string): string {
  function getStatusIcon (line 33) | function getStatusIcon(status: string) {
  function getSignalColor (line 47) | function getSignalColor(signal: string): string {
  function getActionColor (line 61) | function getActionColor(action: string): string {
  function sortAgents (line 77) | function sortAgents(agents: [string, any][]): [string, any][] {

FILE: app/frontend/src/components/panels/bottom/tabs/output-tab.tsx
  type OutputTabProps (line 9) | interface OutputTabProps {
  function OutputTab (line 13) | function OutputTab({ className }: OutputTabProps) {

FILE: app/frontend/src/components/panels/bottom/tabs/problems-tab.tsx
  type ProblemsTabProps (line 1) | interface ProblemsTabProps {
  function ProblemsTab (line 5) | function ProblemsTab({ className }: ProblemsTabProps) {

FILE: app/frontend/src/components/panels/bottom/tabs/reasoning-content.tsx
  function ReasoningContent (line 6) | function ReasoningContent({ content }: { content: any }) {

FILE: app/frontend/src/components/panels/bottom/tabs/regular-output.tsx
  function ProgressSection (line 10) | function ProgressSection({ sortedAgents }: { sortedAgents: [string, any]...
  function SummarySection (line 49) | function SummarySection({ outputData }: { outputData: any }) {
  function AnalysisResultsSection (line 88) | function AnalysisResultsSection({ outputData }: { outputData: any }) {
  function RegularOutput (line 216) | function RegularOutput({

FILE: app/frontend/src/components/panels/bottom/tabs/terminal-tab.tsx
  type TerminalTabProps (line 1) | interface TerminalTabProps {
  function TerminalTab (line 5) | function TerminalTab({ className }: TerminalTabProps) {

FILE: app/frontend/src/components/panels/left/flow-actions.tsx
  type FlowActionsProps (line 6) | interface FlowActionsProps {
  function FlowActions (line 11) | function FlowActions({ onSave, onCreate }: FlowActionsProps) {

FILE: app/frontend/src/components/panels/left/flow-context-menu.tsx
  type FlowContextMenuProps (line 6) | interface FlowContextMenuProps {
  function FlowContextMenu (line 15) | function FlowContextMenu({

FILE: app/frontend/src/components/panels/left/flow-create-dialog.tsx
  type FlowCreateDialogProps (line 16) | interface FlowCreateDialogProps {
  function FlowCreateDialog (line 22) | function FlowCreateDialog({ isOpen, onClose, onFlowCreated }: FlowCreate...

FILE: app/frontend/src/components/panels/left/flow-edit-dialog.tsx
  type FlowEditDialogProps (line 17) | interface FlowEditDialogProps {
  function FlowEditDialog (line 24) | function FlowEditDialog({ flow, isOpen, onClose, onFlowUpdated }: FlowEd...

FILE: app/frontend/src/components/panels/left/flow-item-group.tsx
  type FlowItemGroupProps (line 6) | interface FlowItemGroupProps {
  function FlowItemGroup (line 15) | function FlowItemGroup({ title, flows, onLoadFlow, onDeleteFlow, onRefre...

FILE: app/frontend/src/components/panels/left/flow-item.tsx
  type FlowItemProps (line 18) | interface FlowItemProps {
  function FlowItem (line 26) | function FlowItem({ flow, onLoadFlow, onDeleteFlow, onRefresh, isActive ...

FILE: app/frontend/src/components/panels/left/flow-list.tsx
  type FlowListProps (line 8) | interface FlowListProps {
  function FlowList (line 23) | function FlowList({

FILE: app/frontend/src/components/panels/left/left-sidebar.tsx
  type LeftSidebarProps (line 9) | interface LeftSidebarProps {
  function LeftSidebar (line 17) | function LeftSidebar({

FILE: app/frontend/src/components/panels/right/component-actions.tsx
  type ComponentActionsProps (line 2) | interface ComponentActionsProps {
  function ComponentActions (line 5) | function ComponentActions({ }: ComponentActionsProps) {

FILE: app/frontend/src/components/panels/right/component-item-group.tsx
  type ComponentItemGroupProps (line 6) | interface ComponentItemGroupProps {
  function ComponentItemGroup (line 11) | function ComponentItemGroup({

FILE: app/frontend/src/components/panels/right/component-item.tsx
  type ComponentItemProps (line 6) | interface ComponentItemProps {
  function ComponentItem (line 14) | function ComponentItem({

FILE: app/frontend/src/components/panels/right/component-list.tsx
  type ComponentListProps (line 6) | interface ComponentListProps {
  function ComponentList (line 17) | function ComponentList({

FILE: app/frontend/src/components/panels/right/right-sidebar.tsx
  type RightSidebarProps (line 9) | interface RightSidebarProps {
  function RightSidebar (line 17) | function RightSidebar({

FILE: app/frontend/src/components/panels/search-box.tsx
  type SearchBoxProps (line 4) | interface SearchBoxProps {
  function SearchBox (line 10) | function SearchBox({

FILE: app/frontend/src/components/settings/api-keys.tsx
  type ApiKey (line 8) | interface ApiKey {
  constant FINANCIAL_API_KEYS (line 16) | const FINANCIAL_API_KEYS: ApiKey[] = [
  constant LLM_API_KEYS (line 26) | const LLM_API_KEYS: ApiKey[] = [
  function ApiKeysSettings (line 78) | function ApiKeysSettings() {

FILE: app/frontend/src/components/settings/appearance.tsx
  function ThemeSettings (line 7) | function ThemeSettings() {

FILE: app/frontend/src/components/settings/models.tsx
  type ModelsProps (line 7) | interface ModelsProps {
  type ModelSection (line 11) | interface ModelSection {
  function Models (line 19) | function Models({ className }: ModelsProps) {

FILE: app/frontend/src/components/settings/models/cloud.tsx
  type CloudModelsProps (line 6) | interface CloudModelsProps {
  type CloudModel (line 10) | interface CloudModel {
  type ModelProvider (line 16) | interface ModelProvider {
  function CloudModels (line 24) | function CloudModels({ className }: CloudModelsProps) {

FILE: app/frontend/src/components/settings/models/ollama.tsx
  type OllamaStatus (line 8) | interface OllamaStatus {
  type RecommendedModel (line 16) | interface RecommendedModel {
  type ModelWithStatus (line 22) | interface ModelWithStatus extends RecommendedModel {
  type DownloadProgress (line 26) | interface DownloadProgress {
  function OllamaSettings (line 36) | function OllamaSettings() {

FILE: app/frontend/src/components/settings/settings.tsx
  type SettingsProps (line 8) | interface SettingsProps {
  type SettingsNavItem (line 12) | interface SettingsNavItem {
  function Settings (line 19) | function Settings({ className }: SettingsProps) {

FILE: app/frontend/src/components/tabs/flow-tab-content.tsx
  type FlowTabContentProps (line 12) | interface FlowTabContentProps {
  function FlowTabContent (line 17) | function FlowTabContent({ flow, className }: FlowTabContentProps) {

FILE: app/frontend/src/components/tabs/tab-bar.tsx
  type TabBarProps (line 7) | interface TabBarProps {
  function TabBar (line 23) | function TabBar({ className }: TabBarProps) {

FILE: app/frontend/src/components/tabs/tab-content.tsx
  type TabContentProps (line 7) | interface TabContentProps {
  function TabContent (line 11) | function TabContent({ className }: TabContentProps) {

FILE: app/frontend/src/components/ui/badge.tsx
  type BadgeProps (line 25) | interface BadgeProps
  function Badge (line 29) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: app/frontend/src/components/ui/button.tsx
  type ButtonProps (line 37) | interface ButtonProps

FILE: app/frontend/src/components/ui/llm-selector.tsx
  type ModelSelectorProps (line 22) | interface ModelSelectorProps {
  function ModelSelector (line 29) | function ModelSelector({

FILE: app/frontend/src/components/ui/sheet.tsx
  type SheetContentProps (line 52) | interface SheetContentProps

FILE: app/frontend/src/components/ui/sidebar.tsx
  constant SIDEBAR_COOKIE_NAME (line 26) | const SIDEBAR_COOKIE_NAME = "sidebar_state"
  constant SIDEBAR_COOKIE_MAX_AGE (line 27) | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
  constant SIDEBAR_WIDTH (line 28) | const SIDEBAR_WIDTH = "16rem"
  constant SIDEBAR_WIDTH_MOBILE (line 29) | const SIDEBAR_WIDTH_MOBILE = "18rem"
  constant SIDEBAR_WIDTH_ICON (line 30) | const SIDEBAR_WIDTH_ICON = "3rem"
  constant SIDEBAR_KEYBOARD_SHORTCUT (line 31) | const SIDEBAR_KEYBOARD_SHORTCUT = "b"
  type SidebarContextProps (line 33) | type SidebarContextProps = {
  function useSidebar (line 45) | function useSidebar() {

FILE: app/frontend/src/components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({

FILE: app/frontend/src/components/ui/sonner.tsx
  type ToasterProps (line 4) | type ToasterProps = React.ComponentProps<typeof Sonner>

FILE: app/frontend/src/contexts/flow-context.tsx
  type FlowContextType (line 10) | interface FlowContextType {
  function useFlowContext (line 23) | function useFlowContext() {
  type FlowProviderProps (line 31) | interface FlowProviderProps {
  function FlowProvider (line 35) | function FlowProvider({ children }: FlowProviderProps) {

FILE: app/frontend/src/contexts/layout-context.tsx
  type LayoutContextType (line 4) | interface LayoutContextType {
  function useLayoutContext (line 15) | function useLayoutContext() {
  type LayoutProviderProps (line 23) | interface LayoutProviderProps {
  function LayoutProvider (line 27) | function LayoutProvider({ children }: LayoutProviderProps) {

FILE: app/frontend/src/contexts/node-context.tsx
  type NodeStatus (line 4) | type NodeStatus = 'IDLE' | 'IN_PROGRESS' | 'COMPLETE' | 'ERROR';
  type MessageItem (line 7) | interface MessageItem {
  type AgentNodeData (line 15) | interface AgentNodeData {
  type OutputNodeData (line 27) | interface OutputNodeData {
  constant DEFAULT_AGENT_NODE_STATE (line 49) | const DEFAULT_AGENT_NODE_STATE: AgentNodeData = {
  function createCompositeKey (line 59) | function createCompositeKey(flowId: string | null, nodeId: string): stri...
  type NodeContextType (line 63) | interface NodeContextType {
  function NodeProvider (line 90) | function NodeProvider({ children }: { children: ReactNode }) {
  function useNodeContext (line 430) | function useNodeContext() {

FILE: app/frontend/src/contexts/tabs-context.tsx
  type TabType (line 5) | type TabType = 'flow' | 'settings';
  type Tab (line 7) | interface Tab {
  type SerializableTab (line 19) | interface SerializableTab {
  type TabsContextType (line 27) | interface TabsContextType {
  function useTabsContext (line 43) | function useTabsContext() {
  type TabsProviderProps (line 51) | interface TabsProviderProps {
  constant TABS_STORAGE_KEY (line 56) | const TABS_STORAGE_KEY = 'ai-hedge-fund-tabs';
  constant ACTIVE_TAB_STORAGE_KEY (line 57) | const ACTIVE_TAB_STORAGE_KEY = 'ai-hedge-fund-active-tab';
  function TabsProvider (line 59) | function TabsProvider({ children }: TabsProviderProps) {

FILE: app/frontend/src/data/agents.ts
  type Agent (line 3) | interface Agent {

FILE: app/frontend/src/data/models.ts
  type LanguageModel (line 3) | interface LanguageModel {

FILE: app/frontend/src/data/multi-node-mappings.ts
  type MultiNodeDefinition (line 1) | interface MultiNodeDefinition {
  function getMultiNodeDefinition (line 75) | function getMultiNodeDefinition(name: string): MultiNodeDefinition | null {
  function isMultiNodeComponent (line 79) | function isMultiNodeComponent(componentName: string): boolean {

FILE: app/frontend/src/data/node-mappings.ts
  type NodeTypeDefinition (line 5) | interface NodeTypeDefinition {
  function getNodeTypeDefinition (line 118) | async function getNodeTypeDefinition(componentName: string): Promise<Nod...
  function getNodeIdForComponent (line 124) | async function getNodeIdForComponent(componentName: string): Promise<str...

FILE: app/frontend/src/data/sidebar-components.ts
  type ComponentItem (line 16) | interface ComponentItem {
  type ComponentGroup (line 21) | interface ComponentGroup {

FILE: app/frontend/src/hooks/use-component-groups.ts
  function useComponentGroups (line 4) | function useComponentGroups(componentGroups: ComponentGroup[]) {

FILE: app/frontend/src/hooks/use-enhanced-flow-actions.ts
  function useEnhancedFlowActions (line 16) | function useEnhancedFlowActions() {

FILE: app/frontend/src/hooks/use-flow-connection.ts
  type FlowConnectionState (line 8) | type FlowConnectionState = 'idle' | 'connecting' | 'connected' | 'error'...
  type FlowConnectionInfo (line 10) | interface FlowConnectionInfo {
  class FlowConnectionManager (line 19) | class FlowConnectionManager {
    method getConnection (line 24) | getConnection(flowId: string): FlowConnectionInfo {
    method setConnection (line 34) | setConnection(flowId: string, info: Partial<FlowConnectionInfo>): void {
    method removeConnection (line 47) | removeConnection(flowId: string): void {
    method addListener (line 57) | addListener(listener: () => void): void {
    method removeListener (line 62) | removeListener(listener: () => void): void {
    method notifyListeners (line 67) | private notifyListeners(): void {
  function useFlowConnection (line 80) | function useFlowConnection(flowId: string | null) {
  function useFlowConnectionState (line 253) | function useFlowConnectionState(flowId: string | null) {

FILE: app/frontend/src/hooks/use-flow-history.ts
  type FlowSnapshot (line 4) | interface FlowSnapshot {
  type UseFlowHistoryOptions (line 10) | interface UseFlowHistoryOptions {
  function useFlowHistory (line 15) | function useFlowHistory({ maxHistorySize = 50, flowId }: UseFlowHistoryO...

FILE: app/frontend/src/hooks/use-flow-management-tabs.ts
  type UseFlowManagementTabsReturn (line 15) | interface UseFlowManagementTabsReturn {
  function useFlowManagementTabs (line 45) | function useFlowManagementTabs(): UseFlowManagementTabsReturn {

FILE: app/frontend/src/hooks/use-flow-management.ts
  type UseFlowManagementReturn (line 14) | interface UseFlowManagementReturn {
  function useFlowManagement (line 44) | function useFlowManagement(): UseFlowManagementReturn {

FILE: app/frontend/src/hooks/use-keyboard-shortcuts.ts
  type KeyboardShortcut (line 3) | interface KeyboardShortcut {
  type UseKeyboardShortcutsProps (line 13) | interface UseKeyboardShortcutsProps {
  function useKeyboardShortcuts (line 17) | function useKeyboardShortcuts({ shortcuts }: UseKeyboardShortcutsProps) {
  function useFlowKeyboardShortcuts (line 53) | function useFlowKeyboardShortcuts(saveFlow: (showToast?: boolean) => voi...
  function useLayoutKeyboardShortcuts (line 68) | function useLayoutKeyboardShortcuts(

FILE: app/frontend/src/hooks/use-mobile.tsx
  constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768
  function useIsMobile (line 5) | function useIsMobile() {

FILE: app/frontend/src/hooks/use-node-state.ts
  class FlowStateManager (line 7) | class FlowStateManager {
    method setCurrentFlowId (line 14) | setCurrentFlowId(flowId: string | null): void {
    method getCurrentFlowId (line 23) | getCurrentFlowId(): string | null {
    method createCompositeKey (line 28) | private createCompositeKey(nodeId: string): string {
    method getNodeState (line 33) | getNodeState(nodeId: string, stateKey: string): any {
    method setNodeState (line 39) | setNodeState(nodeId: string, stateKey: string, value: any): void {
    method getNodeInternalState (line 51) | getNodeInternalState(nodeId: string): Record<string, any> | undefined {
    method setNodeInternalState (line 56) | setNodeInternalState(nodeId: string, state: Record<string, any>): void {
    method clearNodeInternalState (line 62) | clearNodeInternalState(nodeId: string): void {
    method getAllNodeStates (line 69) | getAllNodeStates(): Map<string, Record<string, any>> {
    method clearAllNodeStates (line 89) | clearAllNodeStates(): void {
    method clearFlowNodeStates (line 105) | clearFlowNodeStates(flowId: string): void {
    method addStateChangeListener (line 115) | addStateChangeListener(listener: () => void): () => void {
    method addFlowIdChangeListener (line 120) | addFlowIdChangeListener(listener: () => void): () => void {
    method notifyStateChange (line 125) | private notifyStateChange(): void {
    method notifyFlowIdChange (line 129) | private notifyFlowIdChange(): void {
  type UseNodeStateReturn (line 141) | interface UseNodeStateReturn<T> {
  function setCurrentFlowId (line 147) | function setCurrentFlowId(flowId: string | null): void {
  function getNodeInternalState (line 152) | function getNodeInternalState(nodeId: string): Record<string, any> | und...
  function setNodeInternalState (line 156) | function setNodeInternalState(nodeId: string, state: Record<string, any>...
  function clearNodeInternalState (line 160) | function clearNodeInternalState(nodeId: string): void {
  function getAllNodeStates (line 165) | function getAllNodeStates(): Map<string, Record<string, any>> {
  function clearAllNodeStates (line 169) | function clearAllNodeStates(): void {
  function clearFlowNodeStates (line 173) | function clearFlowNodeStates(flowId: string): void {
  function addStateChangeListener (line 177) | function addStateChangeListener(listener: () => void): () => void {
  function useNodeState (line 194) | function useNodeState<T>(

FILE: app/frontend/src/hooks/use-output-node-connection.ts
  function useOutputNodeConnection (line 12) | function useOutputNodeConnection(nodeId: string) {

FILE: app/frontend/src/hooks/use-resizable.ts
  type UseResizableOptions (line 3) | interface UseResizableOptions {
  function useResizable (line 13) | function useResizable({

FILE: app/frontend/src/hooks/use-toast-manager.ts
  type ToastType (line 4) | type ToastType = 'success' | 'error' | 'info' | 'warning';
  type ToastPosition (line 5) | type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-l...
  type ToastOptions (line 7) | interface ToastOptions {
  type ToastState (line 13) | interface ToastState {
  function useToastManager (line 42) | function useToastManager() {

FILE: app/frontend/src/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
  function isMac (line 9) | function isMac(): boolean {
  function formatKeyboardShortcut (line 14) | function formatKeyboardShortcut(key: string): string {
  function getProviderColor (line 20) | function getProviderColor(provider: string): string {

FILE: app/frontend/src/nodes/components/agent-node.tsx
  function AgentNode (line 18) | function AgentNode({

FILE: app/frontend/src/nodes/components/agent-output-dialog.tsx
  type AgentOutputDialogProps (line 16) | interface AgentOutputDialogProps {
  function AgentOutputDialog (line 24) | function AgentOutputDialog({

FILE: app/frontend/src/nodes/components/investment-report-dialog.tsx
  type InvestmentReportDialogProps (line 35) | interface InvestmentReportDialogProps {
  type ActionType (line 42) | type ActionType = 'long' | 'short' | 'hold';
  function InvestmentReportDialog (line 44) | function InvestmentReportDialog({

FILE: app/frontend/src/nodes/components/investment-report-node.tsx
  function InvestmentReportNode (line 14) | function InvestmentReportNode({

FILE: app/frontend/src/nodes/components/json-output-dialog.tsx
  type JsonOutputDialogProps (line 14) | interface JsonOutputDialogProps {
  function JsonOutputDialog (line 21) | function JsonOutputDialog({

FILE: app/frontend/src/nodes/components/json-output-node.tsx
  function JsonOutputNode (line 16) | function JsonOutputNode({

FILE: app/frontend/src/nodes/components/node-shell.tsx
  type NodeShellProps (line 6) | interface NodeShellProps {
  function NodeShell (line 21) | function NodeShell({

FILE: app/frontend/src/nodes/components/output-node-status.tsx
  type OutputNodeStatusProps (line 4) | interface OutputNodeStatusProps {
  function OutputNodeStatus (line 16) | function OutputNodeStatus({

FILE: app/frontend/src/nodes/components/portfolio-manager-node.tsx
  function PortfolioManagerNode (line 19) | function PortfolioManagerNode({

FILE: app/frontend/src/nodes/components/portfolio-start-node.tsx
  type PortfolioPosition (line 31) | interface PortfolioPosition {
  function PortfolioStartNode (line 42) | function PortfolioStartNode({

FILE: app/frontend/src/nodes/components/stock-analyzer-node.tsx
  function StockAnalyzerNode (line 37) | function StockAnalyzerNode({

FILE: app/frontend/src/nodes/types.ts
  type NodeMessage (line 4) | type NodeMessage = MessageItem;
  type AgentNode (line 6) | type AgentNode = Node<{ name: string, description: string, status: strin...
  type InvestmentReportNode (line 7) | type InvestmentReportNode = Node<{ name: string, description: string, st...
  type JsonOutputNode (line 8) | type JsonOutputNode = Node<{ name: string, description: string, status: ...
  type PortfolioStartNode (line 9) | type PortfolioStartNode = Node<{ name: string, description: string, stat...
  type PortfolioManagerNode (line 10) | type PortfolioManagerNode = Node<{ name: string, description: string, st...
  type StockAnalyzerNode (line 11) | type StockAnalyzerNode = Node<{ name: string, description: string, statu...
  type AppNode (line 12) | type AppNode = BuiltInNode | AgentNode | InvestmentReportNode | JsonOutp...

FILE: app/frontend/src/nodes/utils.ts
  type NodeStatus (line 3) | type NodeStatus = 'IDLE' | 'IN_PROGRESS' | 'COMPLETE' | 'ERROR';
  function getStatusColor (line 8) | function getStatusColor(status: NodeStatus): string {
  function getNodesInCompletePaths (line 28) | function getNodesInCompletePaths({

FILE: app/frontend/src/providers/theme-provider.tsx
  type ThemeProviderProps (line 4) | interface ThemeProviderProps {
  function ThemeProvider (line 8) | function ThemeProvider({ children }: ThemeProviderProps) {

FILE: app/frontend/src/services/api-keys-api.ts
  constant API_BASE_URL (line 1) | const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8...
  type ApiKey (line 3) | interface ApiKey {
  type ApiKeySummary (line 14) | interface ApiKeySummary {
  type ApiKeyCreateRequest (line 25) | interface ApiKeyCreateRequest {
  type ApiKeyUpdateRequest (line 32) | interface ApiKeyUpdateRequest {
  type ApiKeyBulkUpdateRequest (line 38) | interface ApiKeyBulkUpdateRequest {
  class ApiKeysService (line 42) | class ApiKeysService {
    method getAllApiKeys (line 45) | async getAllApiKeys(includeInactive = false): Promise<ApiKeySummary[]> {
    method getApiKey (line 58) | async getApiKey(provider: string): Promise<ApiKey> {
    method createOrUpdateApiKey (line 69) | async createOrUpdateApiKey(request: ApiKeyCreateRequest): Promise<ApiK...
    method updateApiKey (line 84) | async updateApiKey(provider: string, request: ApiKeyUpdateRequest): Pr...
    method deleteApiKey (line 102) | async deleteApiKey(provider: string): Promise<void> {
    method deactivateApiKey (line 115) | async deactivateApiKey(provider: string): Promise<ApiKeySummary> {
    method bulkUpdateApiKeys (line 129) | async bulkUpdateApiKeys(request: ApiKeyBulkUpdateRequest): Promise<Api...
    method updateLastUsed (line 144) | async updateLastUsed(provider: string): Promise<void> {

FILE: app/frontend/src/services/api.ts
  constant API_BASE_URL (line 10) | const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8...

FILE: app/frontend/src/services/backtest-api.ts
  constant API_BASE_URL (line 10) | const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8...

FILE: app/frontend/src/services/flow-service.ts
  constant API_BASE_URL (line 3) | const API_BASE_URL = 'http://localhost:8000';
  type CreateFlowRequest (line 5) | interface CreateFlowRequest {
  type UpdateFlowRequest (line 16) | interface UpdateFlowRequest {
  method getFlows (line 29) | async getFlows(): Promise<Flow[]> {
  method getFlow (line 38) | async getFlow(id: number): Promise<Flow> {
  method createFlow (line 47) | async createFlow(data: CreateFlowRequest): Promise<Flow> {
  method updateFlow (line 62) | async updateFlow(id: number, data: UpdateFlowRequest): Promise<Flow> {
  method deleteFlow (line 77) | async deleteFlow(id: number): Promise<void> {
  method duplicateFlow (line 87) | async duplicateFlow(id: number, newName?: string): Promise<Flow> {
  method createDefaultFlow (line 99) | async createDefaultFlow(nodes: any, edges: any, viewport?: any): Promise...

FILE: app/frontend/src/services/sidebar-storage.ts
  type SidebarStates (line 1) | interface SidebarStates {
  class SidebarStorageService (line 7) | class SidebarStorageService {
    method saveLeftSidebarState (line 15) | static saveLeftSidebarState(isCollapsed: boolean): boolean {
    method saveRightSidebarState (line 28) | static saveRightSidebarState(isCollapsed: boolean): boolean {
    method saveBottomPanelState (line 41) | static saveBottomPanelState(isCollapsed: boolean): boolean {
    method saveSidebarStates (line 54) | static saveSidebarStates(states: SidebarStates): boolean {
    method loadLeftSidebarState (line 69) | static loadLeftSidebarState(defaultValue: boolean = false): boolean {
    method loadRightSidebarState (line 87) | static loadRightSidebarState(defaultValue: boolean = false): boolean {
    method loadBottomPanelState (line 105) | static loadBottomPanelState(defaultValue: boolean = true): boolean {
    method loadSidebarStates (line 123) | static loadSidebarStates(defaultStates: SidebarStates = { leftCollapse...
    method clearLeftSidebarState (line 134) | static clearLeftSidebarState(): boolean {
    method clearRightSidebarState (line 147) | static clearRightSidebarState(): boolean {
    method clearBottomPanelState (line 160) | static clearBottomPanelState(): boolean {
    method clearSidebarStates (line 173) | static clearSidebarStates(): boolean {
    method resetToDefaults (line 188) | static resetToDefaults(): boolean {
    method hasLeftSidebarState (line 207) | static hasLeftSidebarState(): boolean {
    method hasRightSidebarState (line 219) | static hasRightSidebarState(): boolean {
    method hasSidebarStates (line 231) | static hasSidebarStates(): { left: boolean; right: boolean } {

FILE: app/frontend/src/services/tab-service.ts
  type TabData (line 6) | interface TabData {
  class TabService (line 13) | class TabService {
    method createTabContent (line 14) | static createTabContent(tabData: TabData): ReactNode {
    method createFlowTab (line 30) | static createFlowTab(flow: Flow): TabData & { content: ReactNode } {
    method createSettingsTab (line 39) | static createSettingsTab(): TabData & { content: ReactNode } {
    method restoreTabContent (line 48) | static restoreTabContent(tabData: TabData): ReactNode {
    method restoreTab (line 53) | static restoreTab(savedTab: TabData): TabData & { content: ReactNode } {

FILE: app/frontend/src/services/types.ts
  type ModelProvider (line 2) | enum ModelProvider {
  type AgentModelConfig (line 9) | interface AgentModelConfig {
  type GraphNode (line 15) | interface GraphNode {
  type GraphEdge (line 22) | interface GraphEdge {
  type PortfolioPosition (line 30) | interface PortfolioPosition {
  type BaseHedgeFundRequest (line 37) | interface BaseHedgeFundRequest {
  type HedgeFundRequest (line 48) | interface HedgeFundRequest extends BaseHedgeFundRequest {
  type BacktestRequest (line 54) | interface BacktestRequest extends BaseHedgeFundRequest {
  type BacktestDayResult (line 60) | interface BacktestDayResult {
  type BacktestPerformanceMetrics (line 75) | interface BacktestPerformanceMetrics {

FILE: app/frontend/src/types/flow.ts
  type Flow (line 1) | interface Flow {

FILE: app/frontend/src/utils/date-utils.ts
  function formatTimeFromTimestamp (line 6) | function formatTimeFromTimestamp(timestamp: string): string {

FILE: app/frontend/src/utils/text-utils.ts
  function formatTextIntoParagraphs (line 8) | function formatTextIntoParagraphs(text: string): string[] {
  function isJsonString (line 53) | function isJsonString(text: string): boolean {
  function formatContent (line 107) | function formatContent(content: string): {
  function createHighlightedJson (line 183) | function createHighlightedJson(jsonString: string): string {
  function createAgentDisplayNames (line 241) | function createAgentDisplayNames(agentIds: string[]): Map<string, string> {

FILE: src/agents/aswath_damodaran.py
  class AswathDamodaranSignal (line 21) | class AswathDamodaranSignal(BaseModel):
  function aswath_damodaran_agent (line 27) | def aswath_damodaran_agent(state: AgentState, agent_id: str = "aswath_da...
  function analyze_growth_and_reinvestment (line 143) | def analyze_growth_and_reinvestment(metrics: list, line_items: list) -> ...
  function analyze_risk_profile (line 193) | def analyze_risk_profile(metrics: list, line_items: list) -> dict[str, a...
  function analyze_relative_valuation (line 254) | def analyze_relative_valuation(metrics: list) -> dict[str, any]:
  function calculate_intrinsic_value_dcf (line 285) | def calculate_intrinsic_value_dcf(metrics: list, line_items: list, risk_...
  function estimate_cost_of_equity (line 350) | def estimate_cost_of_equity(beta: float | None) -> float:
  function generate_damodaran_output (line 361) | def generate_damodaran_output(

FILE: src/agents/ben_graham.py
  class BenGrahamSignal (line 14) | class BenGrahamSignal(BaseModel):
  function ben_graham_agent (line 20) | def ben_graham_agent(state: AgentState, agent_id: str = "ben_graham_agen...
  function analyze_earnings_stability (line 97) | def analyze_earnings_stability(metrics: list, financial_line_items: list...
  function analyze_financial_strength (line 141) | def analyze_financial_strength(financial_line_items: list) -> dict:
  function analyze_valuation_graham (line 207) | def analyze_valuation_graham(financial_line_items: list, market_cap: flo...
  function generate_graham_output (line 282) | def generate_graham_output(

FILE: src/agents/bill_ackman.py
  class BillAckmanSignal (line 13) | class BillAckmanSignal(BaseModel):
  function bill_ackman_agent (line 19) | def bill_ackman_agent(state: AgentState, agent_id: str = "bill_ackman_ag...
  function analyze_business_quality (line 137) | def analyze_business_quality(metrics: list, financial_line_items: list) ...
  function analyze_financial_discipline (line 215) | def analyze_financial_discipline(metrics: list, financial_line_items: li...
  function analyze_activism_potential (line 290) | def analyze_activism_potential(financial_line_items: list) -> dict:
  function analyze_valuation (line 335) | def analyze_valuation(financial_line_items: list, market_cap: float) -> ...
  function generate_ackman_output (line 399) | def generate_ackman_output(

FILE: src/agents/cathie_wood.py
  class CathieWoodSignal (line 13) | class CathieWoodSignal(BaseModel):
  function cathie_wood_agent (line 19) | def cathie_wood_agent(state: AgentState, agent_id: str = "cathie_wood_ag...
  function analyze_disruptive_potential (line 111) | def analyze_disruptive_potential(metrics: list, financial_line_items: li...
  function analyze_innovation_growth (line 210) | def analyze_innovation_growth(metrics: list, financial_line_items: list)...
  function analyze_cathie_wood_valuation (line 318) | def analyze_cathie_wood_valuation(financial_line_items: list, market_cap...
  function generate_cathie_wood_output (line 363) | def generate_cathie_wood_output(

FILE: src/agents/charlie_munger.py
  class CharlieMungerSignal (line 12) | class CharlieMungerSignal(BaseModel):
  function charlie_munger_agent (line 18) | def charlie_munger_agent(state: AgentState, agent_id: str = "charlie_mun...
  function analyze_moat_strength (line 161) | def analyze_moat_strength(metrics: list, financial_line_items: list) -> ...
  function analyze_management_quality (line 268) | def analyze_management_quality(financial_line_items: list, insider_trade...
  function analyze_predictability (line 469) | def analyze_predictability(financial_line_items: list) -> dict:
  function calculate_munger_valuation (line 594) | def calculate_munger_valuation(financial_line_items: list, market_cap: f...
  function analyze_news_sentiment (line 710) | def analyze_news_sentiment(news_items: list) -> str:
  function _r (line 721) | def _r(x, n=3):
  function make_munger_facts_bundle (line 727) | def make_munger_facts_bundle(analysis: dict[str, any]) -> dict[str, any]:
  function compute_confidence (line 778) | def compute_confidence(analysis: dict, signal: str) -> int:
  function generate_munger_output (line 816) | def generate_munger_output(

FILE: src/agents/fundamentals.py
  function fundamentals_analyst_agent (line 11) | def fundamentals_analyst_agent(state: AgentState, agent_id: str = "funda...

FILE: src/agents/growth_agent.py
  function growth_analyst_agent (line 19) | def growth_analyst_agent(state: AgentState, agent_id: str = "growth_anal...
  function _calculate_trend (line 138) | def _calculate_trend(data: list[float | None]) -> float:
  function analyze_growth_trends (line 160) | def analyze_growth_trends(metrics: list) -> dict:
  function analyze_valuation (line 209) | def analyze_valuation(metrics) -> dict:
  function analyze_margin_trends (line 239) | def analyze_margin_trends(metrics: list) -> dict:
  function analyze_insider_conviction (line 282) | def analyze_insider_conviction(trades: list) -> dict:
  function check_financial_health (line 310) | def check_financial_health(metrics) -> dict:

FILE: src/agents/michael_burry.py
  class MichaelBurrySignal (line 24) | class MichaelBurrySignal(BaseModel):
  function michael_burry_agent (line 32) | def michael_burry_agent(state: AgentState, agent_id: str = "michael_burr...
  function _latest_line_item (line 166) | def _latest_line_item(line_items: list):
  function _analyze_value (line 173) | def _analyze_value(metrics, line_items, market_cap):
  function _analyze_balance_sheet (line 221) | def _analyze_balance_sheet(metrics, line_items):
  function _analyze_insider_activity (line 262) | def _analyze_insider_activity(insider_trades):
  function _analyze_contrarian_sentiment (line 287) | def _analyze_contrarian_sentiment(news):
  function _generate_burry_output (line 316) | def _generate_burry_output(

FILE: src/agents/mohnish_pabrai.py
  class MohnishPabraiSignal (line 13) | class MohnishPabraiSignal(BaseModel):
  function mohnish_pabrai_agent (line 19) | def mohnish_pabrai_agent(state: AgentState, agent_id: str = "mohnish_pab...
  function analyze_downside_protection (line 130) | def analyze_downside_protection(financial_line_items: list) -> dict[str,...
  function analyze_pabrai_valuation (line 196) | def analyze_pabrai_valuation(financial_line_items: list, market_cap: flo...
  function analyze_double_potential (line 253) | def analyze_double_potential(financial_line_items: list, market_cap: flo...
  function generate_pabrai_output (line 306) | def generate_pabrai_output(

FILE: src/agents/news_sentiment.py
  class Sentiment (line 18) | class Sentiment(BaseModel):
  function news_sentiment_agent (line 25) | def news_sentiment_agent(state: AgentState, agent_id: str = "news_sentim...
  function _calculate_confidence_score (line 166) | def _calculate_confidence_score(

FILE: src/agents/peter_lynch.py
  class PeterLynchSignal (line 18) | class PeterLynchSignal(BaseModel):
  function peter_lynch_agent (line 27) | def peter_lynch_agent(state: AgentState, agent_id: str = "peter_lynch_ag...
  function analyze_lynch_growth (line 161) | def analyze_lynch_growth(financial_line_items: list) -> dict:
  function analyze_lynch_fundamentals (line 226) | def analyze_lynch_fundamentals(financial_line_items: list) -> dict:
  function analyze_lynch_valuation (line 289) | def analyze_lynch_valuation(financial_line_items: list, market_cap: floa...
  function analyze_sentiment (line 365) | def analyze_sentiment(news_items: list) -> dict:
  function analyze_insider_activity (line 396) | def analyze_insider_activity(insider_trades: list) -> dict:
  function generate_lynch_output (line 441) | def generate_lynch_output(

FILE: src/agents/phil_fisher.py
  class PhilFisherSignal (line 18) | class PhilFisherSignal(BaseModel):
  function phil_fisher_agent (line 24) | def phil_fisher_agent(state: AgentState, agent_id: str = "phil_fisher_ag...
  function analyze_fisher_growth_quality (line 167) | def analyze_fisher_growth_quality(financial_line_items: list) -> dict:
  function analyze_margins_stability (line 262) | def analyze_margins_stability(financial_line_items: list) -> dict:
  function analyze_management_efficiency_leverage (line 328) | def analyze_management_efficiency_leverage(financial_line_items: list) -...
  function analyze_fisher_valuation (line 404) | def analyze_fisher_valuation(financial_line_items: list, market_cap: flo...
  function analyze_insider_activity (line 461) | def analyze_insider_activity(insider_trades: list) -> dict:
  function analyze_sentiment (line 503) | def analyze_sentiment(news_items: list) -> dict:
  function generate_fisher_output (line 531) | def generate_fisher_output(

FILE: src/agents/portfolio_manager.py
  class PortfolioDecision (line 13) | class PortfolioDecision(BaseModel):
  class PortfolioManagerOutput (line 20) | class PortfolioManagerOutput(BaseModel):
  function portfolio_management_agent (line 25) | def portfolio_management_agent(state: AgentState, agent_id: str = "portf...
  function compute_allowed_actions (line 96) | def compute_allowed_actions(
  function _compact_signals (line 160) | def _compact_signals(signals_by_ticker: dict[str, dict]) -> dict[str, di...
  function generate_trading_decision (line 177) | def generate_trading_decision(

FILE: src/agents/rakesh_jhunjhunwala.py
  class RakeshJhunjhunwalaSignal (line 12) | class RakeshJhunjhunwalaSignal(BaseModel):
  function rakesh_jhunjhunwala_agent (line 17) | def rakesh_jhunjhunwala_agent(state: AgentState, agent_id: str = "rakesh...
  function analyze_profitability (line 162) | def analyze_profitability(financial_line_items: list) -> dict[str, any]:
  function analyze_growth (line 246) | def analyze_growth(financial_line_items: list) -> dict[str, any]:
  function analyze_balance_sheet (line 327) | def analyze_balance_sheet(financial_line_items: list) -> dict[str, any]:
  function analyze_cash_flow (line 374) | def analyze_cash_flow(financial_line_items: list) -> dict[str, any]:
  function analyze_management_actions (line 409) | def analyze_management_actions(financial_line_items: list) -> dict[str, ...
  function assess_quality_metrics (line 437) | def assess_quality_metrics(financial_line_items: list) -> float:
  function calculate_intrinsic_value (line 498) | def calculate_intrinsic_value(financial_line_items: list, market_cap: fl...
  function analyze_rakesh_jhunjhunwala_style (line 584) | def analyze_rakesh_jhunjhunwala_style(
  function generate_jhunjhunwala_output (line 644) | def generate_jhunjhunwala_output(

FILE: src/agents/risk_manager.py
  function risk_management_agent (line 11) | def risk_management_agent(state: AgentState, agent_id: str = "risk_manag...
  function calculate_volatility_metrics (line 222) | def calculate_volatility_metrics(prices_df: pd.DataFrame, lookback_days:...
  function calculate_volatility_adjusted_limit (line 270) | def calculate_volatility_adjusted_limit(annualized_volatility: float) ->...
  function calculate_correlation_multiplier (line 301) | def calculate_correlation_multiplier(avg_correlation: float) -> float:

FILE: src/agents/sentiment.py
  function sentiment_analyst_agent (line 12) | def sentiment_analyst_agent(state: AgentState, agent_id: str = "sentimen...

FILE: src/agents/stanley_druckenmiller.py
  class StanleyDruckenmillerSignal (line 20) | class StanleyDruckenmillerSignal(BaseModel):
  function stanley_druckenmiller_agent (line 26) | def stanley_druckenmiller_agent(state: AgentState, agent_id: str = "stan...
  function analyze_growth_and_momentum (line 166) | def analyze_growth_and_momentum(financial_line_items: list, prices: list...
  function analyze_insider_activity (line 273) | def analyze_insider_activity(insider_trades: list) -> dict:
  function analyze_sentiment (line 320) | def analyze_sentiment(news_items: list) -> dict:
  function analyze_risk_reward (line 351) | def analyze_risk_reward(financial_line_items: list, prices: list) -> dict:
  function analyze_druckenmiller_valuation (line 425) | def analyze_druckenmiller_valuation(financial_line_items: list, market_c...
  function generate_druckenmiller_output (line 529) | def generate_druckenmiller_output(

FILE: src/agents/technicals.py
  function safe_float (line 15) | def safe_float(value, default=0.0):
  function technical_analyst_agent (line 35) | def technical_analyst_agent(state: AgentState, agent_id: str = "technica...
  function calculate_trend_signals (line 160) | def calculate_trend_signals(prices_df):
  function calculate_mean_reversion_signals (line 199) | def calculate_mean_reversion_signals(prices_df):
  function calculate_momentum_signals (line 241) | def calculate_momentum_signals(prices_df):
  function calculate_volatility_signals (line 286) | def calculate_volatility_signals(prices_df):
  function calculate_stat_arb_signals (line 333) | def calculate_stat_arb_signals(prices_df):
  function weighted_signal_combination (line 372) | def weighted_signal_combination(signals, weights):
  function normalize_pandas (line 407) | def normalize_pandas(obj):
  function calculate_rsi (line 420) | def calculate_rsi(prices_df: pd.DataFrame, period: int = 14) -> pd.Series:
  function calculate_bollinger_bands (line 431) | def calculate_bollinger_bands(prices_df: pd.DataFrame, window: int = 20)...
  function calculate_ema (line 439) | def calculate_ema(df: pd.DataFrame, window: int) -> pd.Series:
  function calculate_adx (line 453) | def calculate_adx(df: pd.DataFrame, period: int = 14) -> pd.DataFrame:
  function calculate_atr (line 486) | def calculate_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
  function calculate_hurst_exponent (line 507) | def calculate_hurst_exponent(price_series: pd.Series, max_lag: int = 20)...

FILE: src/agents/valuation.py
  function valuation_analyst_agent (line 21) | def valuation_analyst_agent(state: AgentState, agent_id: str = "valuatio...
  function calculate_owner_earnings_value (line 226) | def calculate_owner_earnings_value(
  function calculate_intrinsic_value (line 259) | def calculate_intrinsic_value(
  function calculate_ev_ebitda_value (line 283) | def calculate_ev_ebitda_value(financial_metrics: list):
  function calculate_residual_income_value (line 302) | def calculate_residual_income_value(
  function calculate_wacc (line 338) | def calculate_wacc(
  function calculate_fcf_volatility (line 376) | def calculate_fcf_volatility(fcf_history: list[float]) -> float:
  function calculate_enhanced_dcf_value (line 394) | def calculate_enhanced_dcf_value(
  function calculate_dcf_scenarios (line 451) | def calculate_dcf_scenarios(

FILE: src/agents/warren_buffett.py
  class WarrenBuffettSignal (line 13) | class WarrenBuffettSignal(BaseModel):
  function warren_buffett_agent (line 19) | def warren_buffett_agent(state: AgentState, agent_id: str = "warren_buff...
  function analyze_fundamentals (line 156) | def analyze_fundamentals(metrics: list) -> dict[str, any]:
  function analyze_consistency (line 205) | def analyze_consistency(financial_line_items: list) -> dict[str, any]:
  function analyze_moat (line 238) | def analyze_moat(metrics: list) -> dict[str, any]:
  function analyze_management_quality (line 337) | def analyze_management_quality(financial_line_items: list) -> dict[str, ...
  function calculate_owner_earnings (line 380) | def calculate_owner_earnings(financial_line_items: list) -> dict[str, any]:
  function estimate_maintenance_capex (line 456) | def estimate_maintenance_capex(financial_line_items: list) -> float:
  function calculate_intrinsic_value (line 508) | def calculate_intrinsic_value(financial_line_items: list) -> dict[str, a...
  function analyze_book_value_growth (line 627) | def analyze_book_value_growth(financial_line_items: list) -> dict[str, a...
  function _calculate_book_value_cagr (line 671) | def _calculate_book_value_cagr(book_values: list) -> tuple[int, str]:
  function analyze_pricing_power (line 696) | def analyze_pricing_power(financial_line_items: list, metrics: list) -> ...
  function generate_buffett_output (line 746) | def generate_buffett_output(

FILE: src/backtester.py
  function run_backtest (line 13) | def run_backtest(backtester: BacktestEngine) -> PerformanceMetrics | None:

FILE: src/backtesting/benchmarks.py
  class BenchmarkCalculator (line 8) | class BenchmarkCalculator:
    method get_return_pct (line 9) | def get_return_pct(self, ticker: str, start_date: str, end_date: str) ...

FILE: src/backtesting/cli.py
  function main (line 18) | def main() -> int:

FILE: src/backtesting/controller.py
  class AgentController (line 9) | class AgentController:
    method run_agent (line 12) | def run_agent(

FILE: src/backtesting/engine.py
  class BacktestEngine (line 27) | class BacktestEngine:
    method __init__ (line 35) | def __init__(
    method _prefetch_data (line 81) | def _prefetch_data(self) -> None:
    method run_backtest (line 96) | def run_backtest(self) -> PerformanceMetrics:
    method get_portfolio_values (line 191) | def get_portfolio_values(self) -> Sequence[PortfolioValuePoint]:

FILE: src/backtesting/metrics.py
  class PerformanceMetricsCalculator (line 8) | class PerformanceMetricsCalculator:
    method __init__ (line 11) | def __init__(self, *, annual_trading_days: int = 252, annual_rf_rate: ...
    method update_metrics (line 15) | def update_metrics(self, metrics: PerformanceMetrics, values: Sequence...
    method compute_metrics (line 22) | def compute_metrics(self, values: Sequence[PortfolioValuePoint]) -> Pe...

FILE: src/backtesting/output.py
  class OutputBuilder (line 11) | class OutputBuilder:
    method __init__ (line 17) | def __init__(self, *, initial_capital: float | None = None) -> None:
    method build_day_rows (line 20) | def build_day_rows(
    method print_rows (line 95) | def print_rows(self, rows: List[list]) -> None:

FILE: src/backtesting/portfolio.py
  class Portfolio (line 9) | class Portfolio:
    method __init__ (line 17) | def __init__(
    method get_snapshot (line 44) | def get_snapshot(self) -> PortfolioSnapshot:
    method get_cash (line 67) | def get_cash(self) -> float:
    method get_margin_used (line 70) | def get_margin_used(self) -> float:
    method get_margin_requirement (line 73) | def get_margin_requirement(self) -> float:
    method get_positions (line 76) | def get_positions(self) -> Mapping[str, PositionState]:
    method get_realized_gains (line 79) | def get_realized_gains(self) -> Mapping[str, TickerRealizedGains]:
    method apply_long_buy (line 82) | def apply_long_buy(self, ticker: str, quantity: int, price: float) -> ...
    method apply_long_sell (line 114) | def apply_long_sell(self, ticker: str, quantity: int, price: float) ->...
    method apply_short_open (line 128) | def apply_short_open(self, ticker: str, quantity: int, price: float) -...
    method apply_short_cover (line 172) | def apply_short_cover(self, ticker: str, quantity: int, price: float) ...

FILE: src/backtesting/trader.py
  class TradeExecutor (line 7) | class TradeExecutor:
    method execute_trade (line 10) | def execute_trade(

FILE: src/backtesting/types.py
  class Action (line 10) | class Action(str, Enum):
  class PositionState (line 21) | class PositionState(TypedDict):
  class TickerRealizedGains (line 31) | class TickerRealizedGains(TypedDict):
  class PortfolioSnapshot (line 38) | class PortfolioSnapshot(TypedDict):
  class AgentDecision (line 56) | class AgentDecision(TypedDict):
  class AgentOutput (line 69) | class AgentOutput(TypedDict):
  class PerformanceMetrics (line 90) | class PerformanceMetrics(TypedDict, total=False):

FILE: src/backtesting/valuation.py
  function calculate_portfolio_value (line 8) | def calculate_portfolio_value(portfolio: Portfolio, current_prices: Mapp...
  function compute_exposures (line 24) | def compute_exposures(portfolio: Portfolio, current_prices: Mapping[str,...
  function compute_portfolio_summary (line 54) | def compute_portfolio_summary(

FILE: src/cli/input.py
  function add_common_args (line 16) | def add_common_args(
  function add_date_args (line 47) | def add_date_args(parser: argparse.ArgumentParser, *, default_months_bac...
  function parse_tickers (line 67) | def parse_tickers(tickers_arg: str | None) -> list[str]:
  function select_analysts (line 73) | def select_analysts(flags: dict | None = None) -> list[str]:
  function select_model (line 105) | def select_model(use_ollama: bool, model_flag: str | None = None) -> tup...
  function resolve_dates (line 190) | def resolve_dates(start_date: str | None, end_date: str | None, *, defau...
  class CLIInputs (line 213) | class CLIInputs:
  function parse_cli_inputs (line 227) | def parse_cli_inputs(

FILE: src/data/cache.py
  class Cache (line 1) | class Cache:
    method __init__ (line 4) | def __init__(self):
    method _merge_data (line 11) | def _merge_data(self, existing: list[dict] | None, new_data: list[dict...
    method get_prices (line 24) | def get_prices(self, ticker: str) -> list[dict[str, any]] | None:
    method set_prices (line 28) | def set_prices(self, ticker: str, data: list[dict[str, any]]):
    method get_financial_metrics (line 32) | def get_financial_metrics(self, ticker: str) -> list[dict[str, any]]:
    method set_financial_metrics (line 36) | def set_financial_metrics(self, ticker: str, data: list[dict[str, any]]):
    method get_line_items (line 40) | def get_line_items(self, ticker: str) -> list[dict[str, any]] | None:
    method set_line_items (line 44) | def set_line_items(self, ticker: str, data: list[dict[str, any]]):
    method get_insider_trades (line 48) | def get_insider_trades(self, ticker: str) -> list[dict[str, any]] | None:
    method set_insider_trades (line 52) | def set_insider_trades(self, ticker: str, data: list[dict[str, any]]):
    method get_company_news (line 56) | def get_company_news(self, ticker: str) -> list[dict[str, any]] | None:
    method set_company_news (line 60) | def set_company_news(self, ticker: str, data: list[dict[str, any]]):
  function get_cache (line 69) | def get_cache() -> Cache:

FILE: src/data/models.py
  class Price (line 4) | class Price(BaseModel):
  class PriceResponse (line 13) | class PriceResponse(BaseModel):
  class FinancialMetrics (line 18) | class FinancialMetrics(BaseModel):
  class FinancialMetricsResponse (line 64) | class FinancialMetricsResponse(BaseModel):
  class LineItem (line 68) | class LineItem(BaseModel):
  class LineItemResponse (line 78) | class LineItemResponse(BaseModel):
  class InsiderTrade (line 82) | class InsiderTrade(BaseModel):
  class InsiderTradeResponse (line 98) | class InsiderTradeResponse(BaseModel):
  class CompanyNews (line 102) | class CompanyNews(BaseModel):
  class CompanyNewsResponse (line 112) | class CompanyNewsResponse(BaseModel):
  class CompanyFacts (line 116) | class CompanyFacts(BaseModel):
  class CompanyFactsResponse (line 137) | class CompanyFactsResponse(BaseModel):
  class Position (line 141) | class Position(BaseModel):
  class Portfolio (line 147) | class Portfolio(BaseModel):
  class AnalystSignal (line 152) | class AnalystSignal(BaseModel):
  class TickerAnalysis (line 159) | class TickerAnalysis(BaseModel):
  class AgentStateData (line 164) | class AgentStateData(BaseModel):
  class AgentStateMetadata (line 172) | class AgentStateMetadata(BaseModel):

FILE: src/graph/state.py
  function merge_dicts (line 10) | def merge_dicts(a: dict[str, any], b: dict[str, any]) -> dict[str, any]:
  class AgentState (line 15) | class AgentState(TypedDict):
  function show_agent_reasoning (line 21) | def show_agent_reasoning(output, agent_name):

FILE: src/llm/models.py
  class ModelProvider (line 17) | class ModelProvider(str, Enum):
  class LLMModel (line 35) | class LLMModel(BaseModel):
    method to_choice_tuple (line 42) | def to_choice_tuple(self) -> Tuple[str, str, str]:
    method is_custom (line 46) | def is_custom(self) -> bool:
    method has_json_mode (line 50) | def has_json_mode(self) -> bool:
    method is_deepseek (line 62) | def is_deepseek(self) -> bool:
    method is_gemini (line 66) | def is_gemini(self) -> bool:
    method is_ollama (line 70) | def is_ollama(self) -> bool:
  function load_models_from_json (line 76) | def load_models_from_json(json_path: str) -> List[LLMModel]:
  function get_model_info (line 113) | def get_model_info(model_name: str, model_provider: str) -> LLMModel | N...
  function find_model_by_name (line 119) | def find_model_by_name(model_name: str) -> LLMModel | None:
  function get_models_list (line 125) | def get_models_list():
  function get_model (line 137) | def get_model(model_name: str, model_provider: ModelProvider, api_keys: ...

FILE: src/main.py
  function parse_hedge_fund_response (line 30) | def parse_hedge_fund_response(response):
  function run_hedge_fund (line 46) | def run_hedge_fund(
  function start (line 95) | def start(state: AgentState):
  function create_workflow (line 100) | def create_workflow(selected_analysts=None):

FILE: src/tools/api.py
  function _make_api_request (line 29) | def _make_api_request(url: str, headers: dict, method: str = "GET", json...
  function get_prices (line 63) | def get_prices(ticker: str, start_date: str, end_date: str, api_key: str...
  function get_financial_metrics (line 99) | def get_financial_metrics(
  function search_line_items (line 141) | def search_line_items(
  function get_insider_trades (line 183) | def get_insider_trades(
  function get_company_news (line 249) | def get_company_news(
  function get_market_cap (line 315) | def get_market_cap(
  function prices_to_df (line 351) | def prices_to_df(prices: list[Price]) -> pd.DataFrame:
  function get_price_data (line 364) | def get_price_data(ticker: str, start_date: str, end_date: str, api_key:...

FILE: src/utils/analysts.py
  function get_analyst_nodes (line 175) | def get_analyst_nodes():
  function get_agents_list (line 180) | def get_agents_list():

FILE: src/utils/api_key.py
  function get_api_key_from_state (line 3) | def get_api_key_from_state(state: dict, api_key_name: str) -> str:

FILE: src/utils/display.py
  function sort_agent_signals (line 8) | def sort_agent_signals(signals):
  function print_trading_output (line 17) | def print_trading_output(result: dict) -> None:
  function print_backtest_results (line 257) | def print_backtest_results(table_rows: list) -> None:
  function format_backtest_row (line 333) | def format_backtest_row(

FILE: src/utils/docker.py
  function ensure_ollama_and_model (line 8) | def ensure_ollama_and_model(model_name: str, ollama_url: str) -> bool:
  function is_ollama_available (line 33) | def is_ollama_available(ollama_url: str) -> bool:
  function get_available_models (line 48) | def get_available_models(ollama_url: str) -> list:
  function download_model (line 63) | def download_model(model_name: str, ollama_url: str) -> bool:
  function delete_model (line 108) | def delete_model(model_name: str, ollama_url: str) -> bool:

FILE: src/utils/llm.py
  function call_llm (line 10) | def call_llm(
  function create_default_response (line 87) | def create_default_response(model_class: type[BaseModel]) -> BaseModel:
  function extract_json_from_response (line 109) | def extract_json_from_response(content: str) -> dict | None:
  function get_agent_model_config (line 124) | def get_agent_model_config(state, agent_name):

FILE: src/utils/ollama.py
  function _get_ollama_base_url (line 17) | def _get_ollama_base_url() -> str:
  function _get_ollama_endpoint (line 25) | def _get_ollama_endpoint(path: str) -> str:
  function is_ollama_installed (line 37) | def is_ollama_installed() -> bool:
  function is_ollama_server_running (line 57) | def is_ollama_server_running() -> bool:
  function get_locally_available_models (line 67) | def get_locally_available_models() -> List[str]:
  function start_ollama_server (line 83) | def start_ollama_server() -> bool:
  function install_ollama (line 114) | def install_ollama() -> bool:
  function download_model (line 207) | def download_model(model_name: str) -> bool:
  function ensure_ollama_and_model (line 311) | def ensure_ollama_and_model(model_name: str) -> bool:
  function delete_model (line 360) | def delete_model(model_name: str) -> bool:

FILE: src/utils/progress.py
  class AgentProgress (line 12) | class AgentProgress:
    method __init__ (line 15) | def __init__(self):
    method register_handler (line 22) | def register_handler(self, handler: Callable[[str, Optional[str], str]...
    method unregister_handler (line 27) | def unregister_handler(self, handler: Callable[[str, Optional[str], st...
    method start (line 32) | def start(self):
    method stop (line 38) | def stop(self):
    method update_status (line 44) | def update_status(self, agent_name: str, ticker: Optional[str] = None,...
    method get_all_status (line 66) | def get_all_status(self):
    method _get_display_name (line 70) | def _get_display_name(self, agent_name: str) -> str:
    method _refresh_display (line 74) | def _refresh_display(self):

FILE: src/utils/visualize.py
  function save_graph_as_png (line 5) | def save_graph_as_png(app: CompiledGraph, output_file_path) -> None:

FILE: tests/backtesting/conftest.py
  function portfolio (line 7) | def portfolio() -> Portfolio:
  function prices (line 12) | def prices() -> dict[str, float]:
  function price_df_factory (line 17) | def price_df_factory():

FILE: tests/backtesting/integration/conftest.py
  function _find_price_fixture_file (line 14) | def _find_price_fixture_file(ticker: str, start: str, end: str) -> Path ...
  function _load_price_df_from_fixture (line 30) | def _load_price_df_from_fixture(ticker: str, start: str, end: str) -> pd...
  function _find_fm_fixture_file (line 49) | def _find_fm_fixture_file(ticker: str, end: str) -> Path | None:
  function _load_financial_metrics_from_fixture (line 63) | def _load_financial_metrics_from_fixture(ticker: str, end: str, limit: i...
  function _load_news_from_fixture (line 74) | def _load_news_from_fixture(ticker: str, start: str | None, end: str, li...
  function _load_insider_from_fixture (line 92) | def _load_insider_from_fixture(ticker: str, start: str | None, end: str,...
  function patch_engine_prices (line 108) | def patch_engine_prices(monkeypatch):

FILE: tests/backtesting/integration/mocks.py
  class MockConfigurableAgent (line 4) | class MockConfigurableAgent:
    method __init__ (line 7) | def __init__(self, decision_sequence: list[dict], tickers: list[str]):
    method __call__ (line 24) | def __call__(self, **kwargs) -> AgentOutput:

FILE: tests/backtesting/integration/test_integration_long_only.py
  function test_long_only_strategy_buys_and_sells (line 4) | def test_long_only_strategy_buys_and_sells():
  function test_long_only_strategy_full_liquidation_cycle (line 94) | def test_long_only_strategy_full_liquidation_cycle():
  function test_long_only_strategy_portfolio_rebalancing (line 196) | def test_long_only_strategy_portfolio_rebalancing():
  function test_long_only_strategy_multiple_entry_exit_cycles (line 312) | def test_long_only_strategy_multiple_entry_exit_cycles():

FILE: tests/backtesting/integration/test_integration_long_short.py
  function test_long_short_strategy_partial_exits (line 5) | def test_long_short_strategy_partial_exits():
  function test_long_short_strategy_full_liquidation_to_cash (line 83) | def test_long_short_strategy_full_liquidation_to_cash():
  function test_long_short_strategy_directional_flip_on_ticker (line 159) | def test_long_short_strategy_directional_flip_on_ticker():
  function test_long_short_strategy_dca_both_sides (line 236) | def test_long_short_strategy_dca_both_sides():

FILE: tests/backtesting/integration/test_integration_short_only.py
  function test_short_only_strategy_shorts_and_covers (line 5) | def test_short_only_strategy_shorts_and_covers():
  function test_short_only_strategy_full_cover_cycle (line 81) | def test_short_only_strategy_full_cover_cycle():
  function test_short_only_strategy_multiple_short_cover_cycles (line 166) | def test_short_only_strategy_multiple_short_cover_cycles():
  function test_short_only_strategy_portfolio_rebalancing (line 240) | def test_short_only_strategy_portfolio_rebalancing():
  function test_short_only_strategy_dollar_cost_averaging_on_short (line 330) | def test_short_only_strategy_dollar_cost_averaging_on_short():

FILE: tests/backtesting/test_controller.py
  function dummy_agent (line 4) | def dummy_agent(**kwargs):
  function test_agent_controller_normalizes_and_snapshots (line 13) | def test_agent_controller_normalizes_and_snapshots(portfolio):

FILE: tests/backtesting/test_execution.py
  function test_trade_executor_routes_actions (line 4) | def test_trade_executor_routes_actions(portfolio):
  function test_trade_executor_guards_and_unknown_action (line 21) | def test_trade_executor_guards_and_unknown_action(portfolio):

FILE: tests/backtesting/test_metrics.py
  function _build_values (line 8) | def _build_values(values: list[float]):
  function test_metrics_insufficient_data_no_update (line 24) | def test_metrics_insufficient_data_no_update():
  function test_metrics_basic_sharpe_sortino_and_drawdown (line 33) | def test_metrics_basic_sharpe_sortino_and_drawdown():
  function test_metrics_zero_volatility_sharpe_zero (line 45) | def test_metrics_zero_volatility_sharpe_zero():

FILE: tests/backtesting/test_portfolio.py
  function test_apply_long_buy_basic (line 7) | def test_apply_long_buy_basic(portfolio: Portfolio) -> None:
  function test_apply_long_buy_partial_fill_when_insufficient_cash (line 17) | def test_apply_long_buy_partial_fill_when_insufficient_cash() -> None:
  function test_apply_long_sell_realized_gain_and_cost_basis_reset (line 27) | def test_apply_long_sell_realized_gain_and_cost_basis_reset(portfolio: P...
  function test_apply_long_sell_clamps_to_owned (line 40) | def test_apply_long_sell_clamps_to_owned() -> None:
  function test_apply_short_open_basic (line 49) | def test_apply_short_open_basic(portfolio: Portfolio) -> None:
  function test_apply_short_open_partial_when_insufficient_margin_cash (line 63) | def test_apply_short_open_partial_when_insufficient_margin_cash() -> None:
  function test_apply_short_open_uses_available_cash_not_total_cash (line 77) | def test_apply_short_open_uses_available_cash_not_total_cash() -> None:
  function test_apply_short_cover_realized_gain_and_margin_release (line 95) | def test_apply_short_cover_realized_gain_and_margin_release(portfolio: P...
  function test_apply_short_cover_clamps_to_existing_short (line 113) | def test_apply_short_cover_clamps_to_existing_short() -> None:
  function test_zero_or_negative_quantity_is_noop (line 124) | def test_zero_or_negative_quantity_is_noop(portfolio: Portfolio, action:...

FILE: tests/backtesting/test_results.py
  function test_results_builder_builds_rows_and_summary (line 4) | def test_results_builder_builds_rows_and_summary(monkeypatch, portfolio):

FILE: tests/backtesting/test_valuation.py
  function test_calculate_portfolio_value (line 4) | def test_calculate_portfolio_value(portfolio, prices):
  function test_compute_exposures (line 16) | def test_compute_exposures(portfolio, prices):
  function test_compute_exposures_with_no_shorts_ratio_inf (line 28) | def test_compute_exposures_with_no_shorts_ratio_inf(portfolio, prices):
  function test_compute_portfolio_summary (line 35) | def test_compute_portfolio_summary(portfolio, prices):

FILE: tests/test_api_rate_limiting.py
  class TestRateLimiting (line 7) | class TestRateLimiting:
    method test_handles_single_rate_limit (line 12) | def test_handles_single_rate_limit(self, mock_get, mock_sleep):
    method test_handles_multiple_rate_limits (line 46) | def test_handles_multiple_rate_limits(self, mock_get, mock_sleep):
    method test_handles_post_rate_limiting (line 83) | def test_handles_post_rate_limiting(self, mock_post, mock_sleep):
    method test_ignores_other_errors (line 118) | def test_ignores_other_errors(self, mock_get, mock_sleep):
    method test_normal_success_requests (line 145) | def test_normal_success_requests(self, mock_get, mock_sleep):
    method test_full_integration (line 173) | def test_full_integration(self, mock_get, mock_sleep, mock_cache):
    method test_max_retries_exceeded (line 220) | def test_max_retries_exceeded(self, mock_get, mock_sleep):
Condensed preview — 258 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,658K chars).
[
  {
    "path": ".dockerignore",
    "chars": 226,
    "preview": "# Git\n.git\n.gitignore\n\n# Poetry\n.venv\n__pycache__/\n*.py[cod]\n*$py.class\n.pytest_cache/\n\n# Environment\n.env\n\n# IDEs and e"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 322,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 212,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**De"
  },
  {
    "path": ".gitignore",
    "chars": 677,
    "preview": "# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib64/\np"
  },
  {
    "path": "README.md",
    "chars": 6415,
    "preview": "# AI Hedge Fund\n\nThis is a proof of concept for an AI-powered hedge fund.  The goal of this project is to explore the us"
  },
  {
    "path": "app/README.md",
    "chars": 6000,
    "preview": "# Web Application\nThe AI Hedge Fund app is a complete system with both frontend and backend components that enables you "
  },
  {
    "path": "app/backend/README.md",
    "chars": 3123,
    "preview": "# AI Hedge Fund - Backend [WIP] 🚧\nThis project is currently a work in progress.  To track progress, please get updates ["
  },
  {
    "path": "app/backend/__init__.py",
    "chars": 267,
    "preview": "import sys\nfrom pathlib import Path\n\n# Add the src directory to Python path for imports\n# This is a temporary solution w"
  },
  {
    "path": "app/backend/alembic/README",
    "chars": 38,
    "preview": "Generic single-database configuration."
  },
  {
    "path": "app/backend/alembic/env.py",
    "chars": 2087,
    "preview": "from logging.config import fileConfig\n\nfrom sqlalchemy import engine_from_config\nfrom sqlalchemy import pool\n\nfrom alemb"
  },
  {
    "path": "app/backend/alembic/script.py.mako",
    "chars": 689,
    "preview": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom typ"
  },
  {
    "path": "app/backend/alembic/versions/1b1feba3d897_add_data_column_to_hedge_fund_flows.py",
    "chars": 882,
    "preview": "\"\"\"add_data_column_to_hedge_fund_flows\n\nRevision ID: 1b1feba3d897\nRevises: 5274886e5bee\nCreate Date: 2025-06-22 17:30:50"
  },
  {
    "path": "app/backend/alembic/versions/2f8c5d9e4b1a_add_hedgefundflowrun_table.py",
    "chars": 2042,
    "preview": "\"\"\"Add HedgeFundFlowRun table\n\nRevision ID: 2f8c5d9e4b1a\nRevises: 1b1feba3d897\nCreate Date: 2025-01-01 12:00:00.000000\n\n"
  },
  {
    "path": "app/backend/alembic/versions/3f9a6b7c8d2e_add_hedgefundflowruncycle_table.py",
    "chars": 5206,
    "preview": "\"\"\"Add HedgeFundFlowRunCycle table and update HedgeFundFlowRun\n\nRevision ID: 3f9a6b7c8d2e\nRevises: 2f8c5d9e4b1a\nCreate D"
  },
  {
    "path": "app/backend/alembic/versions/5274886e5bee_add_hedgefundflow_table.py",
    "chars": 1634,
    "preview": "\"\"\"Add HedgeFundFlow table\n\nRevision ID: 5274886e5bee\nRevises: \nCreate Date: 2025-06-20 14:50:24.736989\n\n\"\"\"\nfrom typing"
  },
  {
    "path": "app/backend/alembic/versions/add_api_keys_table.py",
    "chars": 1591,
    "preview": "\"\"\"add_api_keys_table\n\nRevision ID: d5e78f9a1b2c\nRevises: 3f9a6b7c8d2e\nCreate Date: 2025-07-27 15:20:00.000000\n\n\"\"\"\nfrom"
  },
  {
    "path": "app/backend/alembic.ini",
    "chars": 3729,
    "preview": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts\n# Use forward slashes (/) also on win"
  },
  {
    "path": "app/backend/database/__init__.py",
    "chars": 134,
    "preview": "from .connection import get_db, engine, SessionLocal\nfrom .models import Base\n\n__all__ = [\"get_db\", \"engine\", \"SessionLo"
  },
  {
    "path": "app/backend/database/connection.py",
    "chars": 805,
    "preview": "from sqlalchemy import create_engine\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import "
  },
  {
    "path": "app/backend/database/models.py",
    "chars": 5478,
    "preview": "from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, JSON, ForeignKey\nfrom sqlalchemy.sql import fun"
  },
  {
    "path": "app/backend/main.py",
    "chars": 2091,
    "preview": "from fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nimport logging\nimport asyncio\n\nfrom app.b"
  },
  {
    "path": "app/backend/models/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "app/backend/models/events.py",
    "chars": 1166,
    "preview": "from typing import Dict, Optional, Any, Literal\nfrom pydantic import BaseModel\n\n\nclass BaseEvent(BaseModel):\n    \"\"\"Base"
  },
  {
    "path": "app/backend/models/schemas.py",
    "chars": 8335,
    "preview": "from datetime import datetime, timedelta\nfrom pydantic import BaseModel, Field, field_validator\nfrom typing import List,"
  },
  {
    "path": "app/backend/repositories/__init__.py",
    "chars": 74,
    "preview": "from .flow_repository import FlowRepository\n\n__all__ = [\"FlowRepository\"] "
  },
  {
    "path": "app/backend/repositories/api_key_repository.py",
    "chars": 4321,
    "preview": "from sqlalchemy.orm import Session\nfrom sqlalchemy import func\nfrom typing import List, Optional\nfrom datetime import da"
  },
  {
    "path": "app/backend/repositories/flow_repository.py",
    "chars": 3735,
    "preview": "from typing import List, Optional\nfrom sqlalchemy.orm import Session\nfrom app.backend.database.models import HedgeFundFl"
  },
  {
    "path": "app/backend/repositories/flow_run_repository.py",
    "chars": 4778,
    "preview": "from typing import List, Optional, Dict, Any\nfrom datetime import datetime\nfrom sqlalchemy.orm import Session\nfrom sqlal"
  },
  {
    "path": "app/backend/routes/__init__.py",
    "chars": 1130,
    "preview": "from fastapi import APIRouter\n\nfrom app.backend.routes.hedge_fund import router as hedge_fund_router\nfrom app.backend.ro"
  },
  {
    "path": "app/backend/routes/api_keys.py",
    "chars": 7177,
    "preview": "from fastapi import APIRouter, HTTPException, Depends\nfrom sqlalchemy.orm import Session\nfrom typing import List\n\nfrom a"
  },
  {
    "path": "app/backend/routes/flow_runs.py",
    "chars": 10667,
    "preview": "from fastapi import APIRouter, HTTPException, Depends, Query\nfrom sqlalchemy.orm import Session\nfrom typing import List,"
  },
  {
    "path": "app/backend/routes/flows.py",
    "chars": 5807,
    "preview": "from fastapi import APIRouter, HTTPException, Depends\nfrom sqlalchemy.orm import Session\nfrom typing import List\n\nfrom a"
  },
  {
    "path": "app/backend/routes/health.py",
    "chars": 658,
    "preview": "from fastapi import APIRouter\nfrom fastapi.responses import StreamingResponse\nimport asyncio\nimport json\n\nrouter = APIRo"
  },
  {
    "path": "app/backend/routes/hedge_fund.py",
    "chars": 15063,
    "preview": "from fastapi import APIRouter, HTTPException, Request, Depends\nfrom fastapi.responses import StreamingResponse\nfrom sqla"
  },
  {
    "path": "app/backend/routes/language_models.py",
    "chars": 2145,
    "preview": "from fastapi import APIRouter, HTTPException\nfrom typing import List, Dict, Any\n\nfrom app.backend.models.schemas import "
  },
  {
    "path": "app/backend/routes/ollama.py",
    "chars": 12225,
    "preview": "from fastapi import APIRouter, HTTPException\nfrom fastapi.responses import StreamingResponse\nfrom pydantic import BaseMo"
  },
  {
    "path": "app/backend/routes/storage.py",
    "chars": 1470,
    "preview": "from fastapi import APIRouter, HTTPException\nimport json\nfrom pathlib import Path\nfrom pydantic import BaseModel\n\nfrom a"
  },
  {
    "path": "app/backend/services/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "app/backend/services/agent_service.py",
    "chars": 516,
    "preview": "from functools import partial\nfrom typing import Callable\nfrom src.graph.state import AgentState\n\ndef create_agent_funct"
  },
  {
    "path": "app/backend/services/api_key_service.py",
    "chars": 884,
    "preview": "from sqlalchemy.orm import Session\nfrom typing import Dict, Optional\nfrom app.backend.repositories.api_key_repository im"
  },
  {
    "path": "app/backend/services/backtest_service.py",
    "chars": 22371,
    "preview": "from datetime import datetime, timedelta\nfrom dateutil.relativedelta import relativedelta\nimport pandas as pd\nimport num"
  },
  {
    "path": "app/backend/services/graph.py",
    "chars": 7974,
    "preview": "import asyncio\nimport json\nimport re\nfrom langchain_core.messages import HumanMessage\nfrom langgraph.graph import END, S"
  },
  {
    "path": "app/backend/services/ollama_service.py",
    "chars": 20824,
    "preview": "import asyncio\nimport os\nimport sys\nimport platform\nimport subprocess\nimport time\nimport re\nimport json\nimport queue\nimp"
  },
  {
    "path": "app/backend/services/portfolio.py",
    "chars": 2474,
    "preview": "\nfrom typing import Optional, List\nfrom app.backend.models.schemas import PortfolioPosition\n\n\ndef create_portfolio(initi"
  },
  {
    "path": "app/frontend/.eslintrc.cjs",
    "chars": 436,
    "preview": "module.exports = {\n  root: true,\n  env: { browser: true, es2020: true },\n  extends: [\n    'eslint:recommended',\n    'plu"
  },
  {
    "path": "app/frontend/.github/dependabot.yml",
    "chars": 295,
    "preview": "# docs:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-depe"
  },
  {
    "path": "app/frontend/.gitignore",
    "chars": 253,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "app/frontend/LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2023 webkid GmbH\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "app/frontend/README.md",
    "chars": 1372,
    "preview": "# AI Hedge Fund - Frontend [WIP] 🚧\nThis project is currently a work in progress.  To track progress, please get updates "
  },
  {
    "path": "app/frontend/components.json",
    "chars": 443,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": "
  },
  {
    "path": "app/frontend/index.html",
    "chars": 326,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <link rel=\"icon\" href=\"/favicon.ico\" />\n  <meta na"
  },
  {
    "path": "app/frontend/package.json",
    "chars": 1724,
    "preview": "{\n  \"name\": \"vite-react-flow-template\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    "
  },
  {
    "path": "app/frontend/postcss.config.mjs",
    "chars": 157,
    "preview": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer:"
  },
  {
    "path": "app/frontend/src/App.tsx",
    "chars": 197,
    "preview": "import { Layout } from './components/layout';\nimport { Toaster } from './components/ui/sonner';\n\nexport default function"
  },
  {
    "path": "app/frontend/src/components/Flow.tsx",
    "chars": 9971,
    "preview": "import {\n  Background,\n  BackgroundVariant,\n  ColorMode,\n  Connection,\n  Edge,\n  EdgeChange,\n  MarkerType,\n  NodeChange,"
  },
  {
    "path": "app/frontend/src/components/Layout.tsx",
    "chars": 6767,
    "preview": "import { BottomPanel } from '@/components/panels/bottom/bottom-panel';\nimport { LeftSidebar } from '@/components/panels/"
  },
  {
    "path": "app/frontend/src/components/custom-controls.tsx",
    "chars": 662,
    "preview": "import { ResetIcon } from '@radix-ui/react-icons';\nimport { ControlButton, Controls } from '@xyflow/react';\n\ntype Custom"
  },
  {
    "path": "app/frontend/src/components/layout/top-bar.tsx",
    "chars": 2496,
    "preview": "import { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\nimport { PanelBottom, PanelLeft, Pane"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/bottom-panel.tsx",
    "chars": 3008,
    "preview": "import { useLayoutContext } from '@/contexts/layout-context';\nimport { useResizable } from '@/hooks/use-resizable';\nimpo"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/backtest-output.tsx",
    "chars": 16856,
    "preview": "import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Table, TableBody, TableCell, T"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/debug-console-tab.tsx",
    "chars": 387,
    "preview": "interface DebugConsoleTabProps {\n  className?: string;\n}\n\nexport function DebugConsoleTab({ className }: DebugConsoleTab"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/index.ts",
    "chars": 310,
    "preview": "export { DebugConsoleTab } from '@/components/panels/bottom/tabs/debug-console-tab';\nexport { OutputTab } from '@/compon"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/output-tab-utils.ts",
    "chars": 3381,
    "preview": "import { CheckCircle, Clock, MoreHorizontal, XCircle } from 'lucide-react';\n\n// Helper function to detect if content is "
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/output-tab.tsx",
    "chars": 2094,
    "preview": "import { useFlowContext } from '@/contexts/flow-context';\nimport { useNodeContext } from '@/contexts/node-context';\nimpo"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/problems-tab.tsx",
    "chars": 370,
    "preview": "interface ProblemsTabProps {\n  className?: string;\n}\n\nexport function ProblemsTab({ className }: ProblemsTabProps) {\n  r"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/reasoning-content.tsx",
    "chars": 1768,
    "preview": "import { Copy } from 'lucide-react';\nimport { useState } from 'react';\nimport { isJsonString } from './output-tab-utils'"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/regular-output.tsx",
    "chars": 9010,
    "preview": "import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Table, TableBody, TableCell, T"
  },
  {
    "path": "app/frontend/src/components/panels/bottom/tabs/terminal-tab.tsx",
    "chars": 695,
    "preview": "interface TerminalTabProps {\n  className?: string;\n}\n\nexport function TerminalTab({ className }: TerminalTabProps) {\n  r"
  },
  {
    "path": "app/frontend/src/components/panels/left/flow-actions.tsx",
    "chars": 1298,
    "preview": "import { Button } from '@/components/ui/button';\nimport { useFlowContext } from '@/contexts/flow-context';\nimport { cn }"
  },
  {
    "path": "app/frontend/src/components/panels/left/flow-context-menu.tsx",
    "chars": 2522,
    "preview": "import { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\nimport { Copy, Edit, Trash2 } from 'l"
  },
  {
    "path": "app/frontend/src/components/panels/left/flow-create-dialog.tsx",
    "chars": 3756,
    "preview": "import { Button } from '@/components/ui/button';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    Dial"
  },
  {
    "path": "app/frontend/src/components/panels/left/flow-edit-dialog.tsx",
    "chars": 3918,
    "preview": "import { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter"
  },
  {
    "path": "app/frontend/src/components/panels/left/flow-item-group.tsx",
    "chars": 1651,
    "preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';\nimport { Separator } from"
  },
  {
    "path": "app/frontend/src/components/panels/left/flow-item.tsx",
    "chars": 6156,
    "preview": "import { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { useFlowConnecti"
  },
  {
    "path": "app/frontend/src/components/panels/left/flow-list.tsx",
    "chars": 3418,
    "preview": "import { FlowItemGroup } from '@/components/panels/left/flow-item-group';\nimport { SearchBox } from '@/components/panels"
  },
  {
    "path": "app/frontend/src/components/panels/left/left-sidebar.tsx",
    "chars": 2629,
    "preview": "import { useFlowManagementTabs } from '@/hooks/use-flow-management-tabs';\nimport { useResizable } from '@/hooks/use-resi"
  },
  {
    "path": "app/frontend/src/components/panels/right/component-actions.tsx",
    "chars": 712,
    "preview": "\ninterface ComponentActionsProps {\n}\n\nexport function ComponentActions({ }: ComponentActionsProps) {\n  return (\n    <div"
  },
  {
    "path": "app/frontend/src/components/panels/right/component-item-group.tsx",
    "chars": 1590,
    "preview": "import ComponentItem from '@/components/panels/right/component-item';\nimport { AccordionContent, AccordionItem, Accordio"
  },
  {
    "path": "app/frontend/src/components/panels/right/component-item.tsx",
    "chars": 1861,
    "preview": "import { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { LucideIcon, Plus } from \"luc"
  },
  {
    "path": "app/frontend/src/components/panels/right/component-list.tsx",
    "chars": 2030,
    "preview": "import { Accordion } from '@/components/ui/accordion';\nimport { ComponentGroup } from '@/data/sidebar-components';\nimpor"
  },
  {
    "path": "app/frontend/src/components/panels/right/right-sidebar.tsx",
    "chars": 2659,
    "preview": "import { ComponentGroup, getComponentGroups } from '@/data/sidebar-components';\nimport { useComponentGroups } from '@/ho"
  },
  {
    "path": "app/frontend/src/components/panels/search-box.tsx",
    "chars": 1450,
    "preview": "import { Button } from '@/components/ui/button';\nimport { Search } from 'lucide-react';\n\ninterface SearchBoxProps {\n  va"
  },
  {
    "path": "app/frontend/src/components/settings/api-keys.tsx",
    "chars": 10129,
    "preview": "import { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/"
  },
  {
    "path": "app/frontend/src/components/settings/appearance.tsx",
    "chars": 2620,
    "preview": "import { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/"
  },
  {
    "path": "app/frontend/src/components/settings/index.ts",
    "chars": 224,
    "preview": "export { ApiKeysSettings } from './api-keys';\nexport { ThemeSettings } from './appearance';\nexport { Models } from './mo"
  },
  {
    "path": "app/frontend/src/components/settings/models/cloud.tsx",
    "chars": 4262,
    "preview": "import { Badge } from '@/components/ui/badge';\nimport { cn } from '@/lib/utils';\nimport { Cloud, RefreshCw } from 'lucid"
  },
  {
    "path": "app/frontend/src/components/settings/models/ollama.tsx",
    "chars": 36579,
    "preview": "import { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Dialog, DialogC"
  },
  {
    "path": "app/frontend/src/components/settings/models.tsx",
    "chars": 2831,
    "preview": "import { cn } from '@/lib/utils';\nimport { Cloud, Server } from 'lucide-react';\nimport { useState } from 'react';\nimport"
  },
  {
    "path": "app/frontend/src/components/settings/settings.tsx",
    "chars": 2735,
    "preview": "import { cn } from '@/lib/utils';\nimport { CubeIcon } from '@radix-ui/react-icons';\nimport { Key, Palette } from 'lucide"
  },
  {
    "path": "app/frontend/src/components/tabs/flow-tab-content.tsx",
    "chars": 3161,
    "preview": "import { Flow } from '@/components/Flow';\nimport { useFlowContext } from '@/contexts/flow-context';\nimport { useTabsCont"
  },
  {
    "path": "app/frontend/src/components/tabs/tab-bar.tsx",
    "chars": 6169,
    "preview": "import { Button } from '@/components/ui/button';\nimport { useTabsContext } from '@/contexts/tabs-context';\nimport { cn }"
  },
  {
    "path": "app/frontend/src/components/tabs/tab-content.tsx",
    "chars": 2672,
    "preview": "import { useTabsContext } from '@/contexts/tabs-context';\nimport { cn } from '@/lib/utils';\nimport { TabService } from '"
  },
  {
    "path": "app/frontend/src/components/ui/accordion.tsx",
    "chars": 2001,
    "preview": "import * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDown } fr"
  },
  {
    "path": "app/frontend/src/components/ui/badge.tsx",
    "chars": 1176,
    "preview": "import { cva, type VariantProps } from \"class-variance-authority\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/"
  },
  {
    "path": "app/frontend/src/components/ui/button.tsx",
    "chars": 1902,
    "preview": "import { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * a"
  },
  {
    "path": "app/frontend/src/components/ui/card.tsx",
    "chars": 1829,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  Rea"
  },
  {
    "path": "app/frontend/src/components/ui/checkbox.tsx",
    "chars": 1012,
    "preview": "import * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"luci"
  },
  {
    "path": "app/frontend/src/components/ui/command.tsx",
    "chars": 4860,
    "preview": "import { type DialogProps } from \"@radix-ui/react-dialog\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { Se"
  },
  {
    "path": "app/frontend/src/components/ui/dialog.tsx",
    "chars": 3818,
    "preview": "import * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react\"\nimport * as React from \"react"
  },
  {
    "path": "app/frontend/src/components/ui/input.tsx",
    "chars": 778,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Input = React.forwardRef<HTMLInputElement, React"
  },
  {
    "path": "app/frontend/src/components/ui/llm-selector.tsx",
    "chars": 3324,
    "preview": "import { ChevronsUpDown } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { Badge } from \"@/components/ui/bad"
  },
  {
    "path": "app/frontend/src/components/ui/popover.tsx",
    "chars": 1244,
    "preview": "\"use client\"\n\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport * as React from \"react\"\n\nimport { cn } "
  },
  {
    "path": "app/frontend/src/components/ui/resizable.tsx",
    "chars": 1709,
    "preview": "import { GripVertical } from \"lucide-react\"\nimport * as ResizablePrimitive from \"react-resizable-panels\"\n\nimport { cn } "
  },
  {
    "path": "app/frontend/src/components/ui/separator.tsx",
    "chars": 756,
    "preview": "import * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/li"
  },
  {
    "path": "app/frontend/src/components/ui/sheet.tsx",
    "chars": 4280,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type"
  },
  {
    "path": "app/frontend/src/components/ui/sidebar.tsx",
    "chars": 23567,
    "preview": "import { Slot } from \"@radix-ui/react-slot\"\nimport { VariantProps, cva } from \"class-variance-authority\"\nimport { PanelL"
  },
  {
    "path": "app/frontend/src/components/ui/skeleton.tsx",
    "chars": 266,
    "preview": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {"
  },
  {
    "path": "app/frontend/src/components/ui/sonner.tsx",
    "chars": 961,
    "preview": "import { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentPr"
  },
  {
    "path": "app/frontend/src/components/ui/table.tsx",
    "chars": 2859,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n  "
  },
  {
    "path": "app/frontend/src/components/ui/tabs.tsx",
    "chars": 1877,
    "preview": "import * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n"
  },
  {
    "path": "app/frontend/src/components/ui/tooltip.tsx",
    "chars": 1253,
    "preview": "import * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/ut"
  },
  {
    "path": "app/frontend/src/contexts/flow-context.tsx",
    "chars": 13287,
    "preview": "import { getMultiNodeDefinition, isMultiNodeComponent } from '@/data/multi-node-mappings';\nimport { getNodeTypeDefinitio"
  },
  {
    "path": "app/frontend/src/contexts/layout-context.tsx",
    "chars": 1737,
    "preview": "import { SidebarStorageService } from '@/services/sidebar-storage';\nimport { createContext, ReactNode, useContext, useEf"
  },
  {
    "path": "app/frontend/src/contexts/node-context.tsx",
    "chars": 14253,
    "preview": "import { LanguageModel } from '@/data/models';\nimport { createContext, ReactNode, useCallback, useContext, useState } fr"
  },
  {
    "path": "app/frontend/src/contexts/tabs-context.tsx",
    "chars": 8317,
    "preview": "import { Flow } from '@/types/flow';\nimport { createContext, ReactNode, useCallback, useContext, useEffect, useState } f"
  },
  {
    "path": "app/frontend/src/data/agents.ts",
    "chars": 671,
    "preview": "import { api } from '@/services/api';\n\nexport interface Agent {\n  key: string;\n  display_name: string;\n  description: st"
  },
  {
    "path": "app/frontend/src/data/models.ts",
    "chars": 1117,
    "preview": "import { api } from '@/services/api';\n\nexport interface LanguageModel {\n  display_name: string;\n  model_name: string;\n  "
  },
  {
    "path": "app/frontend/src/data/multi-node-mappings.ts",
    "chars": 3212,
    "preview": "export interface MultiNodeDefinition {\n  name: string;\n  nodes: {\n    componentName: string;\n    offsetX: number;\n    of"
  },
  {
    "path": "app/frontend/src/data/node-mappings.ts",
    "chars": 4746,
    "preview": "import { AppNode } from \"@/nodes/types\";\nimport { Agent, getAgents } from \"./agents\";\n\n// Map of sidebar item names to n"
  },
  {
    "path": "app/frontend/src/data/sidebar-components.ts",
    "chars": 1594,
    "preview": "import {\n  BadgeDollarSign,\n  Bot,\n  Brain,\n  Calculator,\n  ChartLine,\n  ChartPie,\n  LucideIcon,\n  Network,\n  Play,\n  Za"
  },
  {
    "path": "app/frontend/src/edges/index.ts",
    "chars": 138,
    "preview": "import type { EdgeTypes } from '@xyflow/react';\n\n\nexport const edgeTypes = {\n  // Add your custom edge types here!\n} sat"
  },
  {
    "path": "app/frontend/src/hooks/use-component-groups.ts",
    "chars": 2410,
    "preview": "import { ComponentGroup } from '@/data/sidebar-components';\nimport { useEffect, useMemo, useState } from 'react';\n\nexpor"
  },
  {
    "path": "app/frontend/src/hooks/use-enhanced-flow-actions.ts",
    "chars": 4297,
    "preview": "import { useFlowContext } from '@/contexts/flow-context';\nimport { useNodeContext } from '@/contexts/node-context';\nimpo"
  },
  {
    "path": "app/frontend/src/hooks/use-flow-connection.ts",
    "chars": 8185,
    "preview": "import { useNodeContext } from '@/contexts/node-context';\nimport { api } from '@/services/api';\nimport { backtestApi } f"
  },
  {
    "path": "app/frontend/src/hooks/use-flow-history.ts",
    "chars": 5815,
    "preview": "import { Edge, Node, useReactFlow } from '@xyflow/react';\nimport { useCallback, useRef, useState } from 'react';\n\ninterf"
  },
  {
    "path": "app/frontend/src/hooks/use-flow-management-tabs.ts",
    "chars": 11455,
    "preview": "import { useFlowContext } from '@/contexts/flow-context';\nimport { useNodeContext } from '@/contexts/node-context';\nimpo"
  },
  {
    "path": "app/frontend/src/hooks/use-flow-management.ts",
    "chars": 12174,
    "preview": "import { useFlowContext } from '@/contexts/flow-context';\nimport { useNodeContext } from '@/contexts/node-context';\nimpo"
  },
  {
    "path": "app/frontend/src/hooks/use-keyboard-shortcuts.ts",
    "chars": 4689,
    "preview": "import { useEffect } from 'react';\n\ninterface KeyboardShortcut {\n  key: string;\n  ctrlKey?: boolean;\n  metaKey?: boolean"
  },
  {
    "path": "app/frontend/src/hooks/use-mobile.tsx",
    "chars": 565,
    "preview": "import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n  const [isMobile, setIsM"
  },
  {
    "path": "app/frontend/src/hooks/use-node-state.ts",
    "chars": 8806,
    "preview": "import { useCallback, useEffect, useState } from 'react';\n\n// =========================================================="
  },
  {
    "path": "app/frontend/src/hooks/use-output-node-connection.ts",
    "chars": 2103,
    "preview": "import { getConnectedEdges, useReactFlow } from '@xyflow/react';\nimport { useMemo } from 'react';\n\nimport { useFlowConte"
  },
  {
    "path": "app/frontend/src/hooks/use-resizable.ts",
    "chars": 2824,
    "preview": "import { useEffect, useRef, useState } from 'react';\n\ninterface UseResizableOptions {\n  minWidth?: number;\n  maxWidth?: "
  },
  {
    "path": "app/frontend/src/hooks/use-toast-manager.ts",
    "chars": 3577,
    "preview": "import { useCallback, useState } from 'react';\nimport { toast } from 'sonner';\n\ntype ToastType = 'success' | 'error' | '"
  },
  {
    "path": "app/frontend/src/index.css",
    "chars": 8404,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 94%;\n    --fo"
  },
  {
    "path": "app/frontend/src/lib/utils.ts",
    "chars": 1389,
    "preview": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: C"
  },
  {
    "path": "app/frontend/src/main.tsx",
    "chars": 442,
    "preview": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\n\nimport App from './App';\nimport { NodeProvider } fr"
  },
  {
    "path": "app/frontend/src/nodes/components/agent-node.tsx",
    "chars": 5282,
    "preview": "import { type NodeProps } from '@xyflow/react';\nimport { Bot } from 'lucide-react';\nimport { useEffect, useState } from "
  },
  {
    "path": "app/frontend/src/nodes/components/agent-output-dialog.tsx",
    "chars": 11137,
    "preview": "import {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '@/components/ui/dialog';\nimp"
  },
  {
    "path": "app/frontend/src/nodes/components/investment-report-dialog.tsx",
    "chars": 9274,
    "preview": "import {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from '@/components/ui/accordion';\nimpor"
  },
  {
    "path": "app/frontend/src/nodes/components/investment-report-node.tsx",
    "chars": 2543,
    "preview": "import { type NodeProps } from '@xyflow/react';\nimport { FileText } from 'lucide-react';\nimport { useState } from 'react"
  },
  {
    "path": "app/frontend/src/nodes/components/json-output-dialog.tsx",
    "chars": 4164,
    "preview": "import { Copy, Download } from 'lucide-react';\nimport { useState } from 'react';\nimport { Prism as SyntaxHighlighter } f"
  },
  {
    "path": "app/frontend/src/nodes/components/json-output-node.tsx",
    "chars": 4305,
    "preview": "import { type NodeProps } from '@xyflow/react';\nimport { FileJson } from 'lucide-react';\nimport { useEffect, useState } "
  },
  {
    "path": "app/frontend/src/nodes/components/node-shell.tsx",
    "chars": 2921,
    "preview": "import { Card, CardHeader } from '@/components/ui/card';\nimport { cn } from '@/lib/utils';\nimport { Handle, Position } f"
  },
  {
    "path": "app/frontend/src/nodes/components/output-node-status.tsx",
    "chars": 2176,
    "preview": "import { cn } from '@/lib/utils';\nimport { getStatusColor } from '../utils';\n\ninterface OutputNodeStatusProps {\n  isProc"
  },
  {
    "path": "app/frontend/src/nodes/components/portfolio-manager-node.tsx",
    "chars": 5313,
    "preview": "import { type NodeProps } from '@xyflow/react';\nimport { Brain } from 'lucide-react';\nimport { useEffect, useState } fro"
  },
  {
    "path": "app/frontend/src/nodes/components/portfolio-start-node.tsx",
    "chars": 17265,
    "preview": "import { useReactFlow, type NodeProps } from '@xyflow/react';\nimport { ChevronDown, PieChart, Play, Plus, Square, X } fr"
  },
  {
    "path": "app/frontend/src/nodes/components/stock-analyzer-node.tsx",
    "chars": 16136,
    "preview": "import { useReactFlow, type NodeProps } from '@xyflow/react';\nimport { ChartLine, ChevronDown, Play, Square } from 'luci"
  },
  {
    "path": "app/frontend/src/nodes/index.ts",
    "chars": 1753,
    "preview": "import { Edge, type NodeTypes } from '@xyflow/react';\n\nimport { AgentNode } from './components/agent-node';\nimport { Inv"
  },
  {
    "path": "app/frontend/src/nodes/types.ts",
    "chars": 990,
    "preview": "import { MessageItem } from '@/contexts/node-context';\nimport type { BuiltInNode, Node } from '@xyflow/react';\n\nexport t"
  },
  {
    "path": "app/frontend/src/nodes/utils.ts",
    "chars": 2346,
    "preview": "import { type Edge, type Node, getConnectedEdges } from '@xyflow/react';\n\nexport type NodeStatus = 'IDLE' | 'IN_PROGRESS"
  },
  {
    "path": "app/frontend/src/providers/theme-provider.tsx",
    "chars": 420,
    "preview": "import { ThemeProvider as NextThemesProvider } from 'next-themes';\nimport { ReactNode } from 'react';\n\ninterface ThemePr"
  },
  {
    "path": "app/frontend/src/services/api-keys-api.ts",
    "chars": 4301,
    "preview": "const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';\n\nexport interface ApiKey {\n  id: number;\n "
  },
  {
    "path": "app/frontend/src/services/api.ts",
    "chars": 11723,
    "preview": "import { NodeStatus, OutputNodeData, useNodeContext } from '@/contexts/node-context';\nimport { Agent } from '@/data/agen"
  },
  {
    "path": "app/frontend/src/services/backtest-api.ts",
    "chars": 12083,
    "preview": "import { NodeStatus, useNodeContext } from '@/contexts/node-context';\nimport { extractBaseAgentKey } from '@/data/node-m"
  },
  {
    "path": "app/frontend/src/services/flow-service.ts",
    "chars": 2725,
    "preview": "import { Flow } from '@/types/flow';\n\nconst API_BASE_URL = 'http://localhost:8000';\n\nexport interface CreateFlowRequest "
  },
  {
    "path": "app/frontend/src/services/sidebar-storage.ts",
    "chars": 7017,
    "preview": "export interface SidebarStates {\n  leftCollapsed: boolean;\n  rightCollapsed: boolean;\n  bottomCollapsed: boolean;\n}\n\nexp"
  },
  {
    "path": "app/frontend/src/services/tab-service.ts",
    "chars": 2016,
    "preview": "import { Settings } from '@/components/settings/settings';\nimport { FlowTabContent } from '@/components/tabs/flow-tab-co"
  },
  {
    "path": "app/frontend/src/services/types.ts",
    "chars": 1884,
    "preview": "// Shared types for API requests and responses\nexport enum ModelProvider {\n  OPENAI = 'OpenAI',\n  ANTHROPIC = 'Anthropic"
  },
  {
    "path": "app/frontend/src/types/flow.ts",
    "chars": 228,
    "preview": "export interface Flow {\n  id: number;\n  name: string;\n  description?: string;\n  nodes: any;\n  edges: any;\n  viewport?: a"
  },
  {
    "path": "app/frontend/src/utils/date-utils.ts",
    "chars": 915,
    "preview": "/**\n * Formats an ISO timestamp to local browser time (HH:MM:SS.ms)\n * @param timestamp ISO timestamp string\n * @returns"
  },
  {
    "path": "app/frontend/src/utils/text-utils.ts",
    "chars": 9859,
    "preview": "import { extractBaseAgentKey } from '@/data/node-mappings';\n\n/**\n * Splits text into smaller paragraphs for better reada"
  },
  {
    "path": "app/frontend/src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "app/frontend/tailwind.config.ts",
    "chars": 3554,
    "preview": "import tailwindcssTypography from '@tailwindcss/typography';\nimport type { Config } from 'tailwindcss';\nimport tailwindc"
  },
  {
    "path": "app/frontend/tsconfig.json",
    "chars": 756,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\n      \"ES2020\",\n     "
  },
  {
    "path": "app/frontend/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "app/frontend/vite.config.ts",
    "chars": 270,
    "preview": "import react from '@vitejs/plugin-react'\nimport path from 'path'\nimport { defineConfig } from 'vite'\n\n// https://vitejs."
  },
  {
    "path": "app/run.bat",
    "chars": 9165,
    "preview": "@echo off\nREM AI Hedge Fund Web Application Setup and Runner (Windows)\nREM This script makes it easy for non-technical u"
  },
  {
    "path": "app/run.sh",
    "chars": 11939,
    "preview": "#!/bin/bash\n\n# AI Hedge Fund Web Application Setup and Runner\n# This script makes it easy for non-technical users to run"
  },
  {
    "path": "docker/.dockerignore",
    "chars": 225,
    "preview": "# Git\n.git\n.gitignore\n\n# Poetry\n.venv\n__pycache__/\n*.py[cod]\n*$py.class\n.pytest_cache/\n\n# Environment\n.env\n\n# IDEs and e"
  },
  {
    "path": "docker/Dockerfile",
    "chars": 526,
    "preview": "FROM python:3.11-slim\n\nWORKDIR /app\n\n# Set PYTHONPATH to include the app directory\nENV PYTHONPATH=/app\n\n# Install Poetry"
  },
  {
    "path": "docker/README.md",
    "chars": 7843,
    "preview": "# AI Hedge Fund\n\nThis is a proof of concept for an AI-powered hedge fund.  The goal of this project is to explore the us"
  },
  {
    "path": "docker/docker-compose.yml",
    "chars": 2289,
    "preview": "services:\n  ollama:\n    profiles:\n      - embedded-ollama\n    image: ollama/ollama:latest\n    container_name: ollama\n   "
  },
  {
    "path": "docker/run.bat",
    "chars": 12015,
    "preview": "@echo off\nsetlocal enabledelayedexpansion\n\n:: Default values\nset TICKER=AAPL,MSFT,NVDA\nset USE_OLLAMA=\nset \"OLLAMA_BASE_"
  },
  {
    "path": "docker/run.sh",
    "chars": 11382,
    "preview": "#!/bin/bash\n\n# Help text to display when --help is provided\nshow_help() {\n  echo \"AI Hedge Fund Docker Runner\"\n  echo \"\""
  },
  {
    "path": "pyproject.toml",
    "chars": 1384,
    "preview": "[tool.poetry]\nname = \"ai-hedge-fund\"\nversion = \"1.0.0\"\ndescription = \"An AI-powered hedge fund that uses multiple agents"
  },
  {
    "path": "src/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/agents/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/agents/aswath_damodaran.py",
    "chars": 14973,
    "preview": "from __future__ import annotations\n\nimport json\nfrom typing_extensions import Literal\nfrom pydantic import BaseModel\n\nfr"
  },
  {
    "path": "src/agents/ben_graham.py",
    "chars": 15571,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import get_financial_metrics, get_market"
  },
  {
    "path": "src/agents/bill_ackman.py",
    "chars": 18673,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import get_financial_metrics, get_market"
  },
  {
    "path": "src/agents/cathie_wood.py",
    "chars": 20484,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import get_financial_metrics, get_market"
  },
  {
    "path": "src/agents/charlie_munger.py",
    "chars": 38299,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import get_financial_metrics, get_market"
  },
  {
    "path": "src/agents/fundamentals.py",
    "chars": 6980,
    "preview": "from langchain_core.messages import HumanMessage\nfrom src.graph.state import AgentState, show_agent_reasoning\nfrom src.u"
  },
  {
    "path": "src/agents/growth_agent.py",
    "chars": 10058,
    "preview": "from __future__ import annotations\n\n\"\"\"Growth Agent\n\nImplements a growth-focused valuation methodology.\n\"\"\"\n\nimport json"
  },
  {
    "path": "src/agents/michael_burry.py",
    "chars": 14489,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime, timedelta\nimport json\nfrom typing_extensions import L"
  },
  {
    "path": "src/agents/mohnish_pabrai.py",
    "chars": 14715,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import get_financial_metrics, get_market"
  },
  {
    "path": "src/agents/news_sentiment.py",
    "chars": 8863,
    "preview": "\n\nfrom langchain_core.messages import HumanMessage\nfrom pydantic import BaseModel, Field\nfrom src.data.models import Com"
  },
  {
    "path": "src/agents/peter_lynch.py",
    "chars": 19670,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import (\n    get_market_cap,\n    search_"
  },
  {
    "path": "src/agents/phil_fisher.py",
    "chars": 24851,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import (\n    get_market_cap,\n    search_"
  },
  {
    "path": "src/agents/portfolio_manager.py",
    "chars": 9885,
    "preview": "import json\nimport time\nfrom langchain_core.messages import HumanMessage\nfrom langchain_core.prompts import ChatPromptTe"
  },
  {
    "path": "src/agents/rakesh_jhunjhunwala.py",
    "chars": 29997,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom "
  },
  {
    "path": "src/agents/risk_manager.py",
    "chars": 13891,
    "preview": "from langchain_core.messages import HumanMessage\nfrom src.graph.state import AgentState, show_agent_reasoning\nfrom src.u"
  },
  {
    "path": "src/agents/sentiment.py",
    "chars": 6259,
    "preview": "from langchain_core.messages import HumanMessage\nfrom src.graph.state import AgentState, show_agent_reasoning\nfrom src.u"
  },
  {
    "path": "src/agents/stanley_druckenmiller.py",
    "chars": 24743,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom src.tools.api import (\n    get_financial_metrics,\n    "
  },
  {
    "path": "src/agents/technicals.py",
    "chars": 17320,
    "preview": "import math\n\nfrom langchain_core.messages import HumanMessage\n\nfrom src.graph.state import AgentState, show_agent_reason"
  },
  {
    "path": "src/agents/valuation.py",
    "chars": 17941,
    "preview": "from __future__ import annotations\n\n\"\"\"Valuation Agent\n\nImplements four complementary valuation methodologies and aggreg"
  },
  {
    "path": "src/agents/warren_buffett.py",
    "chars": 35976,
    "preview": "from src.graph.state import AgentState, show_agent_reasoning\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom "
  },
  {
    "path": "src/backtester.py",
    "chars": 2545,
    "preview": "import sys\n\nfrom colorama import Fore, Style\n\nfrom src.main import run_hedge_fund\nfrom src.backtesting.engine import Bac"
  },
  {
    "path": "src/backtesting/__init__.py",
    "chars": 1305,
    "preview": "\"\"\"Backtesting package: interfaces and shared types for refactoring.\n\nThis module defines the public contracts (Protocol"
  },
  {
    "path": "src/backtesting/benchmarks.py",
    "chars": 1105,
    "preview": "from __future__ import annotations\n\nimport pandas as pd\n\nfrom src.tools.api import get_price_data\n\n\nclass BenchmarkCalcu"
  }
]

// ... and 58 more files (download for full content)

About this extraction

This page contains the full source code of the virattt/ai-hedge-fund GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 258 files (1.5 MB), approximately 386.7k tokens, and a symbol index with 854 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!