Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Poetry
run: pip install poetry
- name: Install deps
run: poetry install --no-interaction --no-root
- name: Run equity tests
run: pytest -q
- name: Run crypto tests
env:
ASSET_CLASS: CRYPTO
run: pytest -q
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ COPY pyproject.toml poetry.lock* /app/

# Configure Poetry to not use a virtual environment
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
&& poetry install --no-interaction --no-ansi --no-root \
&& pip install ccxt pycoingecko

# Copy rest of the source code
COPY . /app/
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ poetry install
cp .env.example .env
```

## 🚀 Crypto Quick-Start

Enable crypto trading by setting the `ASSET_CLASS` environment variable:

```bash
ASSET_CLASS=CRYPTO python src/main.py --pair BTC/USDT --exchange binance
```

Set `ALLOW_MARGIN=1` to enable margin trading.

### Environment Variables

| Variable | Description | Default |
|---|---|---|
| `ASSET_CLASS` | `EQUITY` or `CRYPTO` | `EQUITY` |
| `ALLOW_MARGIN` | Enable shorting/margin when set to `1` | `0` |

4. Set your API keys:
```bash
# For running LLMs hosted by openai (gpt-4o, gpt-4o-mini, etc.)
Expand Down
8 changes: 6 additions & 2 deletions app/backend/models/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import datetime, timedelta
from pydantic import BaseModel, Field
from typing import List, Optional

from pydantic import BaseModel, Field

from src.llm.models import ModelProvider


Expand All @@ -15,7 +17,9 @@ class ErrorResponse(BaseModel):


class HedgeFundRequest(BaseModel):
tickers: List[str]
pairs: List[str] = Field(..., example=["BTC/USDT"])
exchange: str = Field("binance", example="binance")
tickers: Optional[List[str]] = Field(None, deprecated=True)
selected_agents: List[str]
end_date: Optional[str] = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d"))
start_date: Optional[str] = None
Expand Down
19 changes: 14 additions & 5 deletions app/backend/routes/hedge_fund.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import asyncio

from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
import asyncio

from app.backend.models.events import (
CompleteEvent,
ErrorEvent,
ProgressUpdateEvent,
StartEvent,
)
from app.backend.models.schemas import ErrorResponse, HedgeFundRequest
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.graph import create_graph, run_graph_async
from src.utils.parsing import parse_hedge_fund_response
from app.backend.services.portfolio import create_portfolio
from src.utils.progress import progress

Expand All @@ -22,7 +29,8 @@
async def run_hedge_fund(request: HedgeFundRequest):
try:
# Create the portfolio
portfolio = create_portfolio(request.initial_cash, request.margin_requirement, request.tickers)
symbols = request.pairs or request.tickers
portfolio = create_portfolio(request.initial_cash, request.margin_requirement, symbols)

# Construct agent graph
graph = create_graph(request.selected_agents)
Expand Down Expand Up @@ -55,11 +63,12 @@ def progress_handler(agent_name, ticker, status, analysis, timestamp):
run_graph_async(
graph=graph,
portfolio=portfolio,
tickers=request.tickers,
tickers=symbols,
start_date=request.start_date,
end_date=request.end_date,
model_name=request.model_name,
model_provider=model_provider,
exchange=request.exchange,
)
)
# Send initial message
Expand Down
29 changes: 11 additions & 18 deletions app/backend/services/graph.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import asyncio
import json

from src.utils.parsing import parse_hedge_fund_response

from langchain_core.messages import HumanMessage
from langgraph.graph import END, StateGraph

from src.agents.portfolio_manager import portfolio_management_agent
from src.agents.risk_manager import risk_management_agent
from src.graph.state import AgentState
from src.main import start
from src.utils.analysts import ANALYST_CONFIG
from src.graph.state import AgentState


# Helper function to create the agent graph
Expand Down Expand Up @@ -48,12 +51,15 @@ def create_graph(selected_agents: list[str]) -> StateGraph:
return graph


async def run_graph_async(graph, portfolio, tickers, start_date, end_date, model_name, model_provider):
async def run_graph_async(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, exchange=""):
"""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)) # Use default executor
result = await loop.run_in_executor(
None,
lambda: run_graph(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, exchange),
)
return result


Expand All @@ -65,6 +71,7 @@ def run_graph(
end_date: str,
model_name: str,
model_provider: str,
exchange: str = "",
) -> dict:
"""
Run the graph with the given portfolio, tickers,
Expand All @@ -84,6 +91,7 @@ def run_graph(
"start_date": start_date,
"end_date": end_date,
"analyst_signals": {},
"exchange": exchange,
},
"metadata": {
"show_reasoning": False,
Expand All @@ -92,18 +100,3 @@ def run_graph(
},
},
)


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
5 changes: 2 additions & 3 deletions app/backend/services/portfolio.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@


