portfolio_performance_metrics
  1. Metrics
  • Portfolio Performance Metrics with External Cashflows
  • Core
  • IO
  • Metrics
  • CLI
  • Tests (nbdev notebook)

On this page

  • What compute_metrics does
    • MetricsResult
  • Imputing Missing Valuations via Constant-Rate Interpolation
    • Segment Simulation Logic
    • Solving for the Growth Rate
    • Example Illustration
    • Why Constant-Rate Interpolation?
    • Summary
    • impute_sparse_valuations
  • compute_metrics
    • compute_metrics
  • Report an issue

Other Formats

  • CommonMark

Metrics

Author: cs224

Last updated: 2025-12-15

Python implementation: CPython
Python version       : 3.12.9
IPython version      : 9.8.0

numpy     : 2.3.5
pandas    : 2.3.3
scipy     : 1.16.3
nbdev     : 2.4.6
matplotlib: 3.10.8

What compute_metrics does

  1. determine start/end measurement window (first/last real valuation)
  2. validate cashflows in window
  3. strict vs lenient missing valuations
  4. compute TWR + unitization (if possible), Dietz, XIRR
  5. compute annualization

MetricsResult

 MetricsResult (summary:pandas.core.frame.DataFrame,
                nav:pandas.core.frame.DataFrame)

The first and last valuation define the measurement period.

As valid inputs it needs ≥2 valuations and it will sort by date.

Between two valuation support points, assume a constant growth rate (r) and apply cashflows to simulate interim valuations.

Solve for (r) with root finding so the simulated end valuation matches the next support valuation.

Imputing Missing Valuations via Constant-Rate Interpolation

When computing Time-Weighted Returns (TWR) or unitization, we often require a portfolio valuation on every cashflow date. However, real-world datasets may contain:

  • missing valuations on some cashflow dates,
  • valuations only at monthly or quarterly intervals,
  • irregular sets of flows and valuations.

This function provides a lenient mode that reconstructs missing valuations by assuming that, between two known valuation points, the portfolio grew at a constant annualized rate (applied as \((1+r)^{\Delta t}\) using ACT/365F), while external cashflows impact the valuation in the usual way.

Suppose the input DataFrame contains columns:

  • date: chronological sequence of dates
  • cashflow: investor-view cashflows (deposits negative, withdrawals positive)
  • valuation: post-flow valuation (some values may be NaN)

Example:

date cashflow valuation
2025-01-01 0 100
2025-07-01 -10 NaN
2026-01-01 0 110

Valuation is missing on 2025-07-01, but there is a cashflow on that date, so TWR requires a valuation.

To fill this missing valuation, the algorithm:

  1. Identifies valuation support points, i.e. rows where valuation is known. These act as segment boundaries.

  2. For each consecutive pair of support rows \((t_0, V_0),\quad (t_1, V_1),\) it assumes:

    • the portfolio grows at a constant rate \(r\) between \(t_0\) and \(t_1\),
    • external cashflows are applied on their dates,
    • valuations at missing rows should follow this growth+flow simulation,
    • and the simulated final valuation at \(t_1\) must match the known valuation \(V_1\).

Thus we solve for the growth rate \(r\) that makes the simulated terminal valuation correct.

Segment Simulation Logic

For a segment between support indices i0 and i1, we simulate valuation row by row in date order:

Let:

  • \(V_j\) = portfolio value just after applying all flows on date \(j\)
  • \(dt = \operatorname{year\_fraction}(t_{j-1}, t_j)\)

At each date:

  1. Grow the portfolio from last valuation:
    \(V_{\text{pre}} = V_{j-1} \cdot (1+r)^{dt}\)
  2. Apply investor-view cashflow \(\text{cf}_{\text{inv}}\):
    • Convert to portfolio view
      \(\text{cf}*{\text{port}} = -\text{cf}*{\text{inv}}\) (Deposits increase portfolio value; withdrawals decrease it.)
    • Update valuation:
      \(V_j = V_{\text{pre}} + \text{cf}_{\text{port}}\)

If the row’s valuation is missing, we optionally write the computed \(V_j\) back into the DataFrame.

We continue until reaching index i1, returning the simulated terminal valuation.

