GMI · TECHNOLOGY OBSERVATORY // ALL SYSTEMS NOMINAL
ENGINEERED BY LEOPARD DATA

Portfolio Benchmarking: Saying What the Number Measures

Wave 14 compares a position report against SPY or another index ETF: returns over seven horizons, a growth curve, and the standard risk statistics. The math is ordinary. The engineering judgment is in being exact about what the comparison is of, and in making sure the dashboard and the Excel download can't disagree.

BUILT · SEPTEMBER 2026 ClientWeb · AdminWeb · MAUI · Excel

Decision: Name It a "Held-Through" Comparison

A position report stores share counts and an average cost, but no purchase dates. That rules out a true time-weighted return of your trading history. There were three options: ask for dates on every holding (friction nobody would accept), make up a history from the average cost (a number that looks precise and isn't), or answer a narrower question honestly.

GMI answers the narrower question: the positions you hold today, held unchanged through each trailing window, against the benchmark over the same window. It's a useful question ("is what I own now keeping up?"), and the UI notes, the API DTO and the Excel sheet all say it isn't your trading record.

ONE CALCULATOR, TWO OUTPUTS
rendering diagram…
flowchart LR
    PR[Position report<br/>shares + avg cost,<br/>no purchase dates] --> API[Benchmark endpoint<br/>admin-or-owner]
    PR --> ENG[Report engine<br/>combined workbook]
    CACHE[(fmppricecache<br/>via caching decorator)] --> API
    CACHE --> ENG
    API --> CALC[PortfolioBenchmarkCalculator<br/>pure, no I/O]
    ENG --> CALC
    CALC --> UI[Compare to Benchmark panel<br/>web, admin, MAUI]
    CALC --> XLS[PortfolioBenchmark sheet<br/>Excel download]
The API endpoint and the report engine both call the same pure calculator with the same price series, so the dashboard panel and the PortfolioBenchmark sheet in the Excel download show the same numbers.

What It Computes

MeasureDetail
Trailing returns1M, 3M, 6M, YTD, 1Y, 3Y, 5Y side by side, with the difference in points; 3Y and 5Y also annualized
Growth curvePortfolio and benchmark from the same starting dollars (long windows thinned for the chart)
RiskSharpe, beta, correlation, Jensen's alpha, tracking error, information ratio, max drawdown
AttributionPer-holding return and contribution
BenchmarksSPY, ONEQ, QQQ, DIA, IWM, VTI, or any ticker the user types

Benchmarks are ETFs, not index levels. Index levels and constituents aren't in the market-data contract (settled in August 2026: they need separate index-provider licensing), and an ETF is also the more honest comparison, because it's something you could actually have bought, fees included.

Handling Missing Data Without Hiding It

  • Coverage is reported, never silently dropped. A holding without price history back to a window's start (a recent IPO, say) is listed as not covered for that window. Quietly dropping it would flatter or penalize the portfolio without saying so.
  • Window starts are forgiving by exactly five days. A window that starts on a weekend or holiday uses the last close on or before it, or the first close within 5 calendar days if the series starts just after. Anything listed later than that isn't covered.
  • Risk statistics need 15 observations. Below that, Sharpe, beta and the rest are withheld rather than shown with false precision.
  • The risk-free rate is disclosed. Sharpe and Jensen's alpha use BIL's total return when dividend-adjusted history exists (BIL's price alone barely moves; its return is almost all distributions). Otherwise they use an assumed 4.0% (configurable), and the notes say which one was used.
  • Bounded work. A report above 150 holdings is compared on its largest-cost positions (stated), and prices are fetched at most 6 at a time.

Why the Math Has No I/O

An earlier lesson from the report engine: when the dashboard and the downloadable workbook each had their own math path, they drifted, and the two views disagreed about the same portfolio. So PortfolioBenchmarkCalculator is a static class with no I/O. The API's service and the report engine each load prices and hand them over, and the calculator does the rest. That also makes every number unit-testable against hand-computed series (22 unit tests, plus 2 engine tests for the Excel sheet).

Prices go through the existing provider-cache decorator as a new named consumer (PortfolioBenchmark). The cache warmer already keeps five years for every tracked symbol and SPY is kept warm by the macro engine, so in practice only a cold custom benchmark costs a provider call, and that call is written through to the cache.