20 KiB
CLAUDE.md — AgentForge x Ghostfolio
Project Overview
This is AgentForge x Ghostfolio — an AI-powered financial agent layer being built on top of Ghostfolio, an open-source wealth management application. The goal is to add a conversational AI assistant that can analyze portfolios, provide market insights, execute trades, assess risk, and help users manage their finances through natural language.
What Ghostfolio Is (The Foundation)
Ghostfolio is a privacy-first, open-source personal finance dashboard for tracking stocks, ETFs, funds, and cryptocurrencies across multiple accounts and platforms. It provides:
- Multi-account portfolio tracking with performance analytics (ROAI, ROI, TWR, MWR)
- Risk analysis via portfolio rules (cluster risk, currency risk, fee ratios, etc.)
- Multi-currency support with automatic exchange rate conversion
- Multiple data providers (Yahoo Finance, CoinGecko, Alpha Vantage, EOD Historical Data, etc.)
- Import/export of transactions, public portfolio sharing, and an admin panel
What AgentForge Adds (The AI Layer)
AgentForge adds a new, independent AgentModule — separate from the existing AiModule — providing a full conversational agent:
- LangGraph TS StateGraph — 6-node pipeline:
query → plan → execute → verify → disclaim → respond - Model-agnostic LLM provider — defaults to Claude Sonnet 4 via
@langchain/anthropic, swappable to any provider - 7 tools wrapping existing Ghostfolio services (portfolio summary, performance, holdings, activities, market data, risk analysis, account overview)
- Real-time streaming — Server-Sent Events (SSE) for streaming AI responses via
POST /api/v1/agents/chat - Conversation memory — LangGraph checkpointer persisted to PostgreSQL (no custom Prisma models needed)
- LangSmith observability — tracing, latency metrics, and tool-call auditing
- Minimal chat UI — dedicated
/chatpage with message history and streaming display
Tech Stack
| Layer | Technology |
|---|---|
| Monorepo | Nx 21.x |
| Backend | NestJS 11.x (TypeScript 5.9) |
| Frontend | Angular 21.x with Angular Material |
| Database | PostgreSQL 15 via Prisma 6.x ORM |
| Cache | Redis (Bull queues for background jobs) |
| AI/LLM | LangGraph TS (@langchain/langgraph), @langchain/anthropic, LangSmith |
| Auth | JWT, Google OAuth, OIDC, WebAuthn (Passport strategies) |
| Containerization | Docker / Docker Compose |
| i18n | Angular i18n (12 languages) |
Repository Structure
ghostfolio/
├── apps/
│ ├── api/ # NestJS backend (REST API)
│ │ └── src/
│ │ ├── app/ # Feature modules (controllers + services)
│ │ │ ├── portfolio/ # Portfolio controller & service
│ │ │ ├── order/ # Order/activity controller & service
│ │ │ ├── account/ # Account controller & service
│ │ │ ├── admin/ # Admin controller
│ │ │ ├── auth/ # Auth controller + strategies
│ │ │ ├── user/ # User controller & service
│ │ │ ├── import/ # CSV/JSON import
│ │ │ ├── export/ # Portfolio export
│ │ │ └── ... # Additional feature modules
│ │ ├── services/ # Shared backend services
│ │ │ ├── data-provider/ # Multi-provider data aggregation
│ │ │ ├── exchange-rate-data/ # Currency exchange rates
│ │ │ ├── portfolio-snapshot/ # Portfolio snapshot queue
│ │ │ ├── data-gathering/ # Background data jobs
│ │ │ └── ...
│ │ ├── interceptors/ # Request/response interceptors
│ │ ├── guards/ # Auth & permission guards
│ │ └── models/rules/ # Portfolio analysis rules
│ └── client/ # Angular frontend (PWA)
│ └── src/
│ ├── app/
│ │ ├── pages/ # Route-level page components
│ │ ├── components/ # Shared page-level components
│ │ └── services/ # HTTP client services
│ ├── locales/ # i18n translation files
│ └── styles/ # Global SCSS styles
├── libs/
│ ├── common/ # Shared TypeScript types, interfaces, helpers
│ │ └── src/lib/
│ │ ├── interfaces/ # 100+ shared interface definitions
│ │ ├── types/ # Shared type aliases
│ │ └── helper/ # Utility functions
│ └── ui/ # Angular component library + Storybook
│ └── src/lib/ # Reusable UI components (tables, charts, forms)
├── prisma/
│ ├── schema.prisma # Database schema (all models defined here)
│ ├── migrations/ # Prisma migration history
│ └── seed.ts # Database seeding script
├── docker/ # Docker Compose configs (dev, build, prod)
├── .github/workflows/ # CI/CD (lint, test, build, Docker publish)
└── CLAUDE.md # This file
TypeScript Path Aliases
@ghostfolio/api/* → apps/api/src/*
@ghostfolio/client/* → apps/client/src/app/*
@ghostfolio/common/* → libs/common/src/lib/*
@ghostfolio/ui/* → libs/ui/src/lib/*
Key Commands
Development
npm run start:server # Start NestJS API with file watching
npm run start:client # Start Angular dev server (localhost:4200/en)
npm run start:storybook # Start Storybook UI component browser
Build
npm run build:production # Full production build (API + Client + Storybook)
npm run watch:server # Watch mode API build
Test
npm test # Run ALL tests (uses .env.example, 4 parallel workers)
npm run test:api # Run API tests only
npm run test:common # Run common lib tests only
npm run test:ui # Run UI lib tests only
Tests use Jest and require environment variables from .env.example (loaded via dotenv-cli).
Lint & Format
npm run lint # Lint entire codebase (ESLint)
npm run format:check # Check Prettier formatting
npm run format:write # Auto-fix formatting
Database
npm run database:setup # Push schema + seed (for initial setup)
npm run database:push # Sync Prisma schema to DB (no migration)
npm run database:migrate # Run Prisma migrations
npm run database:seed # Seed database with initial data
npm run database:gui # Open Prisma Studio (DB browser)
npm run database:generate-typings # Regenerate Prisma client types
npm run database:format-schema # Format schema.prisma
Docker (local development infrastructure)
docker-compose -f docker/docker-compose.dev.yml up # Start PostgreSQL + Redis
docker-compose -f docker/docker-compose.build.yml up # Build & run full app locally
Key Backend Services
PortfolioService (apps/api/src/app/portfolio/portfolio.service.ts)
The core service. Calculates portfolio performance (ROAI, ROI, TWR, MWR), aggregates holdings across accounts, generates risk analysis reports, handles time-range filtering (1d, WTD, MTD, YTD, 1Y, 5Y, Max), and manages portfolio snapshots.
OrderService (apps/api/src/app/order/order.service.ts)
CRUD for activities/transactions. Handles BUY, SELL, DIVIDEND, FEE, INTEREST, and LIABILITY activity types. Triggers data gathering for new assets and emits portfolio change events for cache invalidation.
AccountService (apps/api/src/app/account/account.service.ts)
Manages trading accounts and platforms. Calculates account balances, retrieves account history, and handles account-level exclusions from portfolio calculations.
DataProviderService (apps/api/src/services/data-provider/data-provider.service.ts)
Orchestrates multiple market data providers (Yahoo Finance, CoinGecko, Alpha Vantage, etc.). Fetches quotes, historical data, asset profiles, and dividends. Handles provider-specific API keys and rate limiting.
ExchangeRateDataService (apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts)
Manages currency exchange rates with caching. Initializes currency pairs on module load, fills historical gaps via forward-fill, and supports derived currencies.
Database Models (Prisma)
Key models in prisma/schema.prisma:
| Model | Purpose |
|---|---|
User |
User accounts with roles (ADMIN, USER) and auth provider |
Account |
Trading/brokerage accounts linked to platforms |
Order |
Transaction records (BUY, SELL, DIVIDEND, FEE, etc.) |
SymbolProfile |
Asset metadata (stocks, ETFs, crypto) with data source |
MarketData |
Historical price data with market state |
AccountBalance |
Historical account balance snapshots |
Tag |
Transaction/holding tags for filtering |
Access |
Portfolio sharing/access control tokens |
Subscription |
Premium subscription records |
Analytics |
User activity analytics |
ApiKey |
API key management |
AuthDevice |
WebAuthn device registration |
Platform |
Trading platform references |
Property |
Application-level key-value configuration |
Settings |
User settings (JSON) |
Key API Routes
| Method | Route | Purpose |
|---|---|---|
GET |
/api/v1/portfolio/details |
Full portfolio details |
GET |
/api/v1/portfolio/holdings |
Current holdings |
GET |
/api/v1/portfolio/performance |
Performance metrics |
GET |
/api/v1/portfolio/report |
Risk analysis report |
GET/POST/PUT/DELETE |
/api/v1/order |
Activity CRUD |
GET/POST/PUT/DELETE |
/api/v1/account |
Account CRUD |
POST |
/api/v1/import |
Import transactions |
GET |
/api/v1/export |
Export portfolio |
POST |
/api/v1/auth/anonymous |
Token-based login |
GET |
/api/v1/auth/google |
Google OAuth |
GET |
/api/v1/health |
Health check |
GET |
/api/v1/info |
System info |
GET |
/api/v1/public/:accessId/portfolio |
Public portfolio |
Existing AI Infrastructure (Reference Only)
Ghostfolio has an existing lightweight AiModule (from PR #4176). AgentForge does not extend it — we build a new, independent AgentModule. The existing module is documented here for reference only.
| Component | Location | What It Does |
|---|---|---|
AiController |
apps/api/src/app/endpoints/ai/ai.controller.ts |
GET /api/v1/ai/prompt/:mode — returns a formatted prompt string |
AiService |
apps/api/src/app/endpoints/ai/ai.service.ts |
getPrompt() builds a markdown holdings table; generateText() calls OpenRouter via Vercel AI SDK |
AiModule |
apps/api/src/app/endpoints/ai/ai.module.ts |
Imports PortfolioService, AccountService, MarketDataService, etc. |
Assistant UI |
libs/ui/src/lib/assistant/assistant.component.ts |
Search/navigation modal — not a chat UI |
We do not touch any of these files. Our AgentModule is fully independent.
AgentForge Integration Plan (v2)
Core Principle
New, independent AgentModule — does not modify or depend on the existing AiModule. Clean separation, own endpoints, own state management.
Architecture: LangGraph TS StateGraph (6 Nodes)
User message
│
▼
┌─────────┐ ┌────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐
│ Query │──▶│ Plan │──▶│ Execute │──▶│ Verify │──▶│ Disclaim │──▶│ Respond │
│ (parse) │ │(decide)│ │ (tools) │ │ (checks) │ │(caveats) │ │ (stream) │
└─────────┘ └────────┘ └───────────┘ └──────────┘ └───────────┘ └───────────┘
| Node | Purpose |
|---|---|
| Query | Parse user input, classify intent, extract entities |
| Plan | Decide which tools to call and in what order |
| Execute | Run tools via LangGraph tool-calling loop (may cycle back to Plan) |
| Verify | Domain-specific validation — sanity-check numbers, flag stale data, validate ranges |
| Disclaim | Inject financial disclaimers ("not financial advice", data freshness caveats) |
| Respond | Format final response and stream via SSE |
Module Structure
apps/api/src/app/endpoints/agents/
├── agent.module.ts # NestJS module (independent from AiModule)
├── agent.controller.ts # REST + SSE endpoints
├── agent.service.ts # Core orchestration — builds and invokes the graph
├── graph/
│ ├── agent.graph.ts # StateGraph definition with 6 nodes
│ ├── state.ts # AgentState type definition
│ └── nodes/
│ ├── query.node.ts # Intent classification + entity extraction
│ ├── plan.node.ts # Tool selection + execution planning
│ ├── execute.node.ts # Tool invocation via LangGraph
│ ├── verify.node.ts # Domain-specific output validation
│ ├── disclaim.node.ts # Financial disclaimer injection
│ └── respond.node.ts # Response formatting + SSE streaming
├── tools/
│ ├── portfolio-summary.tool.ts # Wraps PortfolioService.getDetails()
│ ├── portfolio-performance.tool.ts # Wraps PortfolioService.getPerformance()
│ ├── holdings-detail.tool.ts # Wraps PortfolioService.getHoldings()
│ ├── activity-history.tool.ts # Wraps OrderService
│ ├── market-data.tool.ts # Wraps DataProviderService
│ ├── account-overview.tool.ts # Wraps AccountService
│ └── risk-report.tool.ts # Wraps RulesService + portfolio rules
├── providers/
│ └── llm-provider.ts # Model-agnostic LLM provider interface
└── streaming/
└── sse.service.ts # Server-Sent Events for token delivery
API Endpoints
| Method | Route | Purpose |
|---|---|---|
POST |
/api/v1/agents/chat |
Send message, get streamed SSE response |
GET |
/api/v1/agents/conversations |
List user's conversations |
GET |
/api/v1/agents/conversations/:id |
Get conversation with messages |
DELETE |
/api/v1/agents/conversations/:id |
Delete a conversation |
7 Tools (LangGraph DynamicStructuredTool)
Each tool wraps an existing Ghostfolio service — no duplicate business logic.
| Tool Name | Wraps | Input Schema | Returns |
|---|---|---|---|
portfolio_summary |
PortfolioService.getDetails() |
filters (accounts, tags, assetClasses) | Holdings with allocations, sectors, currencies |
portfolio_performance |
PortfolioService.getPerformance() |
dateRange, filters | ROI, TWR, MWR, chart data |
holdings_detail |
PortfolioService.getHoldings() |
symbol (optional), filters | Quantity, price, P&L per holding |
activity_history |
OrderService.getOrders() |
symbol, type, dateRange | Filtered transaction history |
market_data |
DataProviderService.getQuotes() |
symbols[] | Current quotes, daily change, market state |
risk_report |
RulesService + portfolio rules |
filters | Rule evaluations (cluster risk, currency risk, etc.) |
account_overview |
AccountService.getAccounts() |
accountId (optional) | Account balances, platforms, cash positions |
Conversation Memory
Uses LangGraph checkpointer persisted to PostgreSQL — no custom Prisma Conversation/Message models needed. LangGraph manages state serialization and retrieval natively via @langchain/langgraph-checkpoint-postgres.
Each conversation is identified by a thread_id (UUID). The checkpointer stores the full graph state (messages, tool results, metadata) at each step.
LLM Provider
Model-agnostic provider interface with Claude Sonnet 4 as default:
// providers/llm-provider.ts
import { ChatAnthropic } from '@langchain/anthropic';
// Default: Claude Sonnet 4 via Anthropic API
// Swappable to OpenAI, OpenRouter, or any @langchain/* provider
API key stored via PropertyService (key: API_KEY_ANTHROPIC). Model configurable via ANTHROPIC_MODEL property.
Frontend: Dedicated Chat Page
New lazy-loaded Angular route at /chat:
- Full-page layout with conversation list sidebar + chat area
- Streaming message display via SSE (
EventSource) - Input field sends to
POST /api/v1/agents/chat - Basic markdown rendering for agent responses
- Shows tool call indicators (e.g., "Fetching portfolio summary...")
Observability: LangSmith
- All agent runs traced via
@langchain/corecallbacks - Tool call latency, token usage, and error rates tracked
- Admin-configurable via
PROPERTY_LANGSMITH_API_KEYin PropertyService - Tracing can be toggled on/off without redeployment
Key Design Principles
- Independent module —
AgentModuleis fully separate fromAiModule; no shared state, no shared code - Wrap, don't replace — tools call existing services; no duplicate business logic
- 6-node pipeline — query → plan → execute → verify → disclaim → respond
- Verify node — domain-specific validation (sanity-check portfolio values, flag stale data, validate date ranges)
- Disclaim node — automatic financial disclaimers on all responses
- Streaming-first — all chat responses use SSE for real-time token delivery
- Model-agnostic — swap LLM provider without code changes; just update PropertyService
- LangGraph-native memory — checkpointer handles conversation persistence, no custom ORM models
- Auditable — LangSmith tracing for all tool invocations
24-Hour MVP Requirements (Hard Gate)
All items required to pass:
| # | Requirement | Implementation |
|---|---|---|
| 1 | Agent responds to natural language queries | LangGraph StateGraph with Claude Sonnet 4 |
| 2 | At least 3 functional tools | portfolio_summary, holdings_detail, account_overview |
| 3 | Tool calls execute successfully with structured results | Tools wrap existing services, return typed JSON |
| 4 | Agent synthesizes tool results into coherent responses | Respond node formats tool outputs into natural language |
| 5 | Conversation history maintained across turns | LangGraph checkpointer to PostgreSQL |
| 6 | Basic error handling (graceful failure, not crashes) | Try/catch in each node, fallback error messages |
| 7 | At least one domain-specific verification check | Verify node: validate portfolio totals, flag stale market data |
| 8 | 5+ test cases with expected outcomes | Jest test suite in agent.service.spec.ts |
| 9 | Deployed and publicly accessible | Docker build via existing docker-compose.build.yml |
Coding Conventions
- Backend patterns: NestJS modules with controller + service pairs, dependency injection throughout
- Frontend patterns: Angular standalone components, lazy-loaded routes, Angular Material UI
- Naming: PascalCase for classes/interfaces, camelCase for variables/functions, kebab-case for file names
- Imports: Use
@ghostfolio/path aliases — never relative paths across app/lib boundaries - Testing: Jest for all tests; test files co-located with source as
*.spec.ts - Database changes: Always update
prisma/schema.prisma, then runnpm run database:pushor create a migration - Shared types: Define interfaces in
libs/common/src/lib/interfaces/, export via barrel files - UI components: Reusable components go in
libs/ui/, page-specific components stay inapps/client/
Environment Variables
Required variables (see .env.example):
COMPOSE_PROJECT_NAME=ghostfolio
POSTGRES_DB=ghostfolio-db
POSTGRES_USER=user
POSTGRES_PASSWORD=<password>
DATABASE_URL=postgresql://user:password@localhost:5432/ghostfolio-db
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=<password>
ACCESS_TOKEN_SALT=<random-string>
JWT_SECRET_KEY=<random-string>