Solving for the Growth Rate

To match the known end valuation (V_1), we solve: \[ f(r) = \text{simulate}(r) - V_1 = 0. \]

This is a 1-dimensional root-finding problem. The implementation uses:

  • bracketing (expand interval until a sign change is found),
  • Brent’s method (scipy.optimize.brentq) to find the unique root.

If no bracketed root exists, the segment is inconsistent with constant-rate growth and the function raises:

ValueError("Cannot impute valuations in segment: no feasible constant-rate solution.")

Example Illustration

Between two known valuations:

date cashflow valuation
2025-01-01 0 100
2025-07-01 -10 NaN
2026-01-01 0 110

We solve for rate \(r\) such that:

  1. Grow 100 from Jan → Jul, apply deposit -10 → obtain simulated Jul valuation.
  2. Grow from Jul → Jan (6 months), apply no flows → get simulated Jan valuation.
  3. Force this January simulation to equal 110.

Result: a unique growth rate \(r\) and an imputed valuation at 2025-07-01.

Why Constant-Rate Interpolation?

This approach is:

  • Cashflow-aware: flows change valuation; interpolation respects these jumps.
  • Time-consistent: growth depends on actual day count.
  • Economically meaningful: constant-rate assumption is reasonable when only endpoints are known.
  • Needed for TWR: TWR requires valuations on each cashflow date.

This is not linear interpolation - which would incorrectly ignore compounding and cashflow effects. Instead, this is financially coherent interpolation.

Summary

impute_sparse_valuations reconstructs missing valuations by:

  • dividing the timeline into valuation-support segments,
  • assuming a constant growth rate inside each segment,
  • applying flows exactly on their dates,
  • solving for a rate that makes the simulated end valuation match the actual known valuation,
  • filling in missing valuations accordingly.

This makes downstream return calculations (TWR, unitization) mathematically sound even when the input dataset is sparse.


impute_sparse_valuations

 impute_sparse_valuations (df:pandas.core.frame.DataFrame)

Fill missing post-flow valuations on cashflow dates via constant-rate interpolation.

# Simple sanity test for `impute_sparse_valuations` using a deposit-only example.
#
# Setup:
# - Start valuation: 100 on 2025-01-01
# - Mid-date deposit: -10 (investor view) on 2025-07-01, valuation missing
# - End valuation: 110 on 2026-01-01
#
# Economic story:
# - Portfolio earns 0% over the whole year.
# - At mid-date we add 10 of value (deposit of -10 investor view ⇒ +10 portfolio view).
# - To end at 110 after one year with 0% growth, the mid valuation must be 110 as well.
#
# So after imputation we expect:
# - all valuations non-null
# - start valuation unchanged at 100
# - end valuation unchanged at 110
# - imputed mid valuation equal to 110 (up to tiny numerical noise)


df = pd.DataFrame(
    [
        {"date": "2025-01-01", "cashflow": 0.0,  "valuation": 100.0},
        {"date": "2025-07-01", "cashflow": -10.0, "valuation": float("nan")},
        {"date": "2026-01-01", "cashflow": 0.0,  "valuation": 110.0},
    ]
)

df["date"] = pd.to_datetime(df["date"]).dt.date

df_imputed = impute_sparse_valuations(df.copy())

# No missing valuations anymore
assert not df_imputed["valuation"].isna().any()

# Start and end valuations unchanged
assert isclose(df_imputed.loc[0, "valuation"], 100.0, rel_tol=1e-12, abs_tol=1e-12)
assert isclose(df_imputed.loc[2, "valuation"], 110.0, rel_tol=1e-12, abs_tol=1e-12)

# Mid valuation should be equal to 110 under 0% growth + 10 deposit
assert isclose(df_imputed.loc[1, "valuation"], 110.0, rel_tol=1e-12, abs_tol=1e-12)
# df_imputed

compute_metrics

  • Investor flows are used for XIRR and unitization
  • Portfolio flows are used for TWR and Dietz
  • Strict mode leaves TWR as NaN if valuations missing on cashflow dates

compute_metrics

 compute_metrics (load_result:portfolio_performance_metrics.io.LoadResult,
                  lenient_missing_valuations:bool=False)
  • Report an issue