RealizedVol

Annualized realized volatility, the std dev of log returns

RealizedVol computes the annualized standard deviation of log returns over a rolling window. It’s the standard volatility measure used in Sharpe ratios, Kelly sizing, and vol-targeted portfolios.

Import

python
from horizon.features import RealizedVol

Signature

python
RealizedVol(
    window: int = 60,
    annualize: bool = True,
    periods_per_year: float = 252.0,
    market: str | None = None,
)
windowint
Number of log returns to use. Must be ≥ 2.
annualizebool
If True, multiply by `√periods_per_year` to annualize. If False, return the raw per-period stddev.
periods_per_yearfloat
Annualization factor. Use `252` for equity trading days, `365` for crypto (24/7), `12` for monthly bars, etc.
marketstr | None
Optional pin to a specific market id.

Math

window_rets = last `window` log returns
mean        = Σ window_rets / window
var         = Σ (r - mean)² / (window - 1)
vol         = √var
annualized  = vol × √periods_per_year    (if annualize=True)

The √periods_per_year annualization assumes log returns are i.i.d., a standard but not always accurate assumption. For autocorrelated returns (many crypto markets, intraday equities), the true annual vol is different, but this estimator is the industry standard.

Behavior

python
def compute(self, market_id, history, feeds):
    rets = history.last_n_returns(self.window + 1)
    if len(rets) < self.window + 1:
        return float("nan")
    window_rets = rets[-self.window:]
    mean = sum(window_rets) / len(window_rets)
    var = sum((r - mean) ** 2 for r in window_rets) / max(len(window_rets) - 1, 1)
    vol = math.sqrt(var)
    if self.annualize:
        vol *= math.sqrt(self.periods_per_year)
    return vol

Interpretation

Annualized volMarket type
10%Low-vol blue chip, treasuries, cash-like
15-20%S&P 500, broad equity indices
25-35%Individual equities, small caps
40-60%Crypto, high-beta stocks
80%+Illiquid alt-coins, extreme tails

Use cases

Vol-targeted position sizing

python
from horizon import Strategy, Signal
from horizon.features import RealizedVol, Zscore

class VolTargeted(Strategy):
    asset_classes = [Equity]
    features = {
        "z": Zscore(window=20),
        "vol": RealizedVol(window=60),
    }

    def evaluate(self, f, universe):
        TARGET_VOL = 0.15    # 15% annualized

        return [
            Signal(
                market_id=m.id,
                direction=Direction.Increase if f.z[m.id] < -2 else Direction.Decrease,
                confidence=0.7,
                expected_edge_bps=50,
                expected_expected_vol_bps=f.vol[m.id] * 10_000,
                # Size inversely to vol. lower-vol instruments get larger notional
                preferred_notional_usd=100_000 * (TARGET_VOL / f.vol[m.id]),
                horizon=timedelta(days=3),
            )
            for m in universe
            if abs(f.z[m.id]) > 2 and f.vol[m.id] > 0
        ]

Regime filter

python
features = {"vol": RealizedVol(window=60)}

def evaluate(self, f, universe):
    # Only trade mean reversion in low-vol regimes
    return [
        Signal.from_score(m, score=-f.z[m.id], edge_per_stdev=15)
        for m in universe
        if f.vol[m.id] < 0.20    # < 20% annualized
        and abs(f.z[m.id]) > 2
    ]

Sharpe computation

python
features = {
    "r": Return(window=20),
    "vol": RealizedVol(window=60, annualize=False),
}

def evaluate(self, f, universe):
    for m in universe:
        daily_vol = f.vol[m.id]              # not annualized
        sharpe_20d = f.r[m.id] / max(daily_vol, 0.001) * math.sqrt(20)
        ...

Pitfalls

Realized vol vs implied vol

RealizedVol is historical: it tells you how volatile the market has been. Implied vol (from option prices) tells you how volatile the market is expected to be. They’re related but different:

  • Vol of vol traders bet on the spread between realized and implied
  • Short volatility strategies bet that implied > realized (usually true, by a few points)
  • Long volatility strategies bet that implied is lower than realized (usually wrong, but pays huge when right)

Horizon doesn’t ship an ImpliedVol feature out of the box: it requires option chain data and a Black-Scholes solver. See de Prado Features for approaches to volatility modeling.

Tests

python
# tests/test_features.py::TestRealizedVol

def test_zero_on_flat(self, store_with_history: FeatureStore) -> None:
    rv = RealizedVol(window=60)
    ns = store_with_history.compute({"v": rv}, ["TEST"], {})
    assert ns.v["TEST"] == 0.0

def test_matches_expected_annualized(self) -> None:
    # Daily returns ~ N(0, 0.01) → annualized ≈ √252 × 0.01 ≈ 0.1587
    store = FeatureStore()
    for _ in range(100):
        last = store.history("T").last_price() or 100.0
        next_price = last * math.exp(rng.gauss(0, 0.01))
        store.update_feeds({"T": FeedData(market_id="T", price=next_price)})
    rv = RealizedVol(window=60)
    ns = store.compute({"v": rv}, ["T"], {})
    assert 0.10 < ns.v["T"] < 0.22

Next