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_splitfor the bipower-based decomposition. - You are netting a skewed implied wing against realized — use
realized_semivarianceso 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
| RvMethod | Description |
|---|---|
| CloseToClose | Demeaned sample standard deviation of log close-to-close returns. |
| Parkinson | Parkinson (1980) high-low range estimator. |
| GarmanKlass | Garman-Klass (1980), practical form using open, high, low, and close. |
| YangZhang | Yang-Zhang (2000) drift-independent composite estimator. |
| BipowerVariation | Barndorff-Nielsen & Shephard bipower variation — the jump-robust diffusive leg. |
| Uncentered | Uncentered 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⁺ = RVuse 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>(())| RealizedSemivariance | Description |
|---|---|
| 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. |
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:
| RealizedVolRead | Description |
|---|---|
| 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. |
| RvJumpSplit | Description |
|---|---|
| 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 useN); the count actually used is onperiods(). 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 typedFerroRiskErrorin therealized_vol.*namespace, never a silentNaN.
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.