Weighted statistics and empirical risk¶
Batch statistics¶
These stateless functions accept signed observations. A negative observation is a loss; value at risk (VaR) and expected shortfall (ES) return a nonnegative loss magnitude. Pass one nonnegative weight per observation, or omit weights to give every observation weight one. Inputs must be finite and the total weight must be positive.
The mean is Σ(wᵢ xᵢ) / Σwᵢ. Sample variance uses the core's observation-count
correction, N/(N-1) × Σ(wᵢ (xᵢ - mean)²) / Σwᵢ, even when weights differ or
some are zero. Standard deviation is its square root. Mean requires at least
one observation; variance and standard deviation require at least two.
Percentile accepts a fraction in (0, 1] and returns the first sorted value
whose cumulative weight reaches that fraction of total weight. VaR and ES
accept a confidence level in [0.9, 1). VaR is the negative of the lower
1 - confidence percentile, floored at zero. ES averages observations
strictly below the negative VaR threshold, negates that average, and floors
it at zero. ES raises an error when that tail has no positive-weight samples.
The remaining conditional risk measures use the same signed observations.
semi_variance measures squared distance below the weighted mean;
downside_variance uses zero; regret uses a supplied target. They apply the
count correction to the number of observations strictly below the threshold,
including zero-weight observations, and require at least two such observations
with a positive total tail weight. Their deviation forms are square roots.
shortfall(target) is the weighted probability below target, and
average_shortfall(target) is the conditional mean of target - observation
there. Average shortfall requires a positive-weight tail; shortfall returns zero
if none lies below target. Targets must be finite.
potential_upside(confidence) is the upper percentile floored at zero, with
confidence in [0.9, 1). top_percentile(probability) walks the sorted
observations from highest to lowest and returns the first value whose
cumulative weight reaches probability in (0, 1].
from itofin import statistics
observations = [-5.0, -2.0, 1.0, 2.0]
weights = [1.0, 10.0, 1.0, 8.0]
assert statistics.value_at_risk(observations, 0.9, weights=weights) == 2.0
assert statistics.expected_shortfall(observations, 0.9, weights=weights) == 5.0
assert statistics.mean([1.0, 3.0], weights=[1.0, 3.0]) == 2.5
assert statistics.shortfall(observations, 0.0, weights=weights) == 11 / 20
assert statistics.top_percentile(observations, 0.5, weights=weights) == -2.0
import itofin "github.com/benbenbang/libitofin/sdk/go"
observations := []float64{-5, -2, 1, 2}
weights := []float64{1, 10, 1, 8}
var90, err := itofin.StatisticsValueAtRisk(observations, weights, 0.9)
if err != nil { panic(err) }
es90, err := itofin.StatisticsExpectedShortfall(observations, weights, 0.9)
if err != nil { panic(err) }
_, _ = var90, es90
missed, err := itofin.StatisticsShortfall(observations, weights, 0)
if err != nil { panic(err) }
upper, err := itofin.StatisticsTopPercentile(observations, weights, 0.5)
if err != nil { panic(err) }
_, _ = missed, upper
Use nil weights in Go, or omit weights in Python, for unit weights. The
same signed-observation and tail conventions apply in both languages.
Stored samples¶
GeneralStatistics retains observations so you can add data and query moments,
percentiles, and empirical risk repeatedly. add_batch is atomic: an invalid
value, weight, or length leaves the prior sample set unchanged. A zero-weight
observation increases samples and contributes to count-based moment
corrections. Weight-dependent queries need a positive total weight; min/max
need only samples. reset empties the set.
import itofin "github.com/benbenbang/libitofin/sdk/go"
session, err := itofin.NewSession()
if err != nil { panic(err) }
defer session.Close()
samples, err := session.NewGeneralStatistics()
if err != nil { panic(err) }
defer samples.Close()
if err := samples.AddBatch([]float64{-10, -5, 1}, []float64{1, 2, 17}); err != nil { panic(err) }
var90, err := samples.ValueAtRisk(0.9)
if err != nil { panic(err) }
_ = var90
Python owns the accumulator directly. Go accumulators belong to their creating session; close each handle and the session when finished. The same count-based variance and signed-loss rules as the batch functions apply.
Incremental statistics¶
IncrementalStatistics keeps weighted moments in fixed memory as observations
arrive. It accepts finite values and finite nonnegative weights; a zero-weight
value still counts and can set the minimum or maximum. A batch is added all at
once or leaves the accumulator unchanged on error. reset clears the samples.
Variance, standard deviation, error estimate, skewness and kurtosis use the core's observation-count corrections. Downside statistics include strictly negative observations. Downside variance requires two such observations with positive combined weight. Percentile, VaR and ES require the stored-sample statistics facade; the incremental accumulator has no empirical tail queries.
session, err := itofin.NewSession()
if err != nil { panic(err) }
defer session.Close()
stream, err := session.NewIncrementalStatistics()
if err != nil { panic(err) }
defer stream.Close()
err = stream.AddBatch([]float64{-4, -2, 2, 8}, []float64{1, 2, 1, 0})
if err != nil { panic(err) }
mean, err := stream.Mean()
if err != nil { panic(err) }
_ = mean
Weighted vector statistics¶
Sequence batch functions treat each row as one observation and each column
as one component. All rows must have the same positive width. Pass one optional
nonnegative weight per row, not per component; omitted Python weights or Go
nil weights mean one per row. No NumPy/pandas array conversion or additional Go dependency is
needed. Missing observations, NaN and infinities are rejected: callers align
and clean their own data, including calendar alignment.
Python statistics function |
Go function | Result |
|---|---|---|
sequence_mean |
StatisticsSequenceMean |
One weighted mean per column |
sequence_variance |
StatisticsSequenceVariance |
One count-corrected variance per column |
sequence_standard_deviation |
StatisticsSequenceStandardDeviation |
Square roots of those variances |
sequence_error_estimate |
StatisticsSequenceErrorEstimate |
sqrt(variance / N) per column |
sequence_minimum / sequence_maximum |
StatisticsSequenceMinimum / StatisticsSequenceMaximum |
Column extrema, including zero-weight rows |
covariance_matrix / correlation_matrix |
StatisticsCovariance / StatisticsCorrelation |
Symmetric column-by-column matrices |
Let W = Σwᵢ, μⱼ = Σ(wᵢ xᵢⱼ)/W, and N be the number of rows.
Covariance entry (j, k) is
N/(N-1) × Σ[wᵢ (xᵢⱼ - μⱼ)(xᵢₖ - μₖ)]/W.
Its diagonal is the sequence variance. This is observation-count correction,
not frequency-weight or effective-sample-size correction. Zero-weight rows still
increase N, change the correction/error estimate and can set extrema.
Correlation is covariance divided by the two standard deviations, with these explicit constant-column conventions:
- Diagonal entries are one, including constant columns.
- Two zero-variance columns have correlation one.
- One zero-variance and one varying column have correlation zero.
Nonconstant correlations can differ from [-1, 1] by a few binary64 ULPs;
values are not clipped. Python converts sequence arguments before validating
shape; the limits below bound native storage, flattening and output allocation.
Covariance uses centered products, avoiding QuantLib's raw-product cancellation
for large offsets. For example, rows [10¹²+i, 10¹²+2i] for i=0..3 have
covariance [[5/3, 10/3], [10/3, 20/3]], not a negative variance from subtracting
large raw moments. This is an intentional stability correction; ordinary inputs
retain the pinned native conventions.
from itofin import statistics
rows = [[1.0, 4.0], [3.0, 2.0], [100.0, -20.0]]
weights = [1.0, 3.0, 0.0]
assert statistics.sequence_mean(rows, weights=weights) == [2.5, 2.5]
covariance = statistics.covariance_matrix(rows, weights=weights)
correlation = statistics.correlation_matrix(rows, weights=weights)
assert abs(covariance[0][0] - 1.125) < 1e-12
assert abs(correlation[0][1] + 1.0) < 1e-12
assert statistics.sequence_maximum(rows, weights=weights) == [100.0, 4.0]
import itofin "github.com/benbenbang/libitofin/sdk/go"
rows := [][]float64{{1, 4}, {3, 2}, {100, -20}}
weights := []float64{1, 3, 0}
mean, err := itofin.StatisticsSequenceMean(rows, weights)
if err != nil { panic(err) }
covariance, err := itofin.StatisticsCovariance(rows, weights)
if err != nil { panic(err) }
correlation, err := itofin.StatisticsCorrelation(rows, weights)
if err != nil { panic(err) }
_, _, _ = mean, covariance, correlation
Vector results are Python list[float] or Go []float64; matrices are
Python list[list[float]] or Go [][]float64, with independent rows. Rust batch
inputs and Rust/C matrix outputs use flattened row-major storage. All functions
are stateless and return independent results, with no binding handles or sessions.
Every new sequence query, including min/max, requires a positive total weight. Mean/min/max need one row; variance, deviation, error
estimate and matrices need at least two. This stricter sequence-batch rule does
not change scalar GeneralStatistics min/max behavior.
Limits are 256 columns, 100,000 rows and 1,000,000 input values. Matrix requests
also require rows × columns² ≤ 100,000,000; output matrices have at most 65,536
entries. Invalid shapes, mismatched weight counts, negative/nonfinite weights,
zero total weight, exceeded limits and nonfinite arithmetic/results
return errors, not partial output. Tiny positive weights do not grant additional
sample-count or work-limit capacity.
Rust also provides SequenceStatistics::new(dimension) and add_weighted for
an owned accumulator; the binding APIs expose only these batch operations.
Independent native and exact-centered fixtures are in
sdk/go/testdata/sequence-statistics.
Ordered portfolio drawdown¶
Maximum drawdown accepts ordered positive equity/NAV values, not unordered return samples. It returns a nonnegative fractional running-peak loss and deterministic zero-based peak/trough indices in Rust, C, Go and Python.
Benchmark beta¶
benchmark_beta measures asset return exposure to caller-supplied benchmark
returns. The S&P 500 is one possible input, not a built-in data feed. Supply
already aligned dates and the same return frequency; missing-data filtering,
return calculation and any desired risk-free adjustment are the caller's job.
No annualization or risk-free subtraction is performed. Beta is dimensionless.
For weighted means ā and b̄, the result is:
beta = covariance(a, b) / variance(b)
Both moments use N/(N-1) × Σ(wᵢ × centered product) / Σwᵢ, consistently
with weighted vector statistics. All rows,
including zero-weight rows, count in N, and the common correction cancels.
A zero-weight row still requires finite asset and benchmark observations.
Omitted weights mean unit weights; supplied weights must be finite,
nonnegative, equal-length and have a finite positive sum.
- Accepts 2-100,000 equal-length observation pairs, all finite.
- Constant benchmark returns, including constant positive-weight support, fail.
- Nonfinite moments/results and variance underflow to zero fail rather than returning NaN, infinity or inventing a denominator floor.
- Uses the shared centered vector-statistics arithmetic, avoiding raw-moment cancellation at large offsets. Extremely large moments can still overflow.
- Inputs remain unchanged. This stateless API creates no handle or context.
- The C API writes one caller-owned double only after successful validation.
Runnable examples: python example/python/benchmark_beta.py and, from sdk/go,
go run ./examples/benchmark_beta.
Independent fixtures are generated by scripts/fixtures/generate_benchmark_beta.py
using exact Python Fraction arithmetic, without calling this library.
For asset [1,3,2,10], benchmark [-1,0,2,9] and weights [1,2,3,0],
weighted covariance is 1/27, benchmark variance is 53/27 and beta is 1/53.
Unit weights give beta 53/61. Rust, Go and Python consume the same tracked
CSV, with C/C++ clients separately checking these rational expectations.
Explicit-frequency performance ratios¶
These unweighted functions accept signed simple returns in decimal units
(0.02 = 2%). No frequency, target or risk-free rate is inferred. Both ratios
require at least two observations. Targets are finite scalar per-period
returns, not annual rates or vectors; align them with the observations before
calling. Vector targets and weights are deliberately not supported.
| Function | Definition |
|---|---|
sharpe_ratio / SharpeRatio |
(mean(r) - risk_free_return) / sample_std(r) × sqrt(periods_per_year) |
sortino_ratio / SortinoRatio |
(mean(r) - MAR) / D × sqrt(periods_per_year) |
target_downside_deviation / TargetDownsideDeviation |
D = sqrt(sum(min(r - MAR, 0)^2) / N) |
Sharpe's centered squares divide by N-1. Sortino's shortfall squares divide
by all N observations, including returns meeting the target, without a
Bessel correction. This new utility is not the existing conditional
downside_deviation / StatisticsDownsideDeviation; that API stays unchanged.
The downside utility requires at least one observation and returns zero when
there is no shortfall. Ratios instead raise an error for zero denominators,
including all-positive Sortino samples at target zero. No epsilon or infinity
substitute is used. A flat negative sample can have a valid negative Sortino
but its Sharpe is undefined. Returns equal to target contribute zero downside.
For monthly [+2%, -1%, +3%, -2%] with target zero, the arithmetic mean is
0.5%, sample variance is 0.0017/3, and downside is
sqrt((0 + 0.01² + 0 + 0.02²)/4) = 0.011180339887498948.
The unannualized Sortino is 1/sqrt(5); choosing periods_per_year=12
produces sqrt(12/5) = 1.5491933384829668.
Square-root annualization is an explicit statistical scaling convention. It is not exact compounded annual return or annual downside risk. Serial correlation, changing distributions and aggregation across periods can make it misleading. Compare ratios only with matching targets and sampling conventions. Nonfinite inputs, invalid frequency, nonfinite subtraction/output and invalid counts are errors. Scaled squared deviations and anchored centering reduce avoidable overflow/underflow and cancellation, but inputs remain binary64, not arbitrary-precision arithmetic. C caller outputs remain unchanged on error; these stateless APIs retain no handles or session state.
"""Explicit monthly targets and square-root frequency scaling, not compounding."""
# standard library
import math
# itofin library
from itofin import statistics
returns = [0.02, -0.01, 0.03, -0.02]
downside = statistics.target_downside_deviation(returns, 0.0)
sharpe = statistics.sharpe_ratio(returns, 0.0, periods_per_year=12.0)
sortino = statistics.sortino_ratio(returns, 0.0, periods_per_year=12.0)
assert math.isclose(downside, math.sqrt(0.0005 / 4), abs_tol=1e-14)
assert math.isclose(sortino, math.sqrt(12 / 5), abs_tol=1e-13)
print(f"Monthly downside: {downside:.8f}; annualized Sharpe: {sharpe:.8f}; Sortino: {sortino:.8f}")
package main
import (
"fmt"
"math"
itofin "github.com/benbenbang/libitofin/sdk/go"
)
func main() {
returns := []float64{0.02, -0.01, 0.03, -0.02}
downside, err := itofin.TargetDownsideDeviation(returns, 0)
if err != nil {
panic(err)
}
sharpe, err := itofin.SharpeRatio(returns, 0, 12)
if err != nil {
panic(err)
}
sortino, err := itofin.SortinoRatio(returns, 0, 12)
if err != nil {
panic(err)
}
if math.Abs(downside-math.Sqrt(0.0005/4)) > 1e-14 || math.Abs(sortino-math.Sqrt(12.0/5)) > 1e-13 {
panic("performance ratio self-check failed")
}
fmt.Printf("Monthly downside: %.8f; annualized Sharpe: %.8f; Sortino: %.8f\n", downside, sharpe, sortino)
}
Run python example/python/performance_ratios.py or, from sdk/go,
go run ./examples/performance_ratios after installing the native library.
Independent 80-digit Python Decimal definitions and shared binding fixtures
live in scripts/fixtures/performance-ratios/; regenerate with its generate.py.