Performance methodology
How Robinwood Stocks computes the track record it publishes, why NAV is read from the contract rather than modelled, what the gaps mean, and how to recompute every number yourself.
In this industry a track record is a screenshot. Ours is a URL, and this page is the part that makes that claim checkable: the exact method, the things it deliberately does not do, and how to reproduce it.
Live numbers: app.rwoodstocks.xyz/performance.
Raw dataset: /api/track-record.
Methodology version 1.0.0. Any change that moves a published historical number gets a version bump and a note here. Numbers do not change quietly.
The one decision that matters
We do not model NAV.
The usual way to build a performance layer is to reconstruct holdings, multiply by prices from some source, and hope the result tracks reality. The usual way it dies is divergence: the calculator and the contract gradually disagree, and the published chart becomes fiction with a confident axis.
Instead we ask the basket. Every BasketToken2 exposes nav18(), which
prices its own holdings through the same registered feeds its policy
checks use, and an archive node can answer that call at any past block. So
each point in the series is the contract's own answer at that block,
divided by its totalSupply() at the same block:
navPerShare(t) = nav18() @ block(t) / totalSupply() @ block(t)There is no calculator to diverge, because there is no calculator. What we add on top is arithmetic anyone can redo by hand.
The series
- Cadence. One point per UTC day, plus two extra anchors: the moment of the first mint, and the present.
- Where it starts. At the first mint, not at deployment. A basket exists before anyone owns it, and between the two there is no share price to report. The opening number is therefore what the first holder actually minted at, which is not necessarily the $100 par the units were built for: if the market moved between deployment and the first mint, the opening number shows it.
- Block resolution. Timestamps map to blocks by bisection on real block timestamps, so a "midnight" point is the last block at or before that midnight. This chain's block rate varies by orders of magnitude across its life, so interpolation alone would not be honest enough.
Gaps, and why they are not filled
When nav18() cannot answer, the point is a gap. It is never interpolated,
never carried forward, and never quietly skipped. The dataset says which
kind it is:
no-supply— nothing had been minted yet, so a share price does not exist. Not a failure, just an empty basket.no-price— the call reverted because a constituent's feed was stale or its TWAP window uncovered. For baskets holding Stock Tokens this is mostly the weekend, when those feeds stop publishing. The basket was fine; the honest answer was "no price right now", and the series says so.
A chart with holes in it is telling you something a smooth line would have hidden.
Total return
The headline series reinvests distributions at the moment they were paid:
TR(0) = 100
TR(t) = TR(t-1) × ( navPerShare(t) + distributedPerShare(t) ) / navPerShare(t-1)distributedPerShare comes from the basket's BasketDistributor
CycleFinished events, in USDG, divided by supply at that point. This
matters because agentic baskets pay surplus out of NAV: a naive price
chart would show the payout as a loss, which is the opposite of what
happened to the holder.
Gap points are skipped in the chaining, not treated as zeros.
Risk metrics
- Max drawdown: peak-to-trough on the total-return series, in percent. Displayed in the same row as the return, always, never behind a click.
- Days live: from first mint to now.
- Rotations: count of
Rebalancedevents, each one linkable to its transaction. - Paid out: cumulative USDG distributed.
Ranking rules
- A basket needs 14 days live to be ranked. Younger ones appear in a separate "new" section, shown but not ranked. A five-day number describes the week it landed in, not the strategy.
- No basket is ever removed for performing badly. Survivorship bias is the specific failure this system exists to avoid, and a leaderboard you can be dropped from is a leaderboard that lies by omission.
- First-party and third-party baskets are computed and displayed identically. Ours get no favourable treatment, and none of the rules above have an exception for us.
What this is not
- Not a backtest. Backtests, where they exist, live on the basket's own page, labelled, with their window stated. They are never spliced into this series and never plotted on the same line.
- Not a forecast. These are live results over a very short window.
- Not fee-adjusted for entry. The series is the vehicle's gross result. A minter pays the basket's mint fee once at entry, which the series does not deduct; redemption is free and ungated.
- Not investment advice.
Reproducing it
The builder is scripts/agent/trackRecord.ts in the public repository. It
needs an archive RPC (the chain's public endpoint answers "metadata is not
found" beyond the head, so a third-party archive endpoint is required) and
nothing else. Point it at the same blocks and it produces the same file.
The served dataset also carries a live cross-check: at request time the API
reads each basket's nav18() now and reports driftPct against the last
published point. On a volatile basket that drift is market movement, not
error, which is why it is published as a number rather than a verdict. A
suspect flag is raised only when the gap is too large to be movement.
If you recompute our series and get a different answer, that is a bug we want to hear about, and it will be fixed and versioned like any other.
Manipulation cost
What moving each Robinwood Stocks feed actually costs, computed from live pool state and published openly — model, formulas, assumptions, and the honest verdict that today every native feed is thin.
Trust model
What the contracts guarantee, mechanically, and the exact list of what remains.