Skip to content

The annualised volatility, live

A variance per period as a volatility a year, σ = √(v n): the standard deviation of the log return over a year, from the variance over one sampling period and the number of periods a year, the returns of consecutive periods independent, so their variances add (lusine.finance.annualised_volatility). Below, a spread of 1% a period is annualised on your GPU as you read over one period a year, two, four and so on to 131 072, a doubling a step; and each step sixty-four variances from 10⁻⁶ to 10⁻² are drawn with 1 to 100 000 periods a year and annualised, then again at four times the periods.

This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.

What it shows

  • The strip above the controls is green while every check holds, and turns red for good once one fails.
  • The sweep: the chart draws the volatility a year at 1% a period against the periods a year in decades, log₁₀ n, on a log scale: a line of slope ½, from 1% at one period to 3.6 at 131 072. Below it, the periods and the volatility.
  • The checks: Hull's trading year, a daily standard deviation of 1% over 252 trading days a volatility of 15.87% a year, to the book's four digits (Hull, Options, Futures, and Other Derivatives, ch. 15); the float64 reference, eight variances and counts each within 2 units in the last place (4 unit roundoffs, 2⁻²⁴) of √(v n) in float64, the product rounding once and the root correctly rounded; variances add, every estimate squared its variance over the year, v n, within 8 unit roundoffs; four times the periods, every estimate's volatility doubled at four times the periods, within 8 unit roundoffs; nothing, and one period, no variance no volatility, and one period a year the period's own spread, √v, bit for bit. Each is held at every step.

Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The seed and the sweep's variance are the program's settings, which the host may write; this page offers no control to change them yet.

The program

The estimates, the cases, the sweep and the checks, the panels the page draws, and the one procedure, main, run each step: the sweep a doubling on, the estimates drawn and annualised, and the checks.

// The annualised volatility's live page: a 1% spread a period swept from one period a year to 131 072, the library's
// volatility a year at each on a log scale; sixty-four variances and counts of periods drawn each step, each squared
// back and quadrupled; eight held to a reference in float64, and Hull's trading year, with the page's checks computed
// here on the GPU and shown by the panels the program declares.

namespace annualised_volatility;

lusine.version version = { { number = 1 } };

// The reader's settings: the seed the estimates are drawn from, and the sweep's variance a period, a 1% spread's.
host struct { integer seed = 417; float variance = 0.0001; } settings;

// Sixty-four estimates drawn each step: a variance from 1e-6 to 1e-2 and 1 to 100 000 periods a year; the library's
// volatility a year, and at four times the periods; whether it squares back to the variance over the year, and whether
// four times the periods doubles it.
struct estimate {
  integer index; float variance; float periods; float sigma; float quadrupled; bool squares; bool doubles;
};
table<estimate> estimates(capacity = 64) = {
  { index = 0 }, { index = 1 }, { index = 2 }, { index = 3 }, { index = 4 }, { index = 5 }, { index = 6 },
  { index = 7 }, { index = 8 }, { index = 9 }, { index = 10 }, { index = 11 }, { index = 12 }, { index = 13 },
  { index = 14 }, { index = 15 }, { index = 16 }, { index = 17 }, { index = 18 }, { index = 19 }, { index = 20 },
  { index = 21 }, { index = 22 }, { index = 23 }, { index = 24 }, { index = 25 }, { index = 26 }, { index = 27 },
  { index = 28 }, { index = 29 }, { index = 30 }, { index = 31 }, { index = 32 }, { index = 33 }, { index = 34 },
  { index = 35 }, { index = 36 }, { index = 37 }, { index = 38 }, { index = 39 }, { index = 40 }, { index = 41 },
  { index = 42 }, { index = 43 }, { index = 44 }, { index = 45 }, { index = 46 }, { index = 47 }, { index = 48 },
  { index = 49 }, { index = 50 }, { index = 51 }, { index = 52 }, { index = 53 }, { index = 54 }, { index = 55 },
  { index = 56 }, { index = 57 }, { index = 58 }, { index = 59 }, { index = 60 }, { index = 61 }, { index = 62 },
  { index = 63 },
};

// Eight estimates and each one's volatility a year in float64, sqrt(v n) at the variance as binary32 holds it.
struct case { float variance; float periods; float reference; float sigma; };
host table<case> cases(capacity = 8) = {
  { variance = 0.009528136812150478, periods = 99020.0, reference = 30.71605617814794 },
  { variance = 0.004307356663048267, periods = 16524.0, reference = 8.436513586796952 },
  { variance = 0.008099030703306198, periods = 28293.0, reference = 15.137565051508194 },
  { variance = 0.003959042020142078, periods = 43541.0, reference = 13.12938112018256 },
  { variance = 0.006177724339067936, periods = 81732.0, reference = 22.4703752901615 },
  { variance = 0.0009092894615605474, periods = 41991.0, reference = 6.179156397145888 },
  { variance = 0.009823093190789223, periods = 70332.0, reference = 26.284554215253255 },
  { variance = 0.007932075299322605, periods = 69349.0, reference = 23.4538161059714 },
};

// Hull's trading year, a daily spread of 1% over 252 trading days; and, for the limits, a variance of 2.5e-5 and one
// period a year.
host struct { float daily = 0.0001; float trading = 252.0; float small = 0.000025; float one = 1.0; } hull;

