Tutorial
Historical volatility: formula, annualizing and how to calculate it in Python
The formula fits on one line; the choices around it do not. On the same BTCUSD bars, annualizing with 252 days instead of 365 cuts volatility from 44.00% to 36.56%, and one week moved its 30-day reading from 37% to 81%.
On this page
- What is historical volatility?
- The historical volatility formula
- How to calculate volatility in four steps
- Annualized volatility: 252, 260 or 365 periods a year
- Calculate historical volatility in Python
- Stock, forex, gold and crypto volatility, from 5% to 44%
- Rolling volatility: choosing 21, 63 or 252 sessions
- Parkinson, Garman-Klass, Rogers-Satchell and Yang-Zhang volatility
- From annual volatility to an expected daily move
- Historical (realized) vs implied volatility
- Six data pitfalls that distort volatility
- Questions
Key takeaways
- Historical volatility is the sample standard deviation of log returns, annualized: 12.74% for US500 over the 252 sessions to 23 October 2026 and 44.00% for BTCUSD over 365 days.
- Annualize with the asset’s own calendar: √252 for stocks and indices, √260 for forex and spot gold, √365 for crypto, which trades every day.
- A window is a span of time, not a row count: one month is 21 US sessions, 22 forex weekdays or 30 crypto days.
- Range estimators that ignore overnight gaps read low on stocks: Parkinson put US500 at 7.93% over the last 63 sessions against 11.15% close to close, while on round-the-clock BTCUSD the two were 1.34 points apart.
- Daily returns have fat tails: 22 of the 1,901 daily returns of seven assets in the year to 23 October 2026 fell beyond three standard deviations, where a normal curve expects 5.1.
Historical volatility is the annualized standard deviation of an asset’s past returns over a stated window: how far the price typically moved in a period, not which way. Over the 252 sessions to 23 October 2026, US500 had a historical volatility of 12.74%; BTCUSD, which trades every day, had 44.00% over 365 days.
What is historical volatility?
The volatility of a stock is quoted the same way. US:KO’s was 19.06% over the same year, so a typical session moved it about 1.20% up or down (19.06% ÷ √252). Gains and losses count alike: volatility measures the size of moves, not their direction or the risk of a permanent loss.
Realized volatility is the same measurement, the name options traders use when they set it against implied volatility. A volatility figure is complete only with three labels: the window, the return type and the periods used to annualize it.
The historical volatility formula
ri = ln(Pi ÷ Pi−1)s = √( Σ (ri − r̄)² ÷ (n − 1) )σannual = s × √N
- Pi
- Close on day i; a window of n returns needs n + 1 closes
- ri
- Log return on day i
- r̄
- Mean of the n returns
- s
- Sample standard deviation of the returns: the daily volatility
- N
- Return periods in a year: 252 for stocks and indices, 260 for forex and spot gold, 365 for crypto
- σannual
- Annualized historical volatility
Log returns are the default because they add up: a week’s log return is the sum of its daily ones. For daily moves the choice barely matters: simple returns give US500 the same 12.74% and BTCUSD 43.91% instead of 44.00%.
Dividing by n − 1 corrects for measuring the mean from the same returns; it lifts the result over the population formula by 11.8% with 5 returns, 2.5% with 21 and 0.2% with 252. The √N step assumes each day’s return is independent of the last, so that variance grows in proportion to time; real returns behave that way only roughly.
How to calculate volatility in four steps
To calculate the volatility of a stock by hand, take US:KO’s last six closes through 23 October 2026:
- Take log returnsDivide each close by the one before and take the natural log: ln(95.34 ÷ 92.68) = +2.830% for 22 October.
- Find the meanAdd the five returns and divide by five: -0.255%.
- Take the sample standard deviationSquare each return’s distance from the mean, add the squares, divide by n − 1 = 4 and take the square root: 2.033% a day.
- AnnualizeMultiply by the square root of the periods in a year: 2.033% × √252 = 32.27% for a stock.
| Day | Close | Log return | Squared deviation |
|---|---|---|---|
| Fri 16 Oct | 93.90 | ||
| Mon 19 Oct | 93.42 | -0.512% | 0.0663 |
| Tue 20 Oct | 93.49 | +0.075% | 0.1089 |
| Wed 21 Oct | 92.68 | -0.870% | 0.3783 |
| Thu 22 Oct | 95.34 | +2.830% | 9.5157 |
| Fri 23 Oct | 92.71 | -2.797% | 6.4629 |
Volatility calculator
Annualized volatility: 252, 260 or 365 periods a year
N counts the return periods in one year of the asset’s own calendar:
| Asset class | Periods a year | Why |
|---|---|---|
| Stocks, ETFs, indices | 252 | Weekday sessions less exchange holidays, by convention |
| Forex, spot gold | 260 | Five days a week; some platforms use 252 |
| Crypto | 365 | Trades every day, weekends included |
| Weekly, monthly closes | 52, 12 | One return per week or per month |
| Intraday bars | Bars a day × days | 390 one-minute bars × 252 for US stocks |
252 is a convention: the US had 251 sessions in the year to 23 October 2026, which moves the result by only 0.2%, and how many trading days are in a year varies by market. Intraday stock bars cover the session alone and miss the overnight gap.
Calculate historical volatility in Python
historical_volatility.py pulls daily bars for seven symbols from the aggregate bars endpoint, one request each, and prints close-to-close volatility over one month, three months and a year, plus Parkinson volatility over three months. It needs Python 3.9 or newer, the standard library and a TICKERLAYER_API_KEY environment variable.
"""Historical volatility from daily bars: close-to-close and Parkinson. Python 3.9+, standard library only."""
import json
import math
import os
import statistics
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
API = "https://api.tickerlayer.com"
KEY = os.environ.get("TICKERLAYER_API_KEY") or sys.exit("Set TICKERLAYER_API_KEY first.")
FROM, TO = "2025-09-01", "2026-10-23" # TO: the last daily bar used
ASSETS = { # symbol: (asset class, return periods a year)
"US:KO": ("stocks", 252),
"US:BA": ("stocks", 252),
"US500": ("indices", 252),
"EURUSD": ("forex", 260), # weekdays
"USDJPY": ("forex", 260),
"XAUUSD": ("commodities", 260),
"BTCUSD": ("crypto", 365), # every day of the year
}
WINDOWS = (21, 63, 252) # US sessions: about one month, three months and a year
CLOSES_ONLY = {"XAUUSD"} # range estimators assume clean highs and lows: screen them before moving a symbol out
def daily_bars(asset, symbol):
"""Daily OHLC bars, oldest first, or None when the key's plan lacks the asset class."""
url = f"{API}/{asset}/agg/{symbol}/1/day/{FROM}/{TO}?sort=asc&limit=5000"
req = urllib.request.Request(url, headers={"x-api-key": KEY})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
bars = json.load(resp)["results"]
except urllib.error.HTTPError as err:
if err.code == 403:
return None
raise
if asset != "crypto": # forex and metals series can carry Saturday and Sunday rows: drop them
bars = [b for b in bars if datetime.fromtimestamp(b["t"] / 1000, timezone.utc).weekday() < 5]
return bars
def returns_in(window, periods):
"""The same span of time in each calendar: 21 US sessions = 22 weekdays = 30 days."""
return round(window * periods / 252)
def close_to_close(bars, periods):
"""Annualized sample standard deviation (n - 1) of daily log returns."""
closes = [b["c"] for b in bars]
returns = [math.log(today / before) for before, today in zip(closes, closes[1:])]
return statistics.stdev(returns) * math.sqrt(periods)
def parkinson(bars, periods):
"""Annualized Parkinson volatility from each bar's high and low."""
squares = [math.log(b["h"] / b["l"]) ** 2 for b in bars]
return math.sqrt(sum(squares) / (4 * math.log(2) * len(squares)) * periods)
print(f"Annualized volatility of daily log returns to {TO}")
print(f"{'symbol':<8}{'per year':>9}{'1 month':>10}{'3 months':>10}{'1 year':>10}{'Parkinson 3m':>14}")
for symbol, (asset, periods) in ASSETS.items():
bars = daily_bars(asset, symbol)
if bars is None:
print(f"{symbol:<8}not in this key's plan")
continue
row = f"{symbol:<8}{periods:>9}"
for window in WINDOWS:
n = returns_in(window, periods)
if len(bars) <= n:
sys.exit(f"{symbol}: {len(bars)} bars, {n + 1} needed: start earlier")
row += f"{close_to_close(bars[-(n + 1):], periods):>10.2%}"
n = returns_in(63, periods)
park = "closes only" if symbol in CLOSES_ONLY else f"{parkinson(bars[-n:], periods):.2%}"
print(row + f"{park:>14}")
for periods in sorted({p for _, p in ASSETS.values()}):
sizes = ", ".join(str(returns_in(w, periods)) for w in WINDOWS)
print(f"at {periods} a year the windows hold {sizes} returns")Annualized volatility of daily log returns to 2026-10-23
symbol per year 1 month 3 months 1 year Parkinson 3m
US:KO 252 21.14% 19.10% 19.06% 15.33%
US:BA 252 33.31% 34.00% 33.96% 27.77%
US500 252 9.88% 11.15% 12.74% 7.93%
EURUSD 260 5.12% 4.92% 5.23% 5.12%
USDJPY 260 5.39% 9.34% 7.88% 9.06%
XAUUSD 260 19.59% 24.29% 29.19% closes only
BTCUSD 365 24.69% 37.91% 44.00% 36.57%
at 252 a year the windows hold 21, 63, 252 returns
at 260 a year the windows hold 22, 65, 260 returns
at 365 a year the windows hold 30, 91, 365 returnsUS bars are raw prices, so check for splits: none fell in the year to 23 October 2026, whose largest one-day move was +10.15% for US:BA on 2 December 2025. Symbols in CLOSES_ONLY stay on close-to-close: range estimators assume clean highs and lows, so screen them before moving a symbol out.
Stock, forex, gold and crypto volatility, from 5% to 44%
One year of daily bars through 23 October 2026 puts the seven symbols on one scale, each annualized on its own calendar:
One-year historical volatility to 23 October 2026
Is 20% volatility high?
20% a year is about 1.26% on a typical day (20 ÷ √252). That is US:KO territory: high for a broad index like US500 at 12.74% or for either currency pair here, low for gold at 29.19% or bitcoin at 44.00%. Judge a figure against the asset’s own history and its peers, not a universal threshold.
Forex volatility runs lowest: EURUSD’s 5.23% means a typical day of 0.32%, about 36 pips at the 23 October close of 1.11949. Gold volatility of 29.19% includes a -10.18% day on 30 January 2026.
Rolling volatility: choosing 21, 63 or 252 sessions
Rolling volatility recomputes the figure each day over the latest window. A short window reacts within days and is noisy; a long one is stable and lags. Size it in time: 30 daily returns of BTCUSD cover 30 days, but 30 returns of US:KO reach back six weeks.
BTCUSD volatility over 30, 91 and 365 days
- 30 days
- 91 days
- 365 days
annualized volatility, %
The 30-day jump weighs two large days against 28 others instead of 363. Its drop in the week to 13 March came from the window moving past them, as did the 91-day drop in the week to 8 May.
Long windows show the same artefact a year later. US500’s 252-session volatility fell from 18.21% on 2 April 2026 to 12.57% on 24 April as the April 2025 sessions aged out, among them moves of -4.84%, -5.97% and +9.52%, while its 21-session figure peaked at 19.88% on 10 April. Match the window to the question: 21 sessions for the month ahead, 252 for a yearly risk budget.
Parkinson, Garman-Klass, Rogers-Satchell and Yang-Zhang volatility
Close-to-close volatility uses one price a day. Range-based estimators also read the open, high and low of the same OHLC bars, so they draw more information from a window. The simplest, Parkinson volatility, is σ² = Σ (ln(H ÷ L))² ÷ (4 n ln 2), annualized like the rest:
| Estimator | US:KO | US500 | BTCUSD |
|---|---|---|---|
| Close-to-close | 19.10% | 11.15% | 37.91% |
| Parkinson | 15.33% | 7.93% | 36.57% |
| Garman-Klass | 15.42% | 7.90% | 35.99% |
| Rogers-Satchell | 15.49% | 7.82% | 35.22% |
| Yang-Zhang | 21.54% | 9.90% | 35.62% |
Parkinson uses only the high and low and assumes no drift and no gap between sessions, so on stocks it misses the overnight move: it reads 20% under close-to-close on US:KO and 29% under on US500. Garman-Klass adds the open and close but shares that blind spot. Rogers-Satchell stays unbiased when the price trends through the window, and Yang-Zhang adds the overnight gap back by combining overnight (close-to-open), open-to-close and Rogers-Satchell variances.
Yang-Zhang can land above close-to-close. On US:KO, overnight gaps and the session that followed partly offset (a correlation of -0.20 in this window): close-to-close nets the two out, while Yang-Zhang adds them up. On BTCUSD, whose daily bar opens at or within a fraction of the previous close, all five estimators fall within 2.7 points of each other.
From annual volatility to an expected daily move
Divide annual volatility by √N to get back to one period: σ ÷ √252 for a stock’s day, times √5 for a five-session week, or √7 for a crypto week. At the 23 October 2026 closes, a one-standard-deviation move looked like this:
- ±0.80%US500 day: ±61 points from 7,623.68; ±1.79% (±137) a week
- ±2.30%BTCUSD day: ±$1,870 from $81,189; ±6.09% (±$4,947) a week
- 5.26%Days beyond ±2σ across seven assets; a normal curve predicts 4.55%
- 1.16%Days beyond ±3σ: 22 of 1,901, against 0.27% for a normal curve
The 68/95 rule says a normal distribution keeps 68.3% of days inside ±1σ, 95.4% inside ±2σ and 0.27% beyond ±3σ. US500 spent 73.4% of its year inside ±1σ and 94.8% inside ±2σ, close to the rule, but had 3 days beyond ±3σ where a normal curve expects 0.7. BTCUSD had 2 days beyond ±4σ; a normal curve expects one such day about once in 43 years.
Historical (realized) vs implied volatility
Implied volatility is backed out of option prices: the volatility that makes an option pricing model return the market price. It looks forward and is quoted in the same annualized percent.
Historical volatility also feeds other measures: the Sharpe ratio divides excess return by it, stock beta scales correlation by the ratio of two volatilities, and a correlation matrix in Python starts from the same daily log returns as the script above.
Six data pitfalls that distort volatility
- Use returns, not prices. The standard deviation of US:KO’s last 21 closes is $3.23, or 3.61% of their mean; the volatility of its returns over 21 sessions is 21.14% a year.
- Drop weekend rows from weekday series. Some daily forex and metals series carry Saturday and Sunday rows. Kept and annualized with √260, EURUSD’s last 365 days read 4.61%; the same days without them read 5.22%.
- Adjust for splits. An unadjusted 2-for-1 split is a -69.3% log return; one such day would lift US:KO’s 19.06% year to 74.64%. See how splits show up in historical stock data.
- Screen bad prints. A wrong close distorts two returns, and a wrong high or low enters a range estimator at full size: filter bad ticks first.
- Drop stale holiday bars. A row with no trading repeats the previous close, adds a zero return and pulls volatility down.
- Know where the day ends. BTCUSD’s daily bars run from 00:00 to 24:00 UTC, so its close is not the 4 p.m. New York one; compare round-the-clock assets on the same cut.
Questions
What does historical volatility mean?
It is the annualized standard deviation of an asset’s past returns over a stated window: how far the price typically moved, not which way. Over the 252 sessions to 23 October 2026, US500’s was 12.74%.
Is volatility the same as standard deviation?
Volatility is the standard deviation of returns, scaled to a period, usually a year. The standard deviation of prices is not volatility: it grows with the price level and mixes trend with noise.
Is 20% volatility high?
It depends on the asset. 20% a year is about 1.26% a day: close to US:KO’s 19.06% in the year to 23 October 2026, high next to US500’s 12.74%, low next to bitcoin’s 44.00%.
What is the difference between implied and realized volatility?
Realized, or historical, volatility is measured from past prices. Implied volatility is backed out of option prices and reflects what the market expects; both are quoted as annualized percentages.
How do you annualize volatility for crypto?
Multiply the daily standard deviation by the square root of 365, because crypto trades every day. Using √252 understates it by 16.9%: BTCUSD’s year to 23 October 2026 reads 44.00% at 365 and 36.56% at 252.
How do I check the historical volatility of a stock?
Take its daily closes, compute log returns, take their sample standard deviation and multiply by √252. The calculator on this page does it for pasted closes; the Python script does it from daily bars.