VolatilityStdDev — Annualized Volatility
Annualized historical volatility: the standard deviation of log returns, scaled by √252.
VolatilityStdDev is the identifier a strategy writes; Annualized Volatility is the label the chart’s indicator picker shows. Single value — .value is the only accessor.
Signature
VolatilityStdDev(period, on=<source>)| Argument | Type | Required | Notes |
|---|---|---|---|
period |
int Param or literal | Yes | Number of returns in the window. Default 20, bounds 2–200. |
on= |
symbol, OHLCV field, or indicator | No | Source. Defaults to the trade symbol’s close. |
Formula
Each bar contributes one log return:
$$ r_t = \ln!\left(\frac{\text{close}t}{\text{close}{t-1}}\right) $$
Over the last N returns, with $\bar{r}$ their mean:
$$ \text{VolatilityStdDev} = \sqrt{\frac{252}{N} \sum_{i=0}^{N-1} (r_i - \bar{r})^2} $$
Population standard deviation (divide-by-N), annualized by the 252-trading-day factor folded inside the root.
Warmup
period + 1 bars, not period.
The first bar has no previous close to compare against — the seeded source gates the return on prev_close > 0 — so it produces no return and only seeds the comparison. The window of N returns is therefore full one bar later than the period number suggests. Before that the indicator returns 0.
Reading the value
vol.value # a FRACTION: 0.24 means 24% annualizedvol.value * 100 # percent, for display and thresholdsvol.value_at(1)vol > 0.30 # a high-volatility regimeExample
class VolatilityRegimeFilter(Strategy): """Trend-follow only while annualized volatility stays under a ceiling.""" period = Param(20, min=2, max=200) ceiling = Param(40, min=5, max=200) trade = Symbol() sizing = FixedCapital()
def execute(self): vol = VolatilityStdDev(self.period, on=self.trade) sma = SMA(self.period, on=self.trade) self.stop_loss(2.0)
vol_pct = vol.value * 100
if vol_pct < self.ceiling and self.trade.close > sma.value and self.trade.is_flat: self.buy(self.trade, reason=f"Calm regime ({vol_pct:.1f}% annualized)")
if vol_pct > self.ceiling and self.trade.is_long: self.sell(self.trade, self.trade.position, reason="Volatility spike")Notes
- The value is a fraction, not a percentage. Everyone quotes volatility as “24%”, and this indicator reports
0.24. Comparingvol.value > 30will never be true for any real instrument. Multiply by 100 for display and for percentage thresholds, as the example does. - The 252 factor assumes daily bars. It is hard-coded in the seeded source. On an hourly or 5-minute chart the arithmetic still runs, but “annualized” is then wrong by the square root of the bars-per-day ratio. Treat sub-daily readings as a relative signal, not a quotable percentage.
- Log returns make it symmetric — a +10% move and a −10% move contribute equal magnitudes — and instrument-independent, which is what makes this the right volatility measure to compare across symbols.
StdDevon raw price is not comparable that way. - It measures dispersion, not direction. A calm uptrend and a calm downtrend read the same; pair it with a trend indicator, as the example does.
- Volatility clusters. A reading that has just spiked is more likely than not to stay elevated for several bars — which is what makes it usable as a regime filter rather than a trigger.