The Sqrt(t) Volatility Band Is Wrong in Both Directions; Here's What to Compute Instead
Sigma*sqrt(t) misprices volatility bands for fat-tailed series, and the error flips sign with horizon. Here is what to compute instead.
Join the DZone community and get the full member experience.
Join For FreeIf you have ever drawn a forecast band around a price series, you have probably reached for the same formula everyone reaches for: take a volatility estimate, multiply by the square root of the horizon, and call the result your interval. sigma * sqrt(t) is the closed-form move; it is one line of code, and for fat-tailed financial series it is wrong in a way that is worth understanding precisely, because the error does not even keep the same sign as you change the horizon.
This is a short engineering write-up on what breaks, how we measured it, and the handful of things you actually have to compute to build an honest, reproductible interval.
What Sqrt(t) Assumes, and Why Crypto Violates It
The sigma * sqrt(t) scaling falls out of assuming i.i.d. Gaussian log-returns: variance adds linearly in time, so standard deviation scales with the root of time, and a fixed multiple of it gives a fixed-probability band. Two assumptions are doing all the work — normality and independence — and daily crypto returns honor neither. Realized kurtosis on daily BTC log-returns sits near 16 against the Gaussian's 3. Returns are close to uncorrelated, but their magnitudes are strongly autocorrelated (volatility clustering), so the independence the formula needs is not there either.
We measured the damage against the full Binance history rather than a recent window — 3,261 daily bars for BTC back to 2017. The quantity of interest is the ratio of an empirically measured 80% band to the sigma * rt(t) band at each horizon:
horizon empirical80 / sqrt-t band (BTC)
1 day ~0.80
7 days ~0.88
30 days ~1.00
Read that carefully: at one day the parametric band is too wide (0.80), and by thirty days it is about right (1.00). The error changes sign with the horizon, so there is no single scale factor you can bake in to fix it. The reason the short-horizon band is too wide despite fat tails is that the kurtosis lives in the extreme tails, not in the 10th–90th-percentile shoulders — so the 80% interval is actually narrower than Gaussian while the 99% interval is much wider. Fat tails and a narrow 80% band coexist, which is exactly the kind of thing a parametric shortcut hides from you.
Compute the Empirical Band Instead
The fix is to stop parameterizing and read the interval straight off the empirical distribution of realized h-day log-returns:
import numpy as np
def empirical_band(prices, horizon, lo=0.10, hi=0.90):
p = np.asarray(prices, dtype=float)
# overlapping h-day log-returns
r = np.log(p[horizon:] / p[:-horizon])
q_lo, q_hi = np.quantile(r, [lo, hi])
spot = p[-1]
return spot * np.exp(q_lo), spot * np.exp(q_hi)
That is the whole idea, and it already beats the parametric band because it makes no distributional assumption. But there is a trap in the line that builds r.
The Overlapping-Windows Trap
Those h-day returns overlap: consecutive 30-day windows share 29 days of data. Overlapping samples are heavily autocorrelated, so if you report len(r) as your sample size, you are overstating your evidence by roughly the horizon. For BTC's 30-day band, ~3,231 overlapping windows correspond to only about 107 independent months. Quantile estimates from overlapping windows are still usable, but their uncertainty is far larger than the raw count implies, and you must report the independent count, not the overlapping one:
def independent_count(n_bars, horizon):
return max(1, (n_bars - horizon) // horizon)
We print band from N independent windows directly on every chart for this reason. A band from 107 independent months is a different epistemic object than one implying 3,231 samples, and collapsing the two is how backtests quietly manufacture confidence.
Evaluate With Coverage, in Both Directions
The metric for an interval forecast is coverage, and the failure is symmetric. A claimed 50% band should contain the outcome about half the time across many out-of-sample days. If it contains 90%, the band is padded — a failure that a one-sided "were we inside?" check will never catch, because padding always looks safe. So score both the 50% core and the 80% band against their targets, over many days, and treat over-coverage as a miss.
Make It Reproducible and Tamper-Evident
The last piece is provenance, and it is pure engineering. We serialize each forecast object, hash it with SHA-256, and anchor the hash to the Bitcoin blockchain via OpenTimestamps before publishing. One gotcha worth flagging because it cost us a real bug: hash the exact bytes you publish. We were writing the JSON with a trailing newline but hashing the object without it, so a reader running shasum on the published file got a different digest — which reads as fraud even though nothing was wrong. Write the file, hash the file, timestamp the file; byte-for-byte, no re-serialization in between.
None of this is an edge, and the write-up would be dishonest if it implied one. An empirical band lowers the cost of being wrong about volatility; it does not tell you direction. No method reliably beats a liquid market, and anyone promising that is selling something. What the empirical quantile buys you is a band whose width means what it says — and a pipeline where any reader can recompute the number and check the timestamp themselves.
The live version, scored in public with the misses kept, is at neuportal.ai/experiment.
Educational content — not financial advice.
Opinions expressed by DZone contributors are their own.
Comments