Calculations & Metrics
Understand what every number on the dashboard means, how it's computed, and where the Go calculation service fits in.
The Two-Tier Architecture
Portfolio splits its work across two services, and understanding who does what is essential before diving into formulas.
- All CRUD operations (create, read, update, delete)
- Cache orchestration (Redis)
- Request routing and authorization
- Report generation (PDF rendering)
Owns all writes and decides when to calculate.
- All financial calculations
- Numerical solvers (XIRR, Newton's method)
- Parallel computation via goroutines
- Currency conversion using rates DB
Owns all math and decides how to calculate.
Every dashboard view triggers gRPC calls to the Go service (or returns cached results from Redis). When a user opens the Portfolio overview, Laravel checks the cache first. On a miss, it fires a protobuf request to Go on port 50051, caches the response, and returns JSON to the frontend.
Core vs Derived Calculations
The Go service computes two categories of values:
| Category | What It Is | Examples |
|---|---|---|
| Core | Raw sums directly from the cash_flows table. These are the building blocks. |
Committed, Called, Paid-In, Distributions, Dividends, Redemptions, Investments |
| Derived | Formulas that combine core values (and sometimes NAV) to produce ratios and multiples. | DPI, TVPI, RVPI, Capital Gains, Uncalled Capital, XIRR |
If a core value is wrong (say, a cash flow is miscategorized), every derived metric that depends on it will also be wrong. Debugging a bad DPI? Start by checking the underlying Paid-In and Distributions totals, which come from individual cash flow records.
The Key Metrics
Use the explorer below to understand each metric. Click a metric to see its formula, a plain-English explanation, and a worked example.
Metric Explorer
Direct Investment Metrics
Direct investments (equity stakes, real assets) use a different set of metrics that focus on individual investment performance rather than fund-level aggregates:
| Metric | Formula | What It Tells You |
|---|---|---|
| Yield | Income / Investment | Annual income return as a percentage of the original investment |
| Return on Capital | (Valuation - Investment) / Investment | Paper gain/loss as a percentage — like "unrealized P&L" |
| Multiple | Valuation / Investment | How many times the current value exceeds the cost basis |
| Profit | Valuation - Investment + Cumulative Distributions | Total profit including both paper gains and cash received |
XIRR Deep Dive
XIRR is the most complex calculation in Portfolio. Unlike the other metrics (which are simple arithmetic), XIRR requires a numerical solver — there's no closed-form formula. The Go service uses a multi-strategy approach to find the answer:
1e-7 (0.0000001). Max iterations: 1000 per attempt. If none of the six attempts converge, XIRR returns null/empty.
Cash outflows (money leaving the investor — investments, capital calls) are negative. Cash inflows (money coming back — distributions, the current valuation) are positive. The solver finds the discount rate that makes the sum of all present-valued cash flows equal to zero.
Currency Conversion
Multi-currency portfolios are common — a London-based LP might hold USD, EUR, and GBP assets. The Go service uses two different conversion strategies depending on what's being converted:
| Strategy | Used For | Rate Lookup |
|---|---|---|
| By Cash Flow Date | Paid-In, Distributions, Capital Calls | Exchange rate on the settlement date of each individual cash flow |
| By Cutoff Date | Committed Capital, NAV, Investments | Exchange rate on the reporting date (a single rate for the whole batch) |
Rates come from a separate Service Rates PostgreSQL database. The Go service caches exchange rates in-memory using bigcache with a 24-hour TTL to avoid hammering the rates database on every request.
Cash flow amounts happened at specific moments — you want the rate that was in effect when the money actually moved. But committed capital and NAV are point-in-time snapshots — you want them all converted at a consistent rate (the reporting date) so the numbers make sense side by side.
Caching
All calculation results from the Go service are cached in Redis by the Laravel app. Without caching, every page load would trigger expensive gRPC computation.
| Cache key structure | Tagged by tenant + asset ID(s) + investor ID(s) for surgical invalidation |
| When caches clear | On any data mutation — transaction creation, valuation update, deletion, import. Events fire ClearAllCaches or targeted cache tags. |
| Cache warming | cache:clear-and-warm --tenant={id} artisan command pre-computes common dashboard calculations so the first page load is fast |
| Go-side caching | Exchange rates cached in-memory (bigcache) with 24h TTL — separate from Laravel's Redis cache |
ConfigSettings Toggles
Two settings directly affect what metrics are displayed:
overwriteCGWithReturns
When true, the "Capital Gains" field on dashboards and reports actually shows Total Returns instead. Same UI label, different number. This catches people off guard.
showIRR
When false, XIRR is hidden from all dashboards and reports. Some clients prefer not to show IRR if their fund is young (early IRR numbers can be misleadingly extreme).
Gotchas
When net cash position is negative (investor received more than they paid), the UI shows it as ($500,000) with parentheses instead of a minus sign. This is standard accounting convention but confuses developers who expect negative signs.
The "Units" and "Value Per Unit" columns are only displayed when filtering to exactly one investor. Multi-investor views aggregate amounts, so per-unit data doesn't make sense and is hidden. If a user asks "where are the units?" — check their filter scope.
If the Go calculation service goes down, transaction writes still succeed — the Laravel app saves data to PostgreSQL regardless. But all dashboard calculations will fail. This creates inconsistent atomicity: data can be written that triggers cache invalidation, but new calculations can't be computed until the Go service recovers.
Knowledge Check
A fund has $10M committed, $6M called, $5M paid-in, $2M distributed, and a current NAV of $8M. What is the TVPI?
Why does the XIRR solver try multiple initial guesses and two different numerical methods?
A client complains that their "Capital Gains" number looks wrong — it's much higher than expected. What's the first thing to check?
overwriteCGWithReturns is enabled, the "Capital Gains" label on the dashboard actually displays Total Returns — a different (usually larger) number. This is the most common source of confusion. Same label, different calculation underneath.overwriteCGWithReturns config setting. When enabled, the UI field labeled "Capital Gains" actually shows Total Returns instead — which is typically a larger number. Always check config toggles before debugging the calculation engine. The correct answer is B.What's Next
You now understand what every number on the dashboard means and how it's computed. Next, we'll look at configuration, reports, and the UI — how tenant settings shape the experience, how PDFs are generated, and what users actually see when they log in.