Helm Portfolio for Dummies
Syllabus Next →
Module 07 of 10

Calculations & Metrics

Understand what every number on the dashboard means, how it's computed, and where the Go calculation service fits in.

25 minutes Completed Module 6

The Two-Tier Architecture

Portfolio splits its work across two services, and understanding who does what is essential before diving into formulas.

Laravel (PHP)
  • 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.

Go (gRPC)
  • 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:

CategoryWhat It IsExamples
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
Why this matters

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

Core Metric
Committed Capital
The total amount investors have pledged to a fund. This is the promise, not the payment.
Formula SUM(cash_flows) WHERE type IN (subscription)
Example: Three LPs commit $5M, $3M, and $2M to Fund Alpha. Committed Capital = $10M. No money has moved yet — these are just signed subscription agreements.

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:

MetricFormulaWhat It Tells You
YieldIncome / InvestmentAnnual income return as a percentage of the original investment
Return on Capital(Valuation - Investment) / InvestmentPaper gain/loss as a percentage — like "unrealized P&L"
MultipleValuation / InvestmentHow many times the current value exceeds the cost basis
ProfitValuation - Investment + Cumulative DistributionsTotal 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:

1
Secant Method (Primary) Tries three different initial guesses: 0.1, -0.1, and 0.5. The Secant method approximates the derivative numerically — it doesn't need the actual derivative formula, making it more robust for irregular cash flow patterns.
2
Newton's Method (Fallback) If Secant fails to converge, falls back to Newton's method which uses the analytical derivative. Also tries three initial guesses. More precise but can fail on edge cases (e.g., near-zero derivatives).
3
Convergence Check Tolerance: 1e-7 (0.0000001). Max iterations: 1000 per attempt. If none of the six attempts converge, XIRR returns null/empty.
Cash flow sign convention

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:

StrategyUsed ForRate 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.

Why two strategies?

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.

How the Cache Works
Cache key structureTagged by tenant + asset ID(s) + investor ID(s) for surgical invalidation
When caches clearOn any data mutation — transaction creation, valuation update, deletion, import. Events fire ClearAllCaches or targeted cache tags.
Cache warmingcache:clear-and-warm --tenant={id} artisan command pre-computes common dashboard calculations so the first page load is fast
Go-side cachingExchange 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

Negative net cash displayed in parentheses

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.

Units and VPU only appear for single-investor views

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.

gRPC failure doesn't block writes

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

Question 1

A fund has $10M committed, $6M called, $5M paid-in, $2M distributed, and a current NAV of $8M. What is the TVPI?

1.67x — ($8M + $2M) / $6M
2.00x — ($8M + $2M) / $5M
1.00x — ($8M + $2M) / $10M
0.40x — $2M / $5M
Correct! TVPI = (NAV + Distributions) / Paid-In = ($8M + $2M) / $5M = 2.00x. The denominator is Paid-In (money actually received), not Committed or Called. Option D (0.40x) is the DPI, which only measures cash returned.
Not quite. TVPI = (NAV + Distributions) / Paid-In. The key is using Paid-In ($5M) as the denominator — not Committed ($10M) or Called ($6M). So: ($8M + $2M) / $5M = 2.00x. The correct answer is B.
Question 2

Why does the XIRR solver try multiple initial guesses and two different numerical methods?

To improve precision — each method gives a slightly different answer and they're averaged
Because numerical solvers can fail to converge depending on the starting point and the cash flow pattern — multiple attempts maximize the chance of finding a solution
Because Go doesn't have a built-in XIRR function, so they implemented two workarounds
Correct! Numerical solvers are iterative — they refine a guess until it converges on the answer. But depending on the shape of the curve (driven by cash flow amounts and timing), a solver can get stuck or diverge from a bad starting point. Multiple initial guesses (0.1, -0.1, 0.5) and two methods (Secant, then Newton's) maximize the odds of convergence.
Not quite. The methods don't produce different answers to average. XIRR has one correct answer — the challenge is finding it. Numerical solvers are sensitive to their starting point; from a bad initial guess, they can diverge or oscillate forever. Multiple guesses and fallback methods maximize the chance of convergence. The correct answer is B.
Question 3

A client complains that their "Capital Gains" number looks wrong — it's much higher than expected. What's the first thing to check?

Whether the Go calculation service is running correctly
Whether overwriteCGWithReturns is enabled — the field might be showing Total Returns instead of Capital Gains
Whether the currency conversion rates are up to date
Correct! When 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.
Not a bad thought, but the most likely culprit is the 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.