Methodology
How every number is computed
No black boxes. Each figure below is one formula over your imported trades and the closes fetched for them, and each names the file that implements it.
Portfolio value
Every other figure is derived from one series: what the open positions were worth at each close.
value(d) = Σ over tickers shares_held(t, d) × last_close(t, on or before d)Trades are replayed in date order to give the net shares held in each ticker on every date, and each holding is valued at its most recent close on or before that date. A ticker with no close yet published carries its previous one forward rather than dropping to zero.
The series begins at your first trade date. Price bars published before it are still loaded, because they are what seeds the carry-forward value on the first day.
- Cash is not tracked
- This is the market value of open positions, not of an account. Proceeds from a sale leave the series entirely rather than sitting as cash, so a fully liquidated portfolio reads as zero.
- Dividends, splits, and fees are not modeled
- Closes are taken as published. An unadjusted split will show up as a step change in value, and reinvested dividends will not show up at all.
Implemented in src/lib/analytics/portfolio.ts
Daily returns
Simple period-over-period change, the input to everything below.
r(i) = (value(i) − value(i−1)) / value(i−1)Simple rather than logarithmic returns, because the figures built on them — Sharpe, volatility — are conventionally quoted on simple returns and mixing the two produces numbers that cannot be compared against anything published elsewhere.
A period opening at zero or below produces no return at all rather than an infinite one. A portfolio that was fully liquidated and later rebuilt therefore yields a shorter return series than its value series, which is the honest outcome: the percentage change from nothing is undefined, not enormous.
Implemented in src/lib/analytics/math.ts
Maximum drawdown
The deepest fall from a high-water mark, and how long it lasted.
maxDD = max over d (peak(≤ d) − value(d)) / peak(≤ d)A single pass carries the running peak and records the largest proportional fall below it. The result is reported as a positive fraction, together with the index of the peak it fell from and the trough it fell to, so the decline can be annotated on the chart rather than merely stated.
Recovery is measured separately: the number of trading days from the trough until the series first regains the old peak. A portfolio still below that peak reports no recovery length at all — 'still underwater' and 'recovered immediately' are opposite facts and must not render the same.
- Close-to-close, not intraday
- Only end-of-day prices are fetched, so a fall that reversed within a single session is invisible here. A drawdown measured tick by tick would be deeper.
Implemented in src/lib/analytics/metrics.ts
Volatility and annualized return
The size of the swings, and the growth rate that survived them.
σ = stdev(r) × √252
CAGR = (value_last / value_first)^(252 / periods) − 1Volatility is the sample standard deviation of daily returns scaled by the square root of 252, the conventional count of trading days in a year.
The annualized return compounds rather than averaging. It reads only the first and last values, which makes it blind to the path between them — a smooth climb and a crash followed by a full recovery produce the same figure, which is exactly why it is shown beside the drawdown rather than instead of it.
Implemented in src/lib/analytics/math.ts
Correlation
Pearson correlation of daily returns, over aligned dates only.
ρ(a,b) = Σ (a_i − ā)(b_i − b̄) / √( Σ(a_i − ā)² · Σ(b_i − b̄)² )Every pair is measured over the same dates. The intersection of all holdings' price histories is taken first, and returns are computed within it, because correlating observations from different days is a silent error that produces a completely plausible-looking number.
A ticker bought last month therefore truncates the window for everything it is compared against. Any holding that still has gaps after the intersection is excluded and named, rather than quietly omitted from the matrix.
Below twenty shared observations no matrix is shown at all. A correlation drawn from a handful of days is noise, and presenting it in a colored grid would lend it an authority it has not earned.
- Two-pass formulas throughout
- Every routine subtracts the mean before summing squares rather than using the algebraically equivalent E[x²] − E[x]² shortcut, which loses precision when values share a large offset and can return a negative variance whose square root is NaN.
- No variance means no correlation
- A holding whose price never moved has no direction to share, so its pairs report zero rather than dividing by a zero spread.
Implemented in src/lib/analytics/correlation.ts
Cost basis and profit
Average-cost accounting, replayed in date order.
avg_cost = basis / shares
realized += shares_sold × (sell_price − avg_cost)The ledger is replayed oldest first. Buys add to shares and to basis; sells reduce both proportionally and bank the difference as realized profit. Order matters — pricing a sell against purchases that had not happened yet would produce a figure that looks fine and is wrong.
A sell larger than the recorded position, which usually means an opening buy was never imported, is accounted for only up to the shares the ledger knows about. Realizing profit on shares with no recorded cost would invent money.
- Average cost, not FIFO
- Tax lots (FIFO, LIFO, specific identification) produce different realized figures and are what a tax return needs. Average cost answers 'am I up on this position', which is what a risk dashboard is for. Do not file these numbers.
Implemented in src/lib/analytics/positions.ts
Terms used above
Definitions of every figure here, plus the way each one is commonly misread, are on the glossary.