mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6.8 KiB
6.8 KiB
Quickstart: Portfolio Performance Views
Feature: 003-portfolio-performance-views Date: 2026-03-16
Overview
This feature adds three analytical views to the portfolio management application:
- Portfolio Summary — Entity-level rollup of financial performance metrics
- Asset Class Summary — Same metrics grouped by asset class
- Activity Detail — Transaction-level ledger with tax basis tracking
All three views live on a single tabbed page at route /portfolio-views.
Architecture
┌─────────────────────────────────┐
│ Angular Client │
│ /portfolio-views │
│ ┌─────┐ ┌───────┐ ┌────────┐ │
│ │Tab 1│ │Tab 2 │ │Tab 3 │ │
│ │Port.│ │Asset │ │Activity│ │
│ │Summ.│ │Class │ │Detail │ │
│ └──┬──┘ └──┬────┘ └──┬─────┘ │
│ │ │ │ │
│ FamilyOfficeDataService │
│ │ │ │ │
└─────┼───────┼──────────┼────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────┐
│ NestJS API │
│ FamilyOfficeController │
│ GET /portfolio-summary │
│ GET /asset-class-summary │
│ GET /activity │
│ │ │
│ FamilyOfficeService │
│ │ │
│ FamilyOfficePerformanceCalc │
│ (existing XIRR/TVPI/DPI/RVPI) │
│ │ │
│ PrismaService → PostgreSQL │
└─────────────────────────────────┘
Key Decisions
| # | Decision | See |
|---|---|---|
| R-001 | Reuse existing XIRR calculator with merged cash flows | research.md |
| R-002 | Asset class = majority PartnershipAsset.assetType | research.md |
| R-003 | Extend K1Data JSON with tax basis fields | research.md |
| R-004 | New /portfolio-views page with 3 Material tabs |
research.md |
| R-005 | Custom Angular pipe for accounting number format | research.md |
| R-006 | Page-level valuation year filter | research.md |
Data Flow
Portfolio Summary / Asset Class Summary
1. Client selects valuation year (default: current year)
2. GET /family-office/portfolio-summary?valuationYear=2025
3. Backend:
a. Load all user entities with active memberships
b. For each entity, for each membership:
- Sum capitalCommitment → Original Commitment
- Sum capitalContributed → Paid-In
- Load latest NAV ≤ year-end × ownershipPercent → Residual
- Load distributions ≤ year-end → Distributions
- Build dated cash flows (contributions, distributions, terminal NAV)
c. Per entity: compute DPI, RVPI, TVPI, XIRR from aggregated flows
d. Compute "All Entities" totals row
4. Return IPortfolioSummary
5. Client renders mat-table with accounting format pipe
Asset Class Summary follows the same flow but groups by partnership's dominant asset type instead of entity.
Activity Detail
1. Client optionally selects entity, partnership, year filters
2. GET /family-office/activity?entityId=...&year=2024&skip=0&take=50
3. Backend:
a. Find all (entity, partnership, year) combinations from:
- PartnershipMembership (entity-partnership links)
- KDocument (partnership-year income data)
- Distribution (entity-partnership dated amounts)
b. For each combination:
- Pull K1Data fields → income components
- Pull extended fields → tax basis
- Sum distributions for that year
- Compute derived fields (totalIncome, excessDistribution, etc.)
c. Apply filters, paginate
4. Return IActivityDetail with filter options
5. Client renders mat-table with conditional row highlighting
Files to Create/Modify
New Files
| File | Purpose |
|---|---|
apps/client/src/app/pages/portfolio-views/portfolio-views-page.component.ts |
Main tabbed page |
apps/client/src/app/pages/portfolio-views/portfolio-views-page.html |
Template with 3 tabs |
apps/client/src/app/pages/portfolio-views/portfolio-views-page.scss |
Styles |
apps/client/src/app/pages/portfolio-views/portfolio-views-page.routes.ts |
Route config |
apps/client/src/app/pipes/accounting-number.pipe.ts |
Custom number format pipe |
Modified Files
| File | Changes |
|---|---|
apps/api/src/app/family-office/family-office.controller.ts |
Add 3 new GET endpoints |
apps/api/src/app/family-office/family-office.service.ts |
Add getPortfolioSummary(), getAssetClassSummary(), getActivity() |
apps/client/src/app/services/family-office-data.service.ts |
Add 3 new fetch methods |
apps/client/src/app/app.routes.ts |
Add portfolio-views route |
libs/common/src/lib/interfaces/family-office.interface.ts |
Add response interfaces |
libs/common/src/lib/interfaces/k-document.interface.ts |
Extend K1Data with tax basis fields |
Reused (no changes)
| File | What's Reused |
|---|---|
apps/api/src/app/portfolio/calculator/family-office/performance-calculator.ts |
XIRR, TVPI, DPI, RVPI computations |
libs/ui/src/lib/performance-metrics/ |
Metric display patterns (reference for styling) |
Testing Strategy
| Layer | What | How |
|---|---|---|
| Unit (API) | getPortfolioSummary() aggregation logic |
Jest: mock Prisma, verify metric calculations match expected values |
| Unit (API) | getAssetClassSummary() grouping |
Jest: verify partnerships correctly grouped by asset type |
| Unit (API) | getActivity() K1Data → Activity row mapping |
Jest: verify all derived fields computed correctly |
| Unit (Client) | Accounting number pipe | Jest: verify "(1,000)", "1,000,000", "-" formatting |
| Integration | API endpoints return correct response shapes | Jest: hit endpoints with test data, verify IPortfolioSummary/etc. shapes |
| E2E | Tab navigation and data display | Manual or Playwright: navigate to /portfolio-views, switch tabs, verify data |
Dependencies on Existing Code
FamilyOfficePerformanceCalculator— Must remain stable (no breaking changes)FamilyOfficeService.getUserPartnerships()— Existing private method for scoping by user/entityPrismaService— All database accessAuthGuard— Route protectionpermissions.readEntity— Endpoint authorizationFamilyOfficeAssetTypeenum — Asset class categorizationK1Datainterface — Extended but backward-compatible