Statistics

Portfolio and performance analytics on-chain — mean, std dev, historical volatility, Sharpe, max drawdown, VaR, CVaR. Array-based functions scale linearly with input size; gas figures below are quoted at N = 30.

Contract: Stats.sol

Functions

FunctionGasDescription
geometricMean330sqrt(a · b) — Uniswap V2 invariant
mean6,980 @ 30Arithmetic mean
stdDev15,298 @ 30Sample standard deviation (Bessel-corrected)
weightedAverage15,687 @ 30Σ(v · w) / Σ(w)
historicalVolatility25,915 @ 30Annualized volatility from log returns
sharpeRatio26,053 @ 30Risk-adjusted return
maxDrawdown15,470 @ 30Peak-to-trough decline
valueAtRisk34,531 @ 30Historical VaR (NumPy-compatible)
conditionalValueAtRisk31,889 @ 30Expected shortfall (left-tail mean)

npm install defimath-lib

Conventions

  • Array inputs are uint256[] calldata, 18-decimal fixed-point. Each element must fit within MAX_VALUE.
  • prices arrays are oldest-first; returns are derived internally as ln(pi / pi−1).
  • intervalSecuint32, sampling interval in seconds (e.g. 1 days for daily prices). Used to annualize.
  • riskFreeRateAnnualuint64, annualized rate, 18-decimal fixed-point.
  • confidenceuint64, the α level for VaR / CVaR, in (0, 1) exclusive (e.g. 0.95e18 for 95%).
  • Return type mirrors sign: int256 for quantities that can be negative (Sharpe, VaR, CVaR); uint256 otherwise.
  • All functions are internal pure.

Quick example

solidity
import "defimath-lib/contracts/finance/Stats.sol";

// 30 daily closing prices, 18-decimal fixed-point.
uint256[] memory prices = loadPriceSeries();

// Sample mean and std dev of the raw values.
uint256 mu    = DeFiMathStats.mean(prices);
uint256 sigma = DeFiMathStats.stdDev(prices);

// Annualized volatility from log returns (1-day interval).
uint256 vol = DeFiMathStats.historicalVolatility(prices, 1 days);

// Sharpe ratio at a 2% risk-free rate.
int256 sharpe = DeFiMathStats.sharpeRatio(prices, 1 days, 0.02e18);

Important notes

  • Gas scales linearly with array size. The figures in the table are at N = 30 (a typical month of daily prices). Doubling the array roughly doubles the gas; arrays larger than MAX_ARRAY_LENGTH (1024) revert.
  • historicalVolatility annualizes internally. Pass raw prices and the sampling interval in seconds — the function computes log returns, takes the sample std dev, and scales by √(SECONDS_IN_YEAR / interval). Don't pre-annualize.
  • sharpeRatio is annualized. The function uses annualized return minus annualized risk-free rate over annualized volatility. Pass the risk-free rate as a yearly value regardless of price-series interval.
  • VaR vs. CVaR. valueAtRisk returns the return threshold at the chosen confidence — a probability statement. conditionalValueAtRisk returns the expected return conditional on being beyond that threshold (the left-tail mean). CVaR ≤ VaR (more conservative).
  • VaR uses NumPy-compatible linear interpolation. Matches numpy.quantile(..., method="linear") and simple-statistics.quantile. Useful when reconciling on-chain risk numbers with off-chain notebooks.
  • stdDev is sample (Bessel-corrected). Divides by n − 1, not n. Matches numpy.std(..., ddof=1).

Every function reverts on out-of-bounds inputs with a named error — see the per-function pages for limits and error specifics.

Testing

Hardhat correctness layer. 114 tests across 9 function groups. valueAtRisk / conditionalValueAtRisk validated against simple-statistics.quantile (NumPy-compatible linear interpolation); volatility, Sharpe ratio, and max drawdown validated against inline JS references over series of 30–100 prices. Limits coverage at boundary inputs (single-element arrays, max array length, MAX_VALUE per element, confidence approaching 0 and 1).

Foundry property-fuzz layer. 12 mathematical properties × 32,000 random runs each = 384,000 random executions per CI run.

CategoryCountWhat they check
Monotonicity1geometricMean ↑ in first argument
Identities7geometricMean(a, a) = a, mean(constant) = constant, stdDev(constant) = 0, maxDrawdown(increasing) = 0, weightedAverage(equal_weights) = mean, mean scales linearly (mean(2·v) = 2·mean(v)), stdDev scales linearly
Output bounds3min ≤ mean ≤ max, min(a, b) ≤ geometricMean ≤ max(a, b), maxDrawdown ∈ [0, 1]
Symmetries1geometricMean(a, b) = geometricMean(b, a) — argument-order independence

Sources: test/Stats.test.mjs · test/foundry/Stats.t.sol