Appends one column; undefined for the first period rows. Note that is
period, not period − 1: HV is a σ of returns, and period returns
need period + 1 prices — the same off-by-one rsi and atr
have, for the same reason.
Conventions — the four choices that make two HVs disagree
There is no TA-Lib function to arbitrate these (it has none for HV), so
they are pinned here and in the pandas oracle instead:
Population σ (ddof = 0). The package convention — bollinger,
rollingStdev and zScore all use it, and TA-Lib's own
STDDEV does too. A sample σ (ddof = 1) is √(period/(period − 1))
larger: 2.6% at period 20, 5.4% at 10. If you need it, scale by that
factor rather than looking for an option.
Log returns, not simple returns.ln(p[i]/p[i−1]) is symmetric (a
move up and back down sums to zero) and additive across bars, which is
what makes the √time annualisation below legitimate. On ordinary
daily data the two differ in the third significant figure; on a large
move they diverge materially.
Annualised by √annualize, default 252. Volatility scales with
the square root of time, so a per-bar σ becomes annual by multiplying by
the root of the bars per year. 252 is the US trading-day count and the
right default for daily bars only — it is an option, not a hidden
constant, precisely because intraday and 7-day markets need a different
number. annualize: 1 gives the raw per-bar σ.
A decimal, not a percent.0.18 means 18% annualised. That is the
form volatility is consumed in (option pricing, position sizing, a
σ · √t band), and the one every other study here uses — only
percentChange multiplies by 100, because "percent" is its name.
Edges
A non-positive price has no log. The two returns that touch it
(its own and the next bar's) are missing, and the guard is explicit
rather than left to Math.log: ln(−4 / −5) is a perfectly finite
number that is not a return. Those missing returns then behave like any
gap under the rolling kernel's contract — the window still spans
period rows, σ is over the finite returns in it, and a window with no
finite return is undefined.
Read the consequence before relying on it. A window with exactly
ONE finite return has a σ of 0, so an interior bad price emits
hv = 0 on the bars around it — and 0 reads as "no volatility", the
opposite of what a corrupt price means. This is the package's rolling
contract (rollingStdev, bollinger and zScore do the same over a
gap), kept here for consistency with them; it is a deliberate
divergence from pandas, whose rolling(n).std() reports NaN
unless all n are present. Pinned by a test so the behaviour is
chosen rather than incidental. Fill or drop bad prices upstream if a
gap must not read as calm.
A leading gap shifts the start rather than shrinking the first
window: over another study's output (whose warm-up leaves missing rows
at the head) the first σ still covers period real returns. That is
the Wilder-kernel convention rsi and atr follow, and it
is also what pandas' rolling(n).std() does — so the oracle pins it.
Scale-invariant: log returns are ratios, so multiplying every price
by a constant leaves HV unchanged (pinned by a property test). Contrast
momentum and atr, which are in the price's units.
Why the kernel is called on a raw array
The σ is rollingMeanSdInto — the same range-exact, shifted-frame kernel
rollingStdev and bollinger sit on — but over the derived
returns array rather than a scratch column. A scratch column would go
through rollingValues, whose window is a count of rows: with the
first return undefined (bar 0 has no predecessor) it would emit at bar
period − 1 over period − 1 returns, one bar early with one return
short. Calling the kernel directly on the returns, starting from the first
real one, is what makes the warm-up period without an index hack.
Historical volatility — the standard deviation of log returns over
periodbars, annualised:Appends one column;
undefinedfor the firstperiodrows. Note that isperiod, notperiod − 1: HV is a σ of returns, andperiodreturns needperiod + 1prices — the same off-by-one rsi and atr have, for the same reason.Conventions — the four choices that make two HVs disagree
There is no TA-Lib function to arbitrate these (it has none for HV), so they are pinned here and in the pandas oracle instead:
ddof = 0). The package convention — bollinger, rollingStdev and zScore all use it, and TA-Lib's ownSTDDEVdoes too. A sample σ (ddof = 1) is√(period/(period − 1))larger: 2.6% atperiod 20, 5.4% at10. If you need it, scale by that factor rather than looking for an option.ln(p[i]/p[i−1])is symmetric (a move up and back down sums to zero) and additive across bars, which is what makes the√timeannualisation below legitimate. On ordinary daily data the two differ in the third significant figure; on a large move they diverge materially.√annualize, default252. Volatility scales with the square root of time, so a per-bar σ becomes annual by multiplying by the root of the bars per year.252is the US trading-day count and the right default for daily bars only — it is an option, not a hidden constant, precisely because intraday and 7-day markets need a different number.annualize: 1gives the raw per-bar σ.0.18means 18% annualised. That is the form volatility is consumed in (option pricing, position sizing, aσ · √tband), and the one every other study here uses — only percentChange multiplies by 100, because "percent" is its name.Edges
A non-positive price has no log. The two returns that touch it (its own and the next bar's) are missing, and the guard is explicit rather than left to
Math.log:ln(−4 / −5)is a perfectly finite number that is not a return. Those missing returns then behave like any gap under the rolling kernel's contract — the window still spansperiodrows, σ is over the finite returns in it, and a window with no finite return isundefined.Read the consequence before relying on it. A window with exactly ONE finite return has a σ of
0, so an interior bad price emitshv = 0on the bars around it — and0reads as "no volatility", the opposite of what a corrupt price means. This is the package's rolling contract (rollingStdev,bollingerandzScoredo the same over a gap), kept here for consistency with them; it is a deliberate divergence from pandas, whoserolling(n).std()reportsNaNunless allnare present. Pinned by a test so the behaviour is chosen rather than incidental. Fill or drop bad prices upstream if a gap must not read as calm.A leading gap shifts the start rather than shrinking the first window: over another study's output (whose warm-up leaves missing rows at the head) the first σ still covers
periodreal returns. That is the Wilder-kernel convention rsi and atr follow, and it is also what pandas'rolling(n).std()does — so the oracle pins it.Scale-invariant: log returns are ratios, so multiplying every price by a constant leaves HV unchanged (pinned by a property test). Contrast momentum and atr, which are in the price's units.
Why the kernel is called on a raw array
The σ is
rollingMeanSdInto— the same range-exact, shifted-frame kernel rollingStdev and bollinger sit on — but over the derived returns array rather than a scratch column. A scratch column would go throughrollingValues, whose window is a count of rows: with the first return undefined (bar 0 has no predecessor) it would emit at barperiod − 1overperiod − 1returns, one bar early with one return short. Calling the kernel directly on the returns, starting from the first real one, is what makes the warm-upperiodwithout an index hack.