// The sweep: the steps taken; the periods a year, a power of two from 1 to 131 072, and in decades, log10 of the count;
// the volatility a year at a 1% spread a period, and at four times the periods.
struct { integer step; integer doublings = -1; float periods; float decades; float sigma; float quadrupled; } sweep;

// The checks: Hull's daily 1% over 252 trading days, 15.87% a year to the book's four digits; the eight estimates each
// within 2 units in the last place (4 unit roundoffs, 2^-24) of the float64 reference; each estimate squared its
// variance over the year, v n, within 8 unit roundoffs; four times the periods twice the volatility, within 8; and no
// variance no volatility, one period the period's own spread, bit for bit.
struct { bool hull = true; bool reference = true; bool squared = true; bool quadrupled = true; bool limits = true; }
  checks;

// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);

vector<float, 4> lamp_colour(bool holds)
{
  return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}

// binary32's unit roundoff, 2^-24.
float u() { return 1.0 / 16777216.0; }

float drawn(integer seed, integer step, bits key, integer index)
{
  return lusine.error.uniform(drawn = lusine.error.draw(seed = bits(seed), step = bits(step), key,
                                                        index = bits(index)));
}

(float, float) drawing(integer index, integer seed, integer step)
{
  return (0.000001 + 0.009999 * drawn(seed, step, key = fnv1a("variance"), index),
          floor(1.0 + 99999.0 * drawn(seed, step, key = fnv1a("periods"), index)));
}

// An estimate's volatility squared its variance over the year; at four times the periods, twice it.
bool squaring(float variance, float periods, float sigma)
{
  return abs(sigma * sigma / (variance * periods) - 1.0) <= 8.0 * u();
}
bool doubling(float sigma, float quadrupled) { return abs(quadrupled / (2.0 * sigma) - 1.0) <= 8.0 * u(); }

integer failing(table<bool> holds) { return count(holds) where !holds; }

integer off(table<float> sigmas, table<float> references)
{
  return count(sigmas) where abs(sigmas - references) > 4.0 * u() * references;
}

bool limits(float daily, float small, float trading, float one)
{
  return lusine.finance.annualised_volatility(variance = 0.0 * daily, periods = trading) == 0.0
      && lusine.finance.annualised_volatility(variance = daily, periods = one) == sqrt(daily)
      && lusine.finance.annualised_volatility(variance = small, periods = one) == sqrt(small);
}

output table<lusine.widget> panels = {
  lusine.series { { panel = "the sweep", chart = "volatility a year", quantity = sweep.sigma, steps = 18,
                    against = sweep.decades, log = true, label = "1% a period" } },
  lusine.value { { panel = "the sweep", quantity = sweep.periods, label = "periods a year" } },
  lusine.value { { panel = "the sweep", quantity = sweep.sigma, label = "volatility a year" } },
  lusine.value { { panel = "the checks", quantity = checks.hull, label = "Hull's trading year" } },
  lusine.value { { panel = "the checks", quantity = checks.reference, label = "the float64 reference" } },
  lusine.value { { panel = "the checks", quantity = checks.squared, label = "variances add" } },
  lusine.value { { panel = "the checks", quantity = checks.quadrupled, label = "four times the periods" } },
  lusine.value { { panel = "the checks", quantity = checks.limits, label = "nothing, and one period" } },
};

void main()
{
  sweep.step = sweep.step + 1;
  sweep.doublings = sweep.doublings >= 17 ? 0 : sweep.doublings + 1;
  sweep.periods = sweep.doublings == 0 ? 1.0 : 2.0 * sweep.periods;
  sweep.decades = float(sweep.doublings) * 0.3010299956639812;
  sweep.sigma = lusine.finance.annualised_volatility(settings.variance, sweep.periods);
  sweep.quadrupled = lusine.finance.annualised_volatility(settings.variance, periods = 4.0 * sweep.periods);
  (estimates.variance, estimates.periods) = drawing(estimates.index, settings.seed, step = sweep.step);
  estimates.sigma = lusine.finance.annualised_volatility(estimates.variance, estimates.periods);
  estimates.quadrupled = lusine.finance.annualised_volatility(estimates.variance, periods = 4.0 * estimates.periods);
  estimates.squares = squaring(estimates.variance, estimates.periods, estimates.sigma);
  estimates.doubles = doubling(estimates.sigma, estimates.quadrupled);
  cases.sigma = lusine.finance.annualised_volatility(cases.variance, cases.periods);
  checks.hull = checks.hull
                && abs(lusine.finance.annualised_volatility(variance = hull.daily, periods = hull.trading) - 0.1587)
                   < 0.0001;
  checks.reference = checks.reference && off(sigmas = cases.sigma, references = cases.reference) == 0;
  checks.squared = checks.squared && failing(holds = estimates.squares) == 0
                   && squaring(settings.variance, sweep.periods, sweep.sigma);
  checks.quadrupled = checks.quadrupled && failing(holds = estimates.doubles) == 0
                      && doubling(sweep.sigma, sweep.quadrupled);
  checks.limits = checks.limits && limits(hull.daily, hull.small, hull.trading, hull.one);
  lamps.colour = lamp_colour(holds = checks.hull && checks.reference && checks.squared && checks.quadrupled
                                     && checks.limits);
}

The kernel's other proof (three hundred estimates against a reference in float64) is on its proof page.