ghostfolio-finance-agent
v1.0.3
Published
LangGraph-powered finance agent for Ghostfolio. Drop-in AI layer for natural language portfolio queries, market data, tax estimates, and trade execution with conversation memory.
Maintainers
Readme
ghostfolio-finance-agent
A LangGraph-powered AI agent for natural language portfolio queries. Built on top of Ghostfolio but usable with any portfolio backend via the ports/adapters pattern.
Live demo: https://ghostfolio-agent-production-f5ad.up.railway.app
Eval dataset: evals/ — 55 cases, CI-ready
Features
- Natural language portfolio queries via LangGraph ReAct agent
- 8 tools: portfolio analysis, market data, transaction categorization, tax estimates, compliance checks, news sentiment, trade confirm, trade execute
- Tiered LLM routing — Claude Haiku for simple lookups, Sonnet for complex reasoning
- Redis-backed conversation memory (multi-turn)
- Multi-layer caching: adapter (90s), router (5 min in-memory), LLM response (1h), COL/FX/news (Redis, 30/15 min)
- Streaming responses via async generator (SSE-ready)
- Confidence scoring + citations on every response
- PII-safe LangSmith tracing
- Ports/adapters architecture — bring your own data backend
Installation
npm install ghostfolio-finance-agentPeer dependencies
npm install @langchain/anthropic @langchain/core @langchain/langgraph @langchain/langgraph-checkpoint-redis redis zodQuick Start
1. Implement the port interfaces
The agent has no knowledge of your database or API. You implement three interfaces that tell it how to fetch data:
import type { IPortfolioPort, IMarketDataPort, IOrdersPort } from 'ghostfolio-finance-agent';
class MyPortfolioPort implements IPortfolioPort {
async getDetails(params) {
// fetch from your backend
return { holdings: { ... }, hasErrors: false };
}
async getPerformance(params) {
return { performance: { netPerformance: 1234, totalInvestment: 50000 } };
}
}
class MyMarketDataPort implements IMarketDataPort {
async getQuotes(identifiers) { ... }
async getHistoricalData(symbol, from, to) { ... }
}
class MyOrdersPort implements IOrdersPort {
async getOrders(params) { ... }
async createOrder(params) { ... }
}2. Run a query
import { runAgentGraph } from 'ghostfolio-finance-agent';
import { createClient } from 'redis';
const result = await runAgentGraph(
'How is my portfolio performing this year?',
'thread-id-123',
{
portfolioPort: new MyPortfolioPort(),
marketDataPort: new MyMarketDataPort(),
ordersPort: new MyOrdersPort(),
},
'user-id-456',
{ baseCurrency: 'USD' }
);
console.log(result.message);
// "Your portfolio has returned 12.4% year-to-date, with your largest
// gain coming from AAPL (+28%). Your total portfolio value is $187,450."
console.log(result.confidence); // 0.92
console.log(result.toolCalls); // [{ name: 'portfolio_analysis', success: true, durationMs: 841 }]3. Stream a response
import { streamAgentGraph } from 'ghostfolio-finance-agent';
for await (const event of streamAgentGraph(query, threadId, ports, userId, config)) {
if (event.type === 'token') process.stdout.write(event.content);
if (event.type === 'tool_start') console.log(`Calling ${event.toolName}...`);
if (event.type === 'done') console.log('\nFinal:', event.result);
}Using Individual Tools
You can use tools standalone without the full agent:
import { createPortfolioAnalysisTool, createMarketDataTool } from 'ghostfolio-finance-agent';
const portfolioTool = createPortfolioAnalysisTool(portfolioPort, 'user-123', 'USD');
const result = await portfolioTool.invoke({ includePerformance: true });HTTP Adapter Example (Ghostfolio backend)
If you're connecting to a running Ghostfolio instance, implement the ports as HTTP clients:
import axios from 'axios';
import type { IPortfolioPort } from 'ghostfolio-finance-agent';
class GhostfolioPortfolioPort implements IPortfolioPort {
constructor(private baseUrl: string, private jwt: string) {}
private get headers() {
return { Authorization: `Bearer ${this.jwt}` };
}
async getDetails(params) {
const { data } = await axios.get(
`${this.baseUrl}/api/v1/portfolio/details`,
{ params, headers: this.headers }
);
return data;
}
async getPerformance(params) {
const { data } = await axios.get(
`${this.baseUrl}/api/v2/portfolio/performance`,
{ params, headers: this.headers }
);
return data;
}
}API Reference
runAgentGraph(query, threadId, ports, userId, config)
Runs the full ReAct agent loop and returns a structured result.
| Param | Type | Description |
|---|---|---|
| query | string | Natural language question |
| threadId | string | Unique thread ID for conversation memory |
| ports | AgentDataPorts | Your port implementations |
| userId | string | User identifier |
| config | AgentGraphConfig | Optional config (baseCurrency, hasPriorContext) |
Returns AgentRunResult:
{
message: string;
toolCalls: ToolCallRecord[];
citations: CitationRecord[];
confidence: number; // 0.0–1.0
warnings: string[];
tokenUsage: TokenUsage;
newConversationId: string;
}streamAgentGraph(query, threadId, ports, userId, config)
Async generator that yields StreamEvent objects as the agent works:
type StreamEvent =
| { type: 'tool_start'; toolName: string }
| { type: 'tool_end'; toolName: string; success: boolean }
| { type: 'token'; content: string }
| { type: 'done'; result: AgentRunResult }
| { type: 'error'; error: string };Port interfaces
IPortfolioPort—getDetails(),getPerformance()IMarketDataPort—getQuotes(),getHistoricalData()IOrdersPort—getOrders(),createOrder()
Setup
1. Get an Anthropic API key
The agent uses Claude as its LLM. You need your own key — the package does not include one.
- Sign up at console.anthropic.com
- Go to API Keys → Create Key
- Copy the key (starts with
sk-ant-api03-...)
Cost estimate: A typical portfolio query costs $0.001–$0.005. Simple queries use Claude Haiku (~$0.0008/query); complex multi-step queries use Claude Sonnet (~$0.005/query). See Anthropic pricing.
2. Set up Redis
Redis is required for conversation memory (multi-turn chat). You have several options:
Local (development):
docker run -d -p 6379:6379 redis/redis-stack-server:latest
# REDIS_URL=redis://localhost:6379Railway (recommended for production): Add a Redis Stack service to your Railway project. Use the public TCP URL from the Railway dashboard.
Important: Use
redis-stack-server, not plainredis-server. The LangGraph checkpoint library requires the JSON and Search modules that only ship with Redis Stack.
Upstash / Redis Cloud: Any Redis 6+ instance works. Use the full connection URL including credentials:
REDIS_URL=redis://:yourpassword@your-host:63793. Configure environment variables
Create a .env file (never commit this):
# Required
ANTHROPIC_API_KEY=sk-ant-api03-... # From console.anthropic.com
REDIS_URL=redis://localhost:6379 # Your Redis connection URL
# Optional — LangSmith tracing (free tier available)
LANGSMITH_API_KEY=lsv2_pt_... # From smith.langchain.com
LANGSMITH_TRACING_ENABLED=true
# Optional — conversation TTL
AGENT_MEMORY_TTL_DAYS=7 # How long to keep conversation history
# Optional — trading (see docs/TRADING_SAFETY.md)
ORDER_DATA_SOURCE=MANUAL # MANUAL = any symbol (stocks + ETFs) accepted by Ghostfolio
TRADE_MAX_VALUE=50000 # Max single order value in USDEnvironment variable reference
| Variable | Required | Description |
|---|---|---|
| ANTHROPIC_API_KEY | Yes | Claude API key from console.anthropic.com |
| REDIS_URL | Yes | Redis Stack connection URL for conversation memory |
| LANGSMITH_API_KEY | No | LangSmith tracing key from smith.langchain.com |
| LANGSMITH_TRACING_ENABLED | No | Set to true to enable LangSmith tracing (default: false) |
| AGENT_MEMORY_TTL_DAYS | No | Days to retain conversation history in Redis (default: 7) |
| ORDER_DATA_SOURCE | No | Data source for order creation (default: MANUAL). MANUAL lets any symbol (ETFs + stocks) work; Ghostfolio must have MANUAL in DATA_SOURCES. See TRADING_SAFETY.md. |
| TRADE_MAX_VALUE | No | Max single order value in USD (default: 50000). Orders above this are rejected. |
Caching
The NestJS deployment uses several caches to reduce latency and API cost:
| Layer | Storage | TTL | Key / scope |
|-------|---------|-----|-------------|
| Ghostfolio adapter | Redis | 90s | agent:adapter:{kind}:{userId}:{hash(params)} — portfolio details/performance, market quotes/historical, orders list |
| Query router | In-memory | 5 min | Normalized query string → complexity + tool set (saves one Haiku call per repeated query) |
| LLM response | Redis | 1h | agent:llm:{sha256(conversationId\|query)} — full assistant result for non-streaming chat() only |
| Cost-of-living / exchange rate | Redis | 30 min | agent:cache:col:*, agent:cache:exchange-rate:latest — shared across instances |
| News sentiment | Redis | 15 min | agent:cache:news:{symbol}:{limit} |
All caches are optional from a correctness perspective: on Redis errors or misses, the agent falls back to live calls. The package library (runAgentGraph / streamAgentGraph) does not include the adapter or LLM caches; those are part of the NestJS AgentService in this repo.
Eval Dataset
A 55-case evaluation dataset is published in evals/:
# Validate dataset (no API calls)
npx ts-node evals/run-evals.ts --dry-run
# Run against your agent (CI gate: exits 1 if <80% pass)
npx ts-node evals/run-evals.ts --endpoint https://your-agent.com --token $JWTFull Deployment
For a complete NestJS + Next.js deployment of this agent as a standalone service, see the deployment guide or fork this repo and deploy to Railway.
License
MIT