def create_portfolio(initial_cash: float, margin_requirement: float, tickers: list[str]) -> dict:
return {
return {
"cash": initial_cash, # Initial cash amount
"margin_requirement": margin_requirement, # Initial margin requirement
"margin_used": 0.0, # total margin usage across all short positions
Expand All @@ -22,4 +21,4 @@ def create_portfolio(initial_cash: float, margin_requirement: float, tickers: li
}
for ticker in tickers
},
}
}
2 changes: 1 addition & 1 deletion app/frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="dark">

<head>
<meta charset="UTF-8" />
Expand Down
35 changes: 35 additions & 0 deletions app/frontend/src/components/PairSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';

interface PairSelectorProps {
value: string;
onChange: (pair: string) => void;
}

export function PairSelector({ value, onChange }: PairSelectorProps) {
const [pairs, setPairs] = useState<string[]>([]);

useEffect(() => {
async function fetchPairs() {
try {
const res = await fetch(
'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1'
);
const data = await res.json();
setPairs(data.map((c: any) => `${c.symbol.toUpperCase()}/USDT`));
} catch (e) {
console.error('Failed to fetch coin list', e);
}
}
fetchPairs();
}, []);

return (
<select value={value} onChange={(e) => onChange(e.target.value)} className="p-1 border rounded">
{pairs.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
);
}
4 changes: 2 additions & 2 deletions app/frontend/src/nodes/components/agent-output-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ export function AgentOutputDialog({
<div className="flex justify-between items-center mb-3">
<h3 className="font-medium text-primary">Analysis</h3>
<div className="flex items-center gap-2">
{/* Ticker selector */}
{/* Pair selector */}
{tickersWithDecisions.length > 0 && (
<div className="flex items-center gap-1">
<span className="text-xs text-muted-foreground font-medium">Ticker:</span>
<span className="text-xs text-muted-foreground font-medium">Pair:</span>
<select
className="text-xs p-1 rounded bg-background border border-border cursor-pointer"
value={selectedTicker || ''}
Expand Down
6 changes: 3 additions & 3 deletions app/frontend/src/nodes/components/text-input-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,16 @@ export function TextInputNode({
<div className="text-subtitle text-muted-foreground flex items-center gap-1">
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<span>Tickers</span>
<span>Pairs</span>
</TooltipTrigger>
<TooltipContent side="right">
You can add multiple tickers using commas (AAPL,NVDA,TSLA)
You can add multiple pairs using commas (BTC/USDT,ETH/USDT)
</TooltipContent>
</Tooltip>
</div>
<div className="flex gap-2">
<Input
placeholder="Enter tickers"
placeholder="Enter pairs"
value={tickers}
onChange={handleTickersChange}
/>
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/src/nodes/components/text-output-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function TextOutputDialog({
<Table>
<TableHeader>
<TableRow>
<TableHead>Ticker</TableHead>
<TableHead>Pair</TableHead>
<TableHead>Price</TableHead>
<TableHead>Action</TableHead>
<TableHead>Quantity</TableHead>
Expand Down
16 changes: 16 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,21 @@ services:
tty: true
stdin_open: true

hedge-fund-crypto:
build: .
image: ai-hedge-fund
depends_on:
- ollama
volumes:
- ./.env:/app/.env
command: python src/main.py --pair BTC/USDT --exchange binance
environment:
- PYTHONUNBUFFERED=1
- OLLAMA_BASE_URL=http://ollama:11434
- PYTHONPATH=/app
- ASSET_CLASS=CRYPTO
tty: true
stdin_open: true

volumes:
ollama_data:
15 changes: 15 additions & 0 deletions docs/adr/0002-enable-crypto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 0002 - Enable Crypto Asset Class

## Context

The platform originally supported only equity trading using daily OHLCV data. Expanding to spot crypto pairs requires new data sources, risk constraints, and agent logic.

## Decision

Introduce a feature flag `ASSET_CLASS` defaulting to `EQUITY`. When set to `CRYPTO`, the system routes price data through CCXT, adds crypto specific analysts, and adjusts risk management.

## Consequences

- Maintains backward compatibility with equity workflows.
- Adds dependencies on `ccxt` and `pycoingecko`.
- CI runs test suites for both asset classes.
Loading