The realised variance¶
The realised variance one sample on, v' = v + r^2: the sum of the squared log returns over the window so far, the mean return taken as zero, each sample's square added as it is taken.
Both dimensionless: a total over the window and a log return. The window (a day's samples, reset at the open) and the sampling are the game's; its square root over a year is lusine.finance.annualised_volatility's. Source: Andersen, Bollerslev, Diebold & Labys, Modeling and Forecasting Realized Volatility, Econometrica 71 (2003), Eq. (7): the realised variance as the sum of the squared intraday returns. Checked against a reference in numpy, which has its own tests.
- Source
- Andersen, Bollerslev, Diebold & Labys, Modeling and Forecasting Realized Volatility, Econometrica 71 (2003), Eq. (7).
- Proof
- reference, invariant, limit.
realised_variance.trait
namespace lusine.finance;
float realised_variance(float total, float sample)
{
return total + sample * sample;
}
The sum of the squared log returns over the window so far, one sample on, v + r^2 (Andersen, Bollerslev, Diebold & Labys, 2003, Eq. 7).
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import trait_run
F32 = np.float32
U = float(np.finfo(F32).eps) / 2 # binary32's unit roundoff, 2^-24
The kernel¶
The law as the library states it, and as this page runs it.
print(trait_run.source('finance/realised_variance.trait'))
// lusine.finance.realised_variance: the realised variance one sample on, v' = v + r^2: the sum of the squared log
// returns over the window so far, the mean return taken as zero, each sample's square added as it is taken. Both
// dimensionless: a total over the window and a log return. The window (a day's samples, reset at the open) and the
// sampling are the game's; its square root over a year is lusine.finance.annualised_volatility's.
// Source: Andersen, Bollerslev, Diebold & Labys, Modeling and Forecasting Realized Volatility, Econometrica 71 (2003),
// Eq. (7): the realised variance as the sum of the squared intraday returns.
// Checked against a reference in numpy, which has its own tests.
namespace lusine.finance;
float realised_variance(float total, float sample)
{
return total + sample * sample;
}
The program¶
One series a lattice item: each step the host writes the new sample, and main() adds its square into the item's total by the kernel. Resetting the total at a window's start is the game's.
RV = """namespace rv;
lusine.version version = { { number = 1 } };
struct window { float total; host float sample; };
lattice<window, 1> windows(spacing = 1 m, boundary = wall);
void main() { windows.total = lusine.finance.realised_variance(windows.total, windows.sample); }
"""
print(RV)
def totals(samples, start=0.0):
"""The total after each sample: samples [step, series]."""
samples = np.atleast_2d(np.asarray(samples, dtype=float).T).T
run = trait_run.Program(['finance/realised_variance.trait'], RV, {'rv.windows': [samples.shape[1]]})
run.store('rv.windows', 'total', np.broadcast_to(start, samples.shape[1:]))
out = []
for row in samples:
run.store('rv.windows', 'sample', row)
out.append(run.steps('rv.main', 1, [('rv.windows', 'total')])[0][0])
return np.array(out)
namespace rv;
lusine.version version = { { number = 1 } };
struct window { float total; host float sample; };
lattice<window, 1> windows(spacing = 1 m, boundary = wall);
void main() { windows.total = lusine.finance.realised_variance(windows.total, windows.sample); }
Reference: RiskMetrics' returns, squared and summed¶
USD/DEM's twenty daily returns of RiskMetrics' Table 5.3 (28 March to 24 April 1996), taken as one window: after each day the total is the running sum of the squares, bit for bit with a binary32 model adding them in order, and the twenty days' realised variance is 3.085 %^2 to the digits the printed returns carry.
r = np.array([0.633, 0.115, -0.459, 0.093, 0.176, -0.087, -0.142, 0.324, -0.943, -0.528, -0.107,
-0.159, -0.445, 0.053, 0.152, -0.318, 0.424, -0.708, -0.105, -0.257]) / 100
got = totals(r)[:, 0]
model, t = [], F32(0.0)
for x in F32(r):
t = F32(t + F32(x * x))
model.append(t)
assert np.all(got == np.array(model)), got
assert abs(got[-1] * 1e4 - 3.0849) < 0.0005, got[-1] * 1e4
print('%^2 after each day:', np.round(got * 1e4, 4))
fig, ax = plt.subplots(figsize=(7, 3))
ax.step(np.arange(1, 21), got * 1e4, where='post')
ax.set_xlabel('day')
ax.set_ylabel('realised variance so far (%^2)')
plt.show()
%^2 after each day: [0.4007 0.4139 0.6246 0.6332 0.6642 0.6718 0.692 0.7969 1.6862 1.965 1.9764 2.0017 2.1997 2.2025 2.2256 2.3268 2.5065 3.0078 3.0188 3.0849]
Invariant: windows end to end¶
Four series of 200 samples: the total over all 200 is the total over the first 100 added to the one over the last 100, within the run's rounding; the variance over a day is its sessions' summed.
rng = np.random.default_rng(418)
s = rng.normal(0.0, 0.01, (200, 4))
whole = totals(s)[-1]
first, second = totals(s[:100])[-1], totals(s[100:])[-1]
assert np.all(np.abs(whole / (first.astype(float) + second.astype(float)) - 1) <= 200 * U)
print('whole', whole, 'parts', first + second)
whole [0.0188243 0.0187712 0.0209738 0.01845861] parts [0.0188243 0.0187712 0.02097379 0.01845861]
Limit: a sample of nothing, and a fall¶
A sample of nothing keeps the total bit for bit; a fall adds what its rise would.
kept = totals(np.zeros((5, 1)), 3e-4)[:, 0]
assert np.all(kept == F32(3e-4)), kept
up, down = totals(np.array([[0.02], [0.0]]), 3e-4)[0, 0], totals(np.array([[-0.02], [0.0]]), 3e-4)[0, 0]
assert up == down and up > F32(3e-4)
print(kept, up, down)
[0.0003 0.0003 0.0003 0.0003 0.0003] 0.000699999975040555 0.000699999975040555