Use case · Volatility

Estimate realized volatility from OHLC bars

realized_vol takes a slice of typed OhlcBar, a named RvMethod, and a periods-per-year factor and returns a RealizedVolRead — close-to-close, Parkinson, Garman-Klass, Yang-Zhang, uncentered, or jump-robust bipower variation on one shared path. realized_semivariance splits the same uncentered variance into its downside and upside legs.

When to use it

  • You need annualized realized volatility from OHLC bars with a named, range-aware estimator rather than a hand-rolled close-to-close formula.
  • You want to separate diffusive volatility from jump contribution — use realized_jump_split for the bipower-based decomposition.
  • You are netting a skewed implied wing against realized — use realized_semivariance so the put wing meets downside semivol and the call wing meets upside, instead of both meeting one symmetric number.
  • You only have a bare close series — use realized_vol_from_closes, which is the close-to-close estimator over a &[f64] of closes.

Example

use ferro_risk::{realized_vol, realized_jump_split, OhlcBar, RvMethod};

// One trading year of daily bars (periods_per_year = 252.0).
let bars = vec![
    OhlcBar::new(100.0, 101.5, 99.2, 100.8)?,
    OhlcBar::new(100.8, 102.0, 100.1, 101.3)?,
    // ...
];

let rv = realized_vol(&bars, RvMethod::YangZhang, 252.0)?;
println!("annualized vol = {}", rv.vol());
println!("variance = {}  bars used = {}", rv.variance(), rv.periods());

// Jump-robust split: diffusive vs jump variance.
let split = realized_jump_split(&bars, 252.0)?;
println!("diffusive = {}  jump = {}", split.diffusive_variance(), split.jump_variance());
if let Some(share) = split.jump_share() {
    println!("jump share of realized variance = {}", share);
}
# Ok::<(), ferro_risk::FerroRiskError>(())

Estimators

RvMethodDescription
CloseToCloseDemeaned sample standard deviation of log close-to-close returns.
ParkinsonParkinson (1980) high-low range estimator.
GarmanKlassGarman-Klass (1980), practical form using open, high, low, and close.
YangZhangYang-Zhang (2000) drift-independent composite estimator.
BipowerVariationBarndorff-Nielsen & Shephard bipower variation — the jump-robust diffusive leg.
UncenteredUncentered quadratic variation — Σr²/n, no mean subtracted. The symmetric total that the directional semivariance legs recombine to.

Directional semivariance

realized_semivariance splits the uncentered realized variance by the sign of each return (Barndorff-Nielsen–Kinnebrock–Shephard 2010), so a skewed implied wing can be netted against the matching realized leg without double-counting the skew — put-wing IV against downside semivol, call-wing IV against upside:

RS⁻ = Σ rᵢ²·1{rᵢ<0} / n      RS⁺ = Σ rᵢ²·1{rᵢ>0} / n      RS⁻ + RS⁺ = RV
use ferro_risk::{realized_semivariance, TRADING_DAYS_PER_YEAR};

let read = realized_semivariance(&bars, TRADING_DAYS_PER_YEAR)?;
println!("downside = {}  upside = {}", read.downside_vol(), read.upside_vol());

// The uncentered total is the literal f64 sum of the two legs.
assert_eq!(
    read.downside_variance() + read.upside_variance(),
    read.realized_variance(),
);
# Ok::<(), ferro_risk::FerroRiskError>(())
RealizedSemivarianceDescription
downside_vol() / upside_vol()Annualized directional semivolatility for negative / positive returns.
downside_variance() / upside_variance()The annualized variance legs; their exact f64 sum is realized_variance().
realized_vol() / realized_variance()The uncentered symmetric total.
return_count()Number of return periods (bars.len() − 1).
periods_per_year()The annualization factor supplied by the caller.
Conventions

The split is uncentered with divisor n — estimating no mean forfeits no degree of freedom — which is why it needs its own RvMethod::Uncentered total rather than the demeaned CloseToClose, which is deliberately left unchanged. It is also raw and jump-inclusive: RS⁻ charges for crash risk, which is the point for a put wing. A one-sided window is data, not an error — a monotone up-drift returns downside_vol() == 0.0.

realized_semivariance_from_closes is the same estimator over a bare &[f64] of closes.

The read

RealizedVolRead carries the volatility and its supporting context; realized_jump_split returns an RvJumpSplit with the diffusive / jump breakdown:

RealizedVolReadDescription
vol()Annualized realized volatility.
variance()Annualized realized variance.
method()The RvMethod used.
periods()Number of return periods contributing to the estimate.
periods_per_year()The annualization factor supplied by the caller.
RvJumpSplitDescription
total_variance()Total realized variance (diffusive + jump).
diffusive_variance()Jump-robust diffusive variance from bipower variation.
jump_variance()Residual jump variance, floored at zero.
jump_share()Option<f64> — jump variance over total, None when total variance is zero.

Notes

  • periods_per_yearis the caller's annualization factor — 252 for daily trading bars, 12 for monthly, and so on.
  • Sample-count conventions differ by estimator (close-to-close and Yang-Zhang use N − 1; Parkinson and Garman-Klass use N); the count actually used is on periods().
  • OhlcBar::newrejects a bar whose high / low don't bracket its open / close, so a malformed bar fails at construction.
  • Degenerate inputs — too few bars, a non-positive periods_per_year — fail loud with a typed FerroRiskError in the realized_vol.* namespace, never a silent NaN.
Related

Feed the read straight into the variance risk premium variance_risk_premium takes the realized vol and the same RvMethod so both sides of the premium agree.