Statistics & estimators
Hurst estimators, MLE for 1-D diffusions with 6 transition densities, ADF/KPSS/Phillips-Perron, realized variance with BNHLS, HMM, changepoint.
Statistics & estimators
The stochastic-rs-stats crate ships estimators for the most common
quant-finance workflows. Most take an ArrayView1<T> (price or
log-return series) and return a plain, flat *Result struct — but the
struct's field names are estimator-specific (see
Common pattern below), not a single uniform
point/se/p_value shape.
Sub-modules
| Module | What's inside |
|---|---|
mle | MLE for 1-D diffusions, 6 transition-density approximations (Euler, Ozaki, Shoji-Ozaki, Elerian, Kessler, Aït-Sahalia), L-BFGS-B via Basin. Plus the dedicated Heston MLE / NMLE-CEKF. |
realized | Realised variance / bipower / MinRV / MedRV / flat-top kernel (Bartlett, Parzen, Tukey-Hanning, Cubic, Quadratic-Spectral) with BNHLS bandwidth. Semivariance, realised skew / kurtosis, HAR-RV, EWMA / RiskMetrics variance, Jacod pre-averaging, TSRV, multi-scale RV, BNS and Lee–Mykland jump tests. |
normality | Jarque-Bera, Shapiro-Francia, Anderson-Darling |
stationarity | 9 HypothesisTest implementors: ADF, KPSS, Phillips-Perron, Leybourne-McCabe and ERS DF-GLS test unit-root/stationarity; Andrews-Ploberger and CUSUM test regression-parameter stability instead; Lo-MacKinlay tests the random-walk hypothesis; RESET tests linear functional form. See Choosing a stationarity test — the module's own name undersells how different these nine are. |
econometrics | Engle-Granger and Johansen (trace + maximum-eigenvalue) cointegration with VECM estimation, Granger causality, Gaussian-emission HMM with Baum-Welch, CUSUM and PELT changepoint |
filtering | Particle filter, UKF, random-walk Metropolis-Hastings |
hurst | Six native estimators behind one HurstEstimator trait: rescaled-range, DFA, GPH, wavelet, Whittle (Fukasawa, Takabatake & Westphal 2019 adapted quasi-likelihood; L-BFGS-B + Paxson + Eq.16 corrections, intraday Table 1 validation), variations. Two more implement the same trait by adapting a fractal-dimension estimate (fractal_dim::{Higuchi, Variogram}, H = 2 - D) — 8 HurstEstimator implementors total. hurst::whittle additionally exposes Whittle's own algorithm through dedicated one-shot functions (estimate, estimate_from_prices) outside the shared trait — see below |
gaussian_kde | Gaussian KDE |
tail_index | Hill estimator + variants |
spectral | Periodogram and spectrum search |
leverage | Leverage-effect estimators |
fou_estimator | fOU MLE with Hurst joint estimation |
garch | GARCH / GJR-GARCH / EGARCH Gaussian QMLE with inverse-Hessian and Bollerslev-Wooldridge robust standard errors |
evt | Extreme-value theory: Hill tail index, GPD peaks-over-threshold (MLE and Hosking-Wallis PWM) with VaR / ES, GEV block-maxima MLE with return levels |
distfit | Maximum-likelihood fits of the skewed return laws: Johnson SU, Hansen skew-t with location and scale, variance gamma |
Examples
Realized variance from intraday returns
// docs: stats#realized-variance-from-intraday-returns
//! Backs the realized-variance example on the statistics catalog page.
use ndarray::Array1;
use stochastic_rs::stats::realized::variance::realized_variance;
#[test]
fn realized_variance_from_log_returns() {
// Stand-in for "1-min log-returns over a trading day": a small fixed
// series keeps this example free of a live data feed.
let log_returns: Array1<f64> = Array1::from(vec![
0.001, -0.0007, 0.0012, -0.0003, 0.0009, -0.0011, 0.0004, -0.0002, 0.0006, -0.0008,
]);
let rv = realized_variance(log_returns.view());
assert!(rv > 0.0);
let annualised_vol = (rv * 252.0).sqrt();
assert!(annualised_vol.is_finite());
}import stochastic_rs as srs
import numpy as np
log_returns = ... # 1-min log-returns over a trading day
# annualisation is a constructor argument, not a post-hoc multiplier — it
# scales straight into `.volatility`. There is no standard-error field:
# RealizedMoments reports variance/volatility/skewness/kurtosis/quarticity,
# not a point estimate with a `se` — see the ADF section below for the
# same "no se/p_value field" pattern on the stationarity tests.
res = srs.RealizedMoments(log_returns, annualisation=252.0)
print("RV =", res.variance)
print("annVol =", res.volatility)EWMA / RiskMetrics variance
The exponentially weighted recursion , seeded at . riskmetrics_variance fixes the decay at the RiskMetrics daily ; ewma_variance takes any . The result carries the full conditional-variance series, the one-step-ahead forecast, lambda and nobs.
// docs: stats#ewma-riskmetrics-variance
//! Backs the EWMA / RiskMetrics example on the stats page.
use ndarray::Array1;
use stochastic_rs::stats::realized::ewma::RISKMETRICS_DAILY_LAMBDA;
use stochastic_rs::stats::realized::ewma::ewma_variance;
use stochastic_rs::stats::realized::ewma::riskmetrics_variance;
#[test]
fn ewma_variance_tracks_a_return_series() {
let returns = Array1::from(vec![0.012_f64, -0.007, 0.021, 0.0, -0.015, 0.004]);
// RiskMetrics daily decay (λ = 0.94) is the default convention…
let rm = riskmetrics_variance(returns.view());
assert_eq!(rm.lambda, RISKMETRICS_DAILY_LAMBDA);
assert_eq!(rm.variance.len(), returns.len());
// …and any λ ∈ (0, 1) is accepted explicitly.
let fast = ewma_variance(returns.view(), 0.80);
assert!(fast.forecast > 0.0);
// A faster decay weights the newest shock harder, so the two forecasts
// genuinely differ.
assert!((fast.forecast - rm.forecast).abs() > 1e-9);
}import stochastic_rs as srs
# lambda_ defaults to the RiskMetrics daily 0.94
res = srs.EwmaVariance(returns, lambda_=0.94)
print("sigma2 series:", res.variance())
print("1-step forecast:", res.forecast)Lee-Mykland jump test
Every return is standardised by a bipower estimate of the local volatility from the observations before it, , and flagged as a jump when leaves the Gumbel region of the sample maximum at level (Lee & Mykland 2008, Lemma 1). Unlike the BNS test, which only says whether an interval contains jumps, this one returns the jump times and sizes. lee_mykland_window implements the paper's window rule — the smallest integer above , 16 for daily data.
// docs: stats#lee-mykland-jump-test
//! Backs the Lee–Mykland example on the stats page.
use ndarray::Array1;
use stochastic_rs::distributions::normal::SimdNormal;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stats::realized::lee_mykland::lee_mykland_test;
use stochastic_rs::stats::realized::lee_mykland::lee_mykland_window;
#[test]
fn lee_mykland_locates_a_planted_jump() {
// Stand-in for a day of 5-minute log-returns with one news jump in it.
let dist = SimdNormal::<f64>::new(0.0, 0.001, &Deterministic::new(42));
let mut returns = Array1::<f64>::zeros(2_000);
dist.fill_slice(returns.as_slice_mut().unwrap());
returns[1_200] = 0.02;
// The paper's window rule for 78 five-minute returns a day is K = 141.
let window = lee_mykland_window(78);
assert_eq!(window, 141);
let test = lee_mykland_test(returns.view(), window, 0.01);
assert!(test.jump_indices.contains(&1_200));
// Every flagged return sits beyond the Gumbel threshold on |L(i)|.
for &i in &test.jump_indices {
assert!(test.statistics[i].abs() > test.threshold);
}
}import stochastic_rs as srs
window = srs.LeeMyklandJumpTest.recommended_window(78) # 5-minute data → 141
test = srs.LeeMyklandJumpTest(returns, window, alpha=0.01)
print("jumps at:", test.jump_indices)
print("threshold on |L(i)|:", test.threshold)Johansen cointegration and VECM
johansen_test returns both rank statistics of the concentrated likelihood — and — with the 5% MacKinnon–Haug–Michelis critical values for an unrestricted constant, and the rank each sequential procedure selects. vecm_fit then gives the maximum-likelihood VECM at a chosen rank: (Johansen's normalisation ), , , the short-run , the constant, residuals and their covariance.
// docs: stats#johansen-cointegration-and-vecm
//! Backs the Johansen / VECM example on the stats page.
use ndarray::Array2;
use stochastic_rs::distributions::normal::SimdNormal;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stats::econometrics::johansen_test;
use stochastic_rs::stats::econometrics::vecm_fit;
#[test]
fn johansen_rank_then_vecm() {
// Two prices sharing one random walk: y2 ≈ 0.7·y1, so the pair has
// exactly one cointegrating relation.
let steps = SimdNormal::<f64>::new(0.0, 1.0, &Deterministic::new(7));
let noise = SimdNormal::<f64>::new(0.0, 0.1, &Deterministic::new(11));
let mut dw = vec![0.0_f64; 500];
let mut eps = vec![0.0_f64; 500];
steps.fill_slice(&mut dw);
noise.fill_slice(&mut eps);
let mut y = Array2::<f64>::zeros((500, 2));
let mut w = 0.0;
for t in 0..500 {
w += dw[t];
y[[t, 0]] = w;
y[[t, 1]] = 0.7 * w + eps[t];
}
// Rank tests at VAR order 2: both sequential procedures stop at r = 1.
let test = johansen_test(y.view(), 2);
assert_eq!(test.rank_trace, 1);
assert_eq!(test.rank_max_eig, 1);
assert!(test.max_eig_statistics[0] > test.max_eig_critical_5pct[0]);
// The VECM at that rank recovers the relation y2 − 0.7·y1 up to scale.
let fit = vecm_fit(y.view(), 2, test.rank_trace);
let ratio = fit.beta[[1, 0]] / fit.beta[[0, 0]];
assert!((ratio + 1.0 / 0.7).abs() < 0.1, "beta ratio {ratio}");
assert_eq!(fit.gamma.len(), 1);
assert_eq!(fit.residuals.dim(), (498, 2));
}import stochastic_rs as srs
test = srs.Johansen(prices, lags=2) # prices: (t, k) array
print("rank (trace):", test.rank_trace, "rank (max-eig):", test.rank_max_eig)
print("max-eig stats:", test.max_eig_statistics())
fit = srs.Vecm(prices, lags=2, rank=test.rank_trace)
print("pi:", fit.pi())
print("gamma_1:", fit.gamma()[0])GARCH-family QMLE
garch_fit maximises the Gaussian quasi-likelihood of a GARCH(,), GJR-GARCH(,) or EGARCH(,) recursion with a constant or zero mean, in unconstrained coordinates that keep the fit inside the stationarity region. The result carries the parameters, both the inverse-Hessian and the Bollerslev–Wooldridge robust standard errors (the latter stay valid under non-Gaussian innovations), the conditional-variance path, standardised residuals, log-likelihood, AIC/BIC and persistence. Pre-sample terms follow the backcast convention of the arch reference implementation, whose fits the test-suite reproduces.
// docs: stats#garch-family-qmle
//! Backs the GARCH QMLE example on the stats page.
use ndarray::Array1;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stats::garch::GarchSpec;
use stochastic_rs::stats::garch::MeanSpec;
use stochastic_rs::stats::garch::garch_fit;
use stochastic_rs::stochastic::autoregressive::garch::Garch;
use stochastic_rs::traits::ProcessExt;
#[test]
fn garch_qmle_recovers_the_simulated_parameters() {
// Two thousand daily returns from GARCH(1,1) with ω = 0.05, α = 0.10,
// β = 0.85 (percent units, unconditional variance 1.0).
let process = Garch::<f64, _>::new(
0.05,
Array1::from(vec![0.10]),
Array1::from(vec![0.85]),
2_000,
Deterministic::new(42),
);
let returns = process.sample();
// The simulator has no drift, so a zero-mean fit is the matching model;
// `GarchSpec::garch(1, 1)` alone would estimate a constant mean as well.
let fit = garch_fit(
returns.view(),
GarchSpec::garch(1, 1).with_mean(MeanSpec::Zero),
);
assert!(fit.converged);
assert!((fit.alpha[0] - 0.10).abs() < 3.0 * fit.robust_std_errors[1]);
assert!((fit.beta[0] - 0.85).abs() < 3.0 * fit.robust_std_errors[2]);
assert!(fit.persistence < 1.0);
assert_eq!(fit.conditional_variance.len(), returns.len());
}import stochastic_rs as srs
fit = srs.GarchFit(returns, kind="gjr", p=1, q=1, mean="constant")
for name, value, se in zip(fit.param_names(), fit.params(), fit.robust_std_errors()):
print(f"{name:9s} {value: .5f} ({se:.5f})")
print("persistence:", fit.persistence, "log-likelihood:", fit.log_likelihood)Extreme-value theory: Hill, peaks-over-threshold and block maxima
hill_estimator is Hill's (1975) tail index from the largest positive observations with its standard error. pot_fit fits a generalised Pareto distribution to the excesses over a threshold by maximum likelihood (gpd_fit on its own for pre-computed excesses) and turns it into tail quantiles and expected shortfall with the McNeil–Frey–Embrechts formulas; mean_excess is the mean-residual-life plot for choosing the threshold. block_maxima and gev_fit are the block-maxima route, with return_level(m) for the level exceeded once every blocks. The MLE fitters report inverse-information standard errors, and the fits reproduce scipy on identical data.
// docs: stats#extreme-value-theory-hill-peaks-over-threshold-and-block-maxima
//! Backs the extreme-value example on the stats page.
use ndarray::Array1;
use stochastic_rs::distributions::pareto::SimdPareto;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stats::evt::block_maxima;
use stochastic_rs::stats::evt::gev_fit;
use stochastic_rs::stats::evt::hill_estimator;
use stochastic_rs::stats::evt::pot_fit;
#[test]
fn evt_tail_estimates_agree_on_a_pareto_tail() {
// Losses with an exact Pareto(α = 3) tail, so the true tail index is
// ξ = 1/3.
let dist = SimdPareto::<f64>::new(1.0, 3.0, &Deterministic::new(7));
let mut losses = vec![0.0; 20_000];
dist.fill_slice(&mut losses);
let losses = Array1::from(losses);
let hill = hill_estimator(losses.view(), 500);
assert!((hill.xi - 1.0 / 3.0).abs() < 3.0 * hill.std_error);
// Peaks over a threshold of 3: the GPD shape estimates the same ξ, and
// the tail quantiles follow.
let pot = pot_fit(losses.view(), 3.0);
assert!(pot.gpd.converged);
assert!((pot.gpd.xi - 1.0 / 3.0).abs() < 3.0 * pot.gpd.std_errors[1]);
let var99 = pot.quantile(0.99);
assert!(pot.expected_shortfall(0.99) > var99 && var99 > pot.threshold);
// Block maxima of 100 draws: GEV with ξ ≈ 1/3 again.
let maxima = block_maxima(losses.view(), 100);
let gev = gev_fit(maxima.view());
assert!(gev.converged);
assert!((gev.xi - 1.0 / 3.0).abs() < 3.0 * gev.std_errors[2]);
assert!(gev.return_level(50.0) > gev.mu);
}import numpy as np
import stochastic_rs as srs
losses = -returns # right tail = losses
hill = srs.HillEstimator(losses, k=100)
print("xi:", hill.xi, "+/-", hill.std_error)
pot = srs.PotFit(losses, threshold=np.quantile(losses, 0.95))
print("VaR99:", pot.quantile(0.99), "ES99:", pot.expected_shortfall(0.99))
gev = srs.GevFit(srs.block_maxima(losses, 21)) # monthly maxima
print("100-block return level:", gev.return_level(100.0))Fitting the skewed return distributions
distfit fits the skewed laws of the distributions crate by maximum likelihood with their closed-form densities: johnson_su_fit (γ, δ, ξ, λ), skew_t_fit (Hansen's standardised skew-t with location μ and scale σ, so η > 2 and |λ| < 1 are kept by construction) and variance_gamma_fit (σ, ν, θ, μ through the Bessel-form density). Each returns the estimates, inverse-information standard errors, log-likelihood, AIC and BIC; the test-suite pins them to tightly converged scipy / arch optima on identical data. evt::gpd_pwm is the matching closed-form Hosking-Wallis estimator for threshold excesses.
// docs: stats#fitting-the-skewed-return-distributions
//! Backs the distribution-fitting example on the stats page.
use ndarray::Array1;
use stochastic_rs::distributions::skew_t::SimdSkewT;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stats::distfit::johnson_su_fit;
use stochastic_rs::stats::distfit::skew_t_fit;
#[test]
fn skew_t_fit_recovers_the_simulated_shape() {
// Returns from Hansen's skew-t with η = 6, λ = −0.3, scaled to 1.5%
// daily volatility around a 0.05% drift.
let dist = SimdSkewT::<f64>::new(6.0, -0.3, &Deterministic::new(11));
let mut z = vec![0.0; 4_000];
dist.fill_slice(&mut z);
let returns = Array1::from_iter(z.iter().map(|v| 0.0005 + 0.015 * v));
let fit = skew_t_fit(returns.view());
assert!(fit.converged);
assert!((fit.lambda + 0.3).abs() < 3.0 * fit.std_errors[3]);
assert!((fit.sigma - 0.015).abs() < 3.0 * fit.std_errors[1]);
// A Johnson SU fit of the same sample ranks below it on AIC.
let jsu = johnson_su_fit(returns.view());
assert!(jsu.converged && jsu.aic > fit.aic);
}import stochastic_rs as srs
fit = srs.SkewTFit(returns)
print("eta:", fit.eta, "lambda:", fit.lambda_, "sigma:", fit.sigma)
print("SEs:", fit.std_errors(), "AIC:", fit.aic)
vg = srs.VarianceGammaFit(returns)
print("sigma, nu, theta, mu:", vg.sigma, vg.nu, vg.theta, vg.mu)ADF stationarity test
Null hypothesis: unit root (non-stationary). adf_test lives under the
stationarity module and reports the rejection decision directly as
reject_unit_root: bool at the alpha configured on AdfConfig — there
is no separate p_value field, only the test statistic and tabulated
critical values.
// docs: stats#adf-stationarity-test
//! Backs the ADF example on the statistics catalog page. `adf_test` lives
//! under the `stationarity` module (OLS regression via
//! `ndarray-linalg`), so the whole file is gated on that feature.
use ndarray::ArrayView1;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stats::stationarity::adf::AdfConfig;
use stochastic_rs::stats::stationarity::adf::adf_test;
use stochastic_rs::stochastic::diffusion::ou::Ou;
use stochastic_rs::traits::ProcessExt;
#[test]
fn adf_rejects_unit_root_on_a_mean_reverting_series() {
// An OU path is mean-reverting by construction, so the null (unit
// root) should be rejected — a stand-in for "prices or log-returns"
// that avoids a live data feed.
let series = Ou::<f64, _>::new(
2.0,
0.0,
0.5,
500,
Some(0.0),
Some(10.0),
Deterministic::new(7),
)
.sample();
let result = adf_test(
ArrayView1::from(series.as_slice().unwrap()),
AdfConfig::default(),
);
assert!(
result.reject_unit_root,
"expected the unit-root null to be rejected"
);
}import stochastic_rs as srs
# ADFTest is a class, not a free function; the fitted
# object exposes .statistic/.reject_unit_root/.critical_values directly —
# no .p_value, matching the "no separate p_value field" note above.
res = srs.ADFTest(series, max_lags=5)
print(f"t-stat={res.statistic:.3f}, reject_unit_root={res.reject_unit_root}")Hurst exponent estimation
The hurst module unifies six estimators (rescaled-range, DFA, GPH,
wavelet, Whittle, variations) behind one HurstEstimator trait —
estimate(x) -> Result<HurstResult, HurstError> — plus two more
(Higuchi, Variogram) that reach the same trait by adapting a
fractal-dimension estimate. The example below uses RescaledRange
(Hurst 1951 + Anis-Lloyd 1976 bias correction), which estimates the
Hurst exponent of a signal directly:
// docs: stats#hurst-exponent-estimation
//! Backs the Hurst-estimator example on the statistics catalog page.
//!
//! Uses the rescaled-range estimator (`RescaledRange`) rather than the
//! Fukasawa/Whittle one (`stats::hurst::whittle`): Fukasawa estimates the
//! roughness of a *latent volatility* process from a realized-variance
//! series, not the Hurst exponent of a path sampled directly from `Fgn` —
//! feeding path samples straight into it silently recovers nonsense. See
//! `stochastic-rs-stats/src/hurst/whittle.rs`'s own `simulate_log_rv` test
//! helper for the realized-variance construction Fukasawa actually expects.
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stats::hurst::HurstEstimator;
use stochastic_rs::stats::hurst::rs::RescaledRange;
use stochastic_rs::stochastic::noise::fgn::Fgn;
use stochastic_rs::traits::ProcessExt;
#[test]
fn rescaled_range_recovers_true_h_from_fgn_increments() {
let true_h = 0.3;
let path = Fgn::<f64, _>::new(true_h, 4096, Some(1.0), Deterministic::new(42)).sample();
// `path` is already the stationary fGn increment series, so disable the
// estimator's default first-differencing (meant for an fBM-like walk).
let estimator = RescaledRange {
take_differences: false,
..RescaledRange::default()
};
let res = estimator.estimate(path.view()).unwrap();
assert!(
(res.hurst - true_h).abs() < 0.1,
"H = {:.3} (true = {:.3})",
res.hurst,
true_h
);
}import stochastic_rs as srs
x = ... # a price / level series, e.g. np.cumsum(fgn.sample())
res = srs.RescaledRange().estimate(x)
print(f"H = {res.hurst:.3f}")All eight HurstEstimator implementors are Python-bound (RescaledRange,
Dfa, Gph, Wavelet, Whittle, Variations, Higuchi, Variogram —
the last two also via .estimate_hurst(x) alongside their native
.estimate(x) -> FdResult fractal-dimension output).
Whittle is a different kind of input from the other five native
estimators: it and the free-standing FukasawaHurst class (Python) /
estimate, estimate_from_prices functions (Rust, stats::hurst::whittle)
are the same Fukasawa, Takabatake & Westphal (2019) adapted-Whittle
quasi-likelihood estimator, just reached two ways — the generic trait or
a dedicated one-shot constructor. Either way it estimates the roughness of
a latent volatility process from a realized-variance time series, not
the Hurst exponent of a path sampled directly from Fgn. Feeding raw path
samples into it silently recovers nonsense; see
stochastic-rs-stats/src/hurst/whittle.rs's own simulate_log_rv test
helper for the construction it actually expects (a simulated log-variance
process aggregated into intraday realized-variance bins).
Choosing a stationarity test
The stationarity module holds nine HypothesisTest implementors, but
"stationarity" accurately describes only five of them — the other four
test a different null hypothesis entirely, despite living in the same
module. The module's own doc comment
(stochastic-rs-stats/src/stationarity.rs) has the full comparison with
literature citations and exact minimum-sample requirements; short version:
| Question you're actually asking | Reach for |
|---|---|
| Does this series have a unit root? (H₀: unit root) | adf::adf_test, ers_dfgls::ers_dfgls_test (usually the better default — the module doc explains why), phillips_perron::phillips_perron_test |
| Is this series stationary? (H₀: stationarity) | kpss::kpss_test, leybourne_mccabe::leybourne_mccabe_test |
| Are this regression's coefficients stable over time? | andrews_ploberger::andrews_ploberger_test (one unknown breakpoint) or cusum::cusum_test (gradual drift in the mean or variance) |
| Is this specifically a random walk (uncorrelated increments)? | lo_mackinlay::lo_mackinlay_test |
| Is my linear model's functional form correct? | reset::reset_test — not a time-series question at all |
Run ADF and KPSS together, not either alone: their null hypotheses are opposite, so agreement between the two is real evidence, and disagreement is informative too — often a sign of long-range dependence rather than a clean unit-root/stationary split. See Choosing a Hurst estimator below for estimating that directly instead of forcing a binary call.
Choosing a Hurst estimator
Seven of the eight HurstEstimator implementors estimate the same
quantity — the self-similarity exponent of the series handed to
.estimate() — by different routes; the eighth, Whittle, estimates
something else entirely (the warning in the example above). The full
comparison, with the literature backing each method and exact data-size
requirements, lives in the hurst module's own doc comment
(stochastic-rs-stats/src/hurst/mod.rs). Short version:
| Need | Reach for |
|---|---|
| A reasonable general-purpose default | dfa::Dfa — its polynomial detrending step is specifically why it tolerates real (trending) data better than rs::RescaledRange |
| A confidence interval, not just a point estimate | gph::Gph or wavelet::Wavelet — the only two that populate a closed-form standard error |
| The series is believed to be a rough, mean-reverting (fractional-OU-like) process rather than pure fBM/fGN | variations::Variations with VariationKind::PowerVariation |
You already think in terms of fractal dimension D | fractal_dim::Higuchi / fractal_dim::Variogram via the H = 2 - D adapter — prefer FractalDimEstimator directly when a degenerate fit matters, since the adapter clamps silently |
| Roughness of latent volatility from a realized-variance series (rough-vol calibration) | hurst::whittle::Whittle — and only that input shape; feeding it a raw sampled path is the one mistake this table exists to prevent |
rs::RescaledRange is the historical default (Hurst 1951) but is
documented — in the same paper this crate cites for its own bias
correction — as the least reliable of the group in finite samples; kept
for continuity, not as a recommendation.
Common pattern
Every estimator here returns a plain, flat struct — no boxed traits, no
enums to match on — but the field names are estimator-specific, not
a uniform point/se/p_value triple. For example:
// RescaledRange, Dfa, Gph, Wavelet, Whittle, Variations all return this:
pub struct HurstResult<T: FloatExt = f64> {
pub hurst: T,
pub std_err: Option<T>, // only Gph and Wavelet populate this
pub n_obs: usize,
pub diagnostic: HurstDiagnostic<T>,
}
// while the stationarity tests (ADF, KPSS, ...) return their own shape:
pub struct AdfResult {
pub statistic: f64,
pub used_lags: usize,
pub nobs: usize,
pub critical_values: CriticalValues,
pub reject_unit_root: bool,
}Check the specific estimator's result struct for its field names — the
per-module source under stochastic-rs-stats/src/ documents each one.
Adding an estimator
See the
stats-estimator
SKILL — covers the ArrayView1<T> input shape, *Result struct
conventions, feature gating, and the paper-citation requirement.
Copulas
All 15 bivariate and 8 multivariate copulas with the BivariateExt / MultivariateExt traits — Archimedean, extreme-value, elliptical, and vine families.
Quantitative finance
Pricing, calibration, vol surface, risk, credit, curves, bonds, instruments, portfolio, microstructure — the stochastic-rs-quant crate.