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
from horizon.features import RealizedVol
Signature
RealizedVol(
window: int = 60,
annualize: bool = True,
periods_per_year: float = 252.0,
market: str | None = None,
)
windowintannualizeboolperiods_per_yearfloatmarketstr | NoneMath
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
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 vol | Market 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
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
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
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
# 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