A full-stack portfolio dashboard that tracks holdings, refreshes prices on a 60-second poll, charts allocation and performance, surfaces market news, and generates Claude-powered portfolio briefs.
The frontend is a React + Vite single-page app. The backend is a Spring Boot REST API backed by an in-memory H2 database. Live data is pulled from Finnhub (quotes), NewsAPI (headlines), and the Anthropic Messages API (insights). Each integration has a graceful local fallback so the dashboard remains usable while keys are being configured.
Frontend
- React 18, Vite 5
- Axios for API calls
- Recharts for the line / pie / bar visualizations
- lucide-react icons
Backend
- Java 17, Spring Boot 3.3.5
- Spring Web (REST), Spring WebFlux (
WebClientfor outbound HTTP), Spring Data JPA, Bean Validation - H2 in-memory database (auto-seeded with demo holdings)
External services
- Finnhub – real-time stock quotes
- NewsAPI – market and company headlines
- Anthropic Messages API – portfolio insight generation
portfolio-analytics-dashboard/
├── backend/
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/example/portfolioanalytics/
│ │ ├── PortfolioAnalyticsApplication.java
│ │ ├── config/ # CORS + WebClient beans
│ │ ├── controller/ # REST endpoints
│ │ ├── model/ # Holding entity + DTOs
│ │ ├── repository/ # Spring Data JPA
│ │ └── service/ # Portfolio, MarketData, News, Claude
│ └── resources/application.properties
├── frontend/
│ ├── package.json
│ ├── vite.config.js
│ └── src/
│ ├── api/ # axios client + endpoint wrappers
│ ├── components/ # panels, header, holdings table
│ ├── components/charts/ # Recharts line / pie / bar
│ ├── hooks/ # usePortfolioData (poll + refresh)
│ ├── pages/Dashboard.jsx
│ └── styles/global.css
├── docker-compose.yml
├── .env.example
└── README.md
- Java 17+
- Maven 3.9+ (or use the bundled Spring Boot plugin via
./mvnwif you add a wrapper) - Node.js 20+
- Optional: Docker, for the compose-based dev setup
Copy the example file and fill in the keys you have:
cp .env.example .env| Variable | Used by | Purpose |
|---|---|---|
SERVER_PORT |
backend | Port for the Spring Boot app (default 8080) |
FRONTEND_ORIGINS |
backend | Comma-separated list of allowed CORS origins |
CLAUDE_API_KEY |
backend | Anthropic API key for /api/insights/generate |
CLAUDE_MODEL |
backend | Model ID (default claude-sonnet-4-20250514) |
NEWS_API_KEY |
backend | NewsAPI key for market and company news |
MARKET_DATA_PROVIDER |
backend | Quote provider; only finnhub is implemented |
MARKET_DATA_API_KEY |
backend | Finnhub API key |
VITE_API_BASE_URL |
frontend | Base URL for backend API (default http://localhost:8080/api) |
The backend imports the root .env automatically via spring.config.import=optional:file:../.env[.properties], so running mvn spring-boot:run from backend/ picks it up without extra setup.
Missing keys are not fatal. Each integration falls back to local data if its key is absent or the upstream call fails:
- Quotes → deterministic synthetic prices seeded for common tickers (AAPL, MSFT, NVDA, GOOGL, AMZN, TSLA, META).
- News → two demo articles tagged accordingly.
- Insights → a locally-composed brief that flags whichever upstream condition prevented the live call (missing key, HTTP error, empty response).
cd backend
mvn spring-boot:runListens on http://localhost:8080. H2 is in-memory, so the demo holdings reset on every restart. The H2 console is enabled at http://localhost:8080/h2-console (JDBC URL jdbc:h2:mem:portfolio, user sa, no password).
In a second terminal:
cd frontend
npm install
npm run devOpen http://localhost:5173. The dashboard polls the backend every 60 seconds and on demand via the Refresh button.
If you load the app from http://127.0.0.1:5173, keep both origins in .env:
FRONTEND_ORIGINS=http://localhost:5173,http://127.0.0.1:5173docker compose upThis boots the backend (Maven + Temurin 17) and frontend (Node 20) with the repo mounted as bind volumes and .env shared between both. Useful for a one-command spin-up; not optimized for production.
All endpoints are rooted at /api.
| Method | Path | Body | Description |
|---|---|---|---|
GET |
/portfolio/holdings |
– | List holdings with refreshed prices, sorted by symbol |
POST |
/portfolio/holdings |
Holding |
Create a holding (returns 201) |
PUT |
/portfolio/holdings/{id} |
Holding |
Update an existing holding |
DELETE |
/portfolio/holdings/{id} |
– | Delete a holding (returns 204) |
GET |
/portfolio/summary |
– | Totals, allocation slices, and a 30-day performance series |
Holding shape:
{
"symbol": "AAPL",
"companyName": "Apple Inc.",
"quantity": 12,
"averageCost": 164.50
}symbol is required and is normalized to upper-case. quantity must be >= 0.0001. averageCost must be >= 0. currentPrice, marketValue, costBasis, unrealizedGainLoss, and unrealizedGainLossPercent are computed server-side.
| Method | Path | Description |
|---|---|---|
GET |
/news/market |
Eight most recent market / investing / finance headlines |
GET |
/news/company/{symbol} |
Recent headlines filtered to the given ticker |
| Method | Path | Description |
|---|---|---|
POST |
/insights/generate |
Sends current holdings, summary, and news to Claude and returns a structured brief |
Request body:
{
"holdings": [...],
"summary": {...},
"news": [...]
}Response includes headline, summary, opportunities, risks, and generatedByClaude (boolean indicating whether the response is live or from the local fallback).
- On mount,
usePortfolioDatafetchesholdings,summary, andnewsin parallel, thenPOSTs them to/insights/generate. - The same hook re-runs every 60 seconds and on user-triggered refresh.
PortfolioServicerecomputescurrentPrice,marketValue, andcostBasison every read by callingMarketDataService.getLatestPrice(...). The 30-day performance series is synthesized fromtotalCost→totalValuewith a small sinusoidal curve (this is illustrative, not historical).ClaudeInsightServiceposts a single user message tohttps://api.anthropic.com/v1/messages(max_tokens=700) and parsescontent[0].text. Any failure falls back to a deterministic local brief.
- Start the backend and frontend.
- Open
http://localhost:5173and confirm the seeded demo holdings (AAPL, MSFT, NVDA) appear. - Add a new holding via the form; confirm it appears in the table and updates the totals.
- Edit quantity or average cost inline and click Save; confirm allocation and gain/loss update.
- Delete a holding; confirm the charts re-render.
- Click Refresh and watch the prices and insight panel update.
- Add real
MARKET_DATA_API_KEY,NEWS_API_KEY, andCLAUDE_API_KEY, restart the backend, and confirm the source pill on the insights panel switches fromLocal fallbacktoClaude APIand that real headlines appear.
.envis gitignored — never commit it.- H2 is in-memory by default; data resets on every backend restart. Point
DATABASE_URLat a persistent JDBC URL if you want durable storage. - The dashboard polls every 60 s; "real-time" here means "frequently refreshed", not streaming. Wiring up server-sent events or websockets is a natural extension if you need true push.
- Claude output is informational. Do not treat it as financial advice.