GitHub Issue: Frontend Real-Time Portfolio Polling & Live Updates
🎯 Overview
Implement real-time portfolio updates in the frontend by polling backend API endpoints. Backend calculates portfolio values using cached market data from CoinGecko/Coinbase, frontend displays live updates without making direct calls to price providers.
🔴 Problem Statement
Frontend needs to display:
- Live portfolio values for each agent (cash + holdings at current prices)
- Current market prices for all supported tokens
- Performance metrics (P&L, ROI, win rate) updated in real-time
- Trade history with real-time additions
Current State:
- No backend endpoints exist to serve enriched portfolio data
- No price caching mechanism in backend
- No polling infrastructure in frontend
- Frontend can't display live portfolio values without calling external APIs directly (rate limit risk)
🎯 Goals
📐 Architecture Decision
✅ CHOSEN: Backend Aggregation + Frontend Polling
┌─────────────────┐
│ Frontend │ Polls every 5s
│ │──────────────────┐
│ - Portfolio │ │
│ - Market Data │ ▼
│ - Trade Feed │ ┌────────────────────┐
└─────────────────┘ │ Backend API │
│ │
│ ┌──────────────┐ │
│ │ Price Cache │ │ Fetch prices
│ │ (5s TTL) │◄─┼────────────┐
│ └──────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ Calculate: │ │
│ cash + Σ(holdings │ │
│ × cached_price) │ │
└────────────────────┘ │
│
┌─────────▼────────┐
│ CoinGecko API │
│ (Rate Limited) │
└──────────────────┘
Why This Approach?
✅ Pros:
- Single source of truth - Backend owns price data
- Rate limit protection - Backend controls API call frequency with caching
- Consistent pricing - All agents use same price snapshot
- Simple frontend - Just polls one endpoint, no API key management
- Better UX - Can show last-updated timestamp
- Scalable - Easy to add WebSockets later
❌ Rejected Alternative: Frontend Calls CoinGecko Directly
- Multiple rate limit risk (every user polling = N×rate)
- Inconsistent data between backend agent decisions and frontend display
- Duplicate API calls (backend already fetching for trades)
- Frontend complexity (API key management, error handling)
- Can't cache effectively (each browser instance separate)
📝 Backend Implementation
1. Add Price Caching to MarketDataTool
File: tools/market_data.py
from datetime import datetime, timedelta
from typing import Dict, Optional
class MarketDataTool:
def __init__(self, api_base: str = None, api_key: str = None):
# ... existing code ...
# Price caching to avoid hitting rate limits
self._price_cache: Dict[str, float] = {}
self._cache_timestamp: Optional[datetime] = None
self._cache_ttl_seconds: int = 5 # 5-second cache
def _is_cache_valid(self) -> bool:
"""Check if cached prices are still fresh."""
if not self._cache_timestamp:
return False
age = (datetime.utcnow() - self._cache_timestamp).total_seconds()
return age < self._cache_ttl_seconds
def get_all_prices(self) -> Dict[str, float]:
"""
Get current USD prices for all supported tokens with caching.
Returns:
Dict[str, float]: {"BTC": 42000.50, "ETH": 2300.25, ...}
"""
# Return cached prices if still valid
if self._is_cache_valid():
return self._price_cache.copy()
# Fetch fresh prices
prices = {}
for token in self.supported_tokens.keys():
try:
price_data = self.get_price(token)
prices[token] = price_data[token.lower()]['usd']
except Exception as e:
# Fallback to last known price if fetch fails
if token in self._price_cache:
prices[token] = self._price_cache[token]
logger.warning(f"Using cached price for {token} due to error: {e}")
else:
logger.error(f"Failed to fetch {token} price: {e}")
# Update cache
self._price_cache = prices
self._cache_timestamp = datetime.utcnow()
return prices.copy()
2. Create FastAPI Routes
File: api/routes/agents.py
from fastapi import APIRouter, HTTPException
from typing import Dict, Any, List
from datetime import datetime
from decimal import Decimal
router = APIRouter(prefix="/api/agents", tags=["agents"])
# Shared market tool instance for caching
_market_tool = MarketDataTool()
@router.get("/{agent_id}/portfolio")
async def get_agent_portfolio(agent_id: int) -> Dict[str, Any]:
"""
Get agent's current portfolio with live market values.
Returns:
- cash: Available USD cash
- holdings: {token: {quantity, price, value}}
- holdings_value: Total value of all holdings
- total_value: cash + holdings_value
- performance: P&L, ROI, win rate, etc.
- timestamp: When prices were fetched
"""
# Get agent from in-memory store (or MCP server)
if agent_id not in ACTIVE_AGENTS:
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
agent = ACTIVE_AGENTS[agent_id]
# Get agent's holdings
holdings = agent.get_holdings() # {"ETH": 1.5, "BTC": 0.25}
cash = agent.get_available_cash()
# Get current market prices (cached!)
all_prices = _market_tool.get_all_prices()
# Calculate holdings value with current prices
holdings_value = 0.0
holdings_enriched = {}
for token, quantity in holdings.items():
price = all_prices.get(token, 0.0)
value = quantity * price
holdings_value += value
holdings_enriched[token] = {
"quantity": quantity,
"current_price": price,
"total_value": value
}
# Total portfolio value
total_value = cash + holdings_value
# Get performance metrics from portfolio
portfolio = agent.get_portfolio_status()
return {
"agent_id": agent_id,
"cash": cash,
"holdings": holdings_enriched,
"holdings_value": holdings_value,
"total_value": total_value,
"performance": {
"starting_value": portfolio.starting_val,
"realized_pnl": portfolio.realized_pnl,
"unrealized_pnl": portfolio.unrealized_pnl,
"total_pnl": portfolio.realized_pnl + portfolio.unrealized_pnl,
"roi": portfolio.roi,
"roi_percent": portfolio.roi * 100,
"num_trades": portfolio.num_trades,
"win_rate": portfolio.win_rate,
"num_winning_trades": portfolio.num_winning_trades,
"num_losing_trades": portfolio.num_losing_trades
},
"timestamp": datetime.utcnow().isoformat()
}
@router.get("/")
async def list_agents() -> Dict[str, Any]:
"""
List all active agents with portfolio summaries.
Optimized: Fetches prices once, calculates for all agents.
"""
agents_list = []
# Fetch prices once for all agents
all_prices = _market_tool.get_all_prices() if ACTIVE_AGENTS else {}
for agent_id, agent in ACTIVE_AGENTS.items():
holdings = agent.get_holdings()
cash = agent.get_available_cash()
# Calculate total value using shared price data
holdings_value = sum(
qty * all_prices.get(token, 0.0)
for token, qty in holdings.items()
)
total_value = cash + holdings_value
portfolio = agent.get_portfolio_status()
agents_list.append({
"agent_id": agent_id,
"name": f"Agent-{agent_id}", # Add if you have names
"type": agent.__class__.__name__,
"personality": agent.personality,
"risk_score": agent.risk_score,
"total_value": total_value,
"cash": cash,
"holdings_value": holdings_value,
"roi": portfolio.roi,
"roi_percent": portfolio.roi * 100,
"num_trades": portfolio.num_trades,
"win_rate": portfolio.win_rate
})
return {
"agents": agents_list,
"total_agents": len(agents_list),
"timestamp": datetime.utcnow().isoformat()
}
@router.get("/market/prices")
async def get_market_prices() -> Dict[str, Any]:
"""
Get current market prices for all supported tokens.
Returns prices, sentiment, and market overview.
"""
prices = _market_tool.get_all_prices()
sentiment = _market_tool.get_market_sentiment()
# Enrich with additional data
market_data = {}
for token, price in prices.items():
try:
volume = _market_tool.get_volume(token)
market_data[token] = {
"symbol": token,
"price": price,
"volume_24h": volume
}
except:
market_data[token] = {
"symbol": token,
"price": price
}
return {
"prices": market_data,
"market_sentiment": sentiment,
"timestamp": datetime.utcnow().isoformat()
}
@router.get("/{agent_id}/trades")
async def get_agent_trades(
agent_id: int,
limit: int = 20
) -> Dict[str, Any]:
"""
Get agent's recent trade history.
Args:
agent_id: Agent identifier
limit: Max trades to return (default 20)
"""
if agent_id not in ACTIVE_AGENTS:
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
agent = ACTIVE_AGENTS[agent_id]
recent_trades = agent.get_short_term_memory(limit)
trades_list = [
{
"trade_id": trade.trade_id,
"action": trade.action,
"token": trade.token,
"quantity": trade.qty,
"price": trade.price,
"total_value": trade.qty * trade.price,
"confidence": trade.confidence,
"realized_pnl": trade.realized_pnl,
"roi": trade.roi,
"summary": trade.summary,
"timestamp": trade.timestamp.isoformat()
}
for trade in recent_trades
]
return {
"agent_id": agent_id,
"trades": trades_list,
"count": len(trades_list),
"timestamp": datetime.utcnow().isoformat()
}
3. FastAPI App Setup
File: main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.routes import agents
import os
app = FastAPI(
title="Crypto Trading Agents API",
version="1.0.0"
)
# CORS for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:5173",
os.getenv("FRONTEND_URL", "http://localhost:3000")
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routes
app.include_router(agents.router)
@app.get("/health")
async def health_check():
return {"status": "healthy", "service": "trading-agents-api"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
🎨 Frontend Implementation
1. API Client
File: src/lib/api.ts
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
export interface AgentPortfolio {
agent_id: number;
cash: number;
holdings: {
[token: string]: {
quantity: number;
current_price: number;
total_value: number;
};
};
holdings_value: number;
total_value: number;
performance: {
starting_value: number;
realized_pnl: number;
unrealized_pnl: number;
total_pnl: number;
roi: number;
roi_percent: number;
num_trades: number;
win_rate: number;
num_winning_trades: number;
num_losing_trades: number;
};
timestamp: string;
}
export interface MarketPrices {
prices: {
[token: string]: {
symbol: string;
price: number;
volume_24h?: number;
};
};
market_sentiment: string;
timestamp: string;
}
export interface AgentSummary {
agent_id: number;
name: string;
type: string;
personality: string;
risk_score: number;
total_value: number;
cash: number;
holdings_value: number;
roi: number;
roi_percent: number;
num_trades: number;
win_rate: number;
}
export const api = {
async getAgentPortfolio(agentId: number): Promise<AgentPortfolio> {
const res = await fetch(`${API_BASE_URL}/api/agents/${agentId}/portfolio`);
if (!res.ok) throw new Error(`Failed to fetch portfolio: ${res.statusText}`);
return res.json();
},
async listAgents(): Promise<{ agents: AgentSummary[]; total_agents: number; timestamp: string }> {
const res = await fetch(`${API_BASE_URL}/api/agents/`);
if (!res.ok) throw new Error(`Failed to fetch agents: ${res.statusText}`);
return res.json();
},
async getMarketPrices(): Promise<MarketPrices> {
const res = await fetch(`${API_BASE_URL}/api/agents/market/prices`);
if (!res.ok) throw new Error(`Failed to fetch market prices: ${res.statusText}`);
return res.json();
},
async getAgentTrades(agentId: number, limit: number = 20): Promise<any> {
const res = await fetch(`${API_BASE_URL}/api/agents/${agentId}/trades?limit=${limit}`);
if (!res.ok) throw new Error(`Failed to fetch trades: ${res.statusText}`);
return res.json();
}
};
2. Polling Hook
File: src/hooks/usePortfolioPolling.ts
import { useState, useEffect, useRef, useCallback } from 'react';
import { api, AgentPortfolio } from '@/lib/api';
export interface UsePortfolioPollingOptions {
intervalMs?: number;
enabled?: boolean;
onError?: (error: Error) => void;
}
export function usePortfolioPolling(
agentId: number,
options: UsePortfolioPollingOptions = {}
) {
const {
intervalMs = 5000, // Poll every 5 seconds
enabled = true,
onError
} = options;
const [portfolio, setPortfolio] = useState<AgentPortfolio | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const intervalRef = useRef<NodeJS.Timeout>();
const isMounted = useRef(true);
const fetchPortfolio = useCallback(async () => {
try {
const data = await api.getAgentPortfolio(agentId);
if (isMounted.current) {
setPortfolio(data);
setError(null);
setLoading(false);
}
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
if (isMounted.current) {
setError(errorMsg);
setLoading(false);
}
if (onError) {
onError(err instanceof Error ? err : new Error(errorMsg));
}
}
}, [agentId, onError]);
useEffect(() => {
isMounted.current = true;
if (!enabled) {
setLoading(false);
return;
}
// Fetch immediately on mount
fetchPortfolio();
// Set up polling interval
intervalRef.current = setInterval(fetchPortfolio, intervalMs);
// Cleanup
return () => {
isMounted.current = false;
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [agentId, intervalMs, enabled, fetchPortfolio]);
const refetch = useCallback(() => {
fetchPortfolio();
}, [fetchPortfolio]);
return {
portfolio,
loading,
error,
refetch,
lastUpdated: portfolio?.timestamp
};
}
3. Market Prices Polling Hook
File: src/hooks/useMarketPrices.ts
import { useState, useEffect, useRef, useCallback } from 'react';
import { api, MarketPrices } from '@/lib/api';
export function useMarketPrices(intervalMs: number = 10000) {
const [prices, setPrices] = useState<MarketPrices | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const intervalRef = useRef<NodeJS.Timeout>();
const fetchPrices = useCallback(async () => {
try {
const data = await api.getMarketPrices();
setPrices(data);
setError(null);
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch prices');
setLoading(false);
}
}, []);
useEffect(() => {
fetchPrices();
intervalRef.current = setInterval(fetchPrices, intervalMs);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [fetchPrices, intervalMs]);
return { prices, loading, error, refetch: fetchPrices };
}
4. Portfolio Display Component
File: src/components/AgentPortfolio.tsx
import React from 'react';
import { usePortfolioPolling } from '@/hooks/usePortfolioPolling';
import { formatCurrency, formatPercent } from '@/lib/utils';
interface AgentPortfolioProps {
agentId: number;
}
export function AgentPortfolio({ agentId }: AgentPortfolioProps) {
const { portfolio, loading, error, lastUpdated } = usePortfolioPolling(agentId);
if (loading) {
return <div className="animate-pulse">Loading portfolio...</div>;
}
if (error) {
return <div className="text-red-500">Error: {error}</div>;
}
if (!portfolio) {
return <div>No portfolio data available</div>;
}
const { cash, holdings, total_value, performance } = portfolio;
const isProfit = performance.total_pnl >= 0;
return (
<div className="portfolio-card">
{/* Header */}
<div className="portfolio-header">
<div>
<h2 className="text-2xl font-bold">
{formatCurrency(total_value)}
</h2>
<p className={`text-lg ${isProfit ? 'text-green-500' : 'text-red-500'}`}>
{isProfit ? '+' : ''}{formatCurrency(performance.total_pnl)}
({formatPercent(performance.roi_percent)})
</p>
</div>
<div className="text-sm text-gray-500">
Updated: {new Date(lastUpdated!).toLocaleTimeString()}
</div>
</div>
{/* Cash */}
<div className="portfolio-section">
<h3 className="font-semibold">Cash</h3>
<p className="text-xl">{formatCurrency(cash)}</p>
</div>
{/* Holdings */}
<div className="portfolio-section">
<h3 className="font-semibold">Holdings</h3>
{Object.entries(holdings).length === 0 ? (
<p className="text-gray-500">No positions</p>
) : (
<div className="holdings-list">
{Object.entries(holdings).map(([token, data]) => (
<div key={token} className="holding-item">
<div className="flex justify-between">
<span className="font-medium">{token}</span>
<span>{formatCurrency(data.total_value)}</span>
</div>
<div className="text-sm text-gray-500">
{data.quantity.toFixed(4)} @ {formatCurrency(data.current_price)}
</div>
</div>
))}
</div>
)}
</div>
{/* Performance Stats */}
<div className="portfolio-section">
<h3 className="font-semibold">Performance</h3>
<div className="stats-grid">
<div>
<span className="text-gray-500">Trades:</span>
<span className="font-medium">{performance.num_trades}</span>
</div>
<div>
<span className="text-gray-500">Win Rate:</span>
<span className="font-medium">{formatPercent(performance.win_rate * 100)}</span>
</div>
<div>
<span className="text-gray-500">Realized P&L:</span>
<span className={performance.realized_pnl >= 0 ? 'text-green-500' : 'text-red-500'}>
{formatCurrency(performance.realized_pnl)}
</span>
</div>
<div>
<span className="text-gray-500">Unrealized P&L:</span>
<span className={performance.unrealized_pnl >= 0 ? 'text-green-500' : 'text-red-500'}>
{formatCurrency(performance.unrealized_pnl)}
</span>
</div>
</div>
</div>
</div>
);
}
5. Market Overview Component
File: src/components/MarketOverview.tsx
import React from 'react';
import { useMarketPrices } from '@/hooks/useMarketPrices';
import { formatCurrency } from '@/lib/utils';
export function MarketOverview() {
const { prices, loading, error } = useMarketPrices(10000);
if (loading) return <div>Loading market data...</div>;
if (error) return <div>Error: {error}</div>;
if (!prices) return null;
const sentimentColor = {
bullish: 'text-green-500',
bearish: 'text-red-500',
neutral: 'text-gray-500',
unknown: 'text-gray-400'
}[prices.market_sentiment];
return (
<div className="market-overview">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Market Overview</h3>
<span className={`${sentimentColor} font-medium capitalize`}>
{prices.market_sentiment}
</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{Object.entries(prices.prices).map(([token, data]) => (
<div key={token} className="price-card">
<div className="font-semibold">{token}</div>
<div className="text-xl">{formatCurrency(data.price)}</div>
{data.volume_24h && (
<div className="text-sm text-gray-500">
Vol: {formatCurrency(data.volume_24h, { compact: true })}
</div>
)}
</div>
))}
</div>
<div className="text-xs text-gray-500 mt-2">
Last updated: {new Date(prices.timestamp).toLocaleTimeString()}
</div>
</div>
);
}
📋 Implementation Checklist
Backend
Frontend
Integration Testing
🧪 Testing Strategy
Manual Testing
- Start backend API server
- Create 2-3 test agents
- Open frontend with network throttling
- Verify portfolio updates every 5s
- Execute a trade, verify immediate update on next poll
- Monitor network tab for rate limit errors
- Let run for 10+ minutes to verify stability
Load Testing
- Simulate 10+ concurrent users polling
- Verify backend cache is working (CoinGecko calls ≤ 1 per 5s)
- Monitor API response times (should be < 200ms)
📊 Success Criteria
🔗 Dependencies
- Backend:
fastapi, uvicorn (already installed)
- Frontend: No new dependencies needed
⚠️ Known Limitations
-
Polling interval trade-off:
- Too fast = more bandwidth/server load
- Too slow = stale data
- Recommended: 3-5s for portfolio, 10s for market prices
-
Price accuracy:
- Cached prices may be up to 5 seconds stale
- Acceptable for MVP, can reduce TTL if needed
-
Scalability:
- Each poll = 1 API request per agent
- 10 agents × 12 polls/min = 120 req/min
- Fine for MVP, consider WebSockets for 100+ agents
-
No real-time push:
- User won't see trade execution instantly
- Must wait up to 5s for next poll
- Future: Upgrade to WebSockets for instant updates
🚀 Future Enhancements (Separate Issues)
📚 Related Files
Backend:
tools/market_data.py - Add caching logic
api/routes/agents.py - Create new file
main.py - FastAPI app setup
Frontend:
src/lib/api.ts - Create new file
src/hooks/usePortfolioPolling.ts - Create new file
src/hooks/useMarketPrices.ts - Create new file
src/components/AgentPortfolio.tsx - Create new file
src/components/MarketOverview.tsx - Create new file
⏱️ Estimated Time
- Backend: 2-3 hours (caching + endpoints + testing)
- Frontend: 3-4 hours (hooks + components + styling)
- Testing: 1-2 hours (integration + load testing)
- Total: 6-9 hours
📝 Notes
- Start with backend endpoints first (can test with curl/Postman)
- Price caching is critical - verify it works before frontend
- Frontend can use mock data initially while backend is being built
- Consider adding
/api/agents/health endpoint that includes cache status
- Log cache hits/misses for monitoring effectiveness
GitHub Issue: Frontend Real-Time Portfolio Polling & Live Updates
🎯 Overview
Implement real-time portfolio updates in the frontend by polling backend API endpoints. Backend calculates portfolio values using cached market data from CoinGecko/Coinbase, frontend displays live updates without making direct calls to price providers.
🔴 Problem Statement
Frontend needs to display:
Current State:
🎯 Goals
MarketDataTool(5-second TTL)📐 Architecture Decision
✅ CHOSEN: Backend Aggregation + Frontend Polling
Why This Approach?
✅ Pros:
❌ Rejected Alternative: Frontend Calls CoinGecko Directly
📝 Backend Implementation
1. Add Price Caching to MarketDataTool
File:
tools/market_data.py2. Create FastAPI Routes
File:
api/routes/agents.py3. FastAPI App Setup
File:
main.py🎨 Frontend Implementation
1. API Client
File:
src/lib/api.ts2. Polling Hook
File:
src/hooks/usePortfolioPolling.ts3. Market Prices Polling Hook
File:
src/hooks/useMarketPrices.ts4. Portfolio Display Component
File:
src/components/AgentPortfolio.tsx5. Market Overview Component
File:
src/components/MarketOverview.tsx📋 Implementation Checklist
Backend
MarketDataTool_is_cache_valid()methodget_all_prices()with cachingGET /api/agents/{id}/portfolioGET /api/agents/GET /api/agents/market/pricesGET /api/agents/{id}/tradesFrontend
api.ts)usePortfolioPollinghookuseMarketPriceshookAgentPortfoliocomponentMarketOverviewcomponentformatCurrency,formatPercent)Integration Testing
🧪 Testing Strategy
Manual Testing
Load Testing
📊 Success Criteria
🔗 Dependencies
fastapi,uvicorn(already installed)Polling interval trade-off:
Price accuracy:
Scalability:
No real-time push:
🚀 Future Enhancements (Separate Issues)
📚 Related Files
Backend:
tools/market_data.py- Add caching logicapi/routes/agents.py- Create new filemain.py- FastAPI app setupFrontend:
src/lib/api.ts- Create new filesrc/hooks/usePortfolioPolling.ts- Create new filesrc/hooks/useMarketPrices.ts- Create new filesrc/components/AgentPortfolio.tsx- Create new filesrc/components/MarketOverview.tsx- Create new file⏱️ Estimated Time
📝 Notes
/api/agents/healthendpoint that includes cache status