Time Series¶
timeseries
¶
Heavy tails that come from volatility clustering rather than from the shocks.
Financial returns are heavy tailed and they are volatility clustered, and the second explains a large part of the first. Fitting a static distribution to raw returns conflates the two, and the conflation is not small:
============================ ==================== ================== Innovations Their tail index Raw returns show ============================ ==================== ================== Gaussian infinite 4.50 Student-t, 5 df 5 2.85 ============================ ==================== ==================
Measured on 60,000 simulated GARCH(1,1) returns with alpha = 0.10 and
beta = 0.88. The first row is the one to sit with: the innovations have no
power-law tail whatsoever, and the returns they generate have a tail index of
4.5. Estimate a tail index on raw returns and a good part of what is measured
is the volatility process, not the shocks.
Which of the two you want depends on the question:
The conditional tail -- the innovation distribution -- answers "how large is tomorrow's shock, given what volatility is now". Estimate it on the standardised residuals.
The unconditional tail -- what the raw returns show -- answers "how large is a return picked at random from the next decade". That is the right one for long-horizon capital, and it is heavier.
There is a second consequence, and it breaks the simplest return-period
calculation. The simplest iid extreme value theory assumes independent
observations; for stationary dependent sequences, clustering must be accounted
for, for example through the extremal index. :func:extremal_index
estimates that correction. Under the usual limiting cluster interpretation, a
value of 0.5 corresponds to an effective mean cluster size near two, so a
"hundred-year event" happens half as often as its marginal probability suggests
-- but when it does, it often brings another with it.
GARCH11
dataclass
¶
GARCH(1,1) with normal or Student-t innovations.
The variance recursion is sigma2_t = omega + alpha r2_{t-1} + beta
sigma2_{t-1} and the return is r_t = sigma_t z_t with z of unit
variance. alpha + beta is the persistence: how much of today's
volatility survives into tomorrow, and how slowly a shock decays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
omega
|
float
|
Constant in the variance recursion, positive. |
required |
alpha
|
float
|
Weight on the last squared return, non-negative. |
required |
beta
|
float
|
Weight on the last variance, non-negative. |
required |
nu
|
float | None
|
Degrees of freedom of the innovations, above two, or None for
normal innovations. The innovations are standardised to unit
variance either way, so |
None
|
Raises:
| Type | Description |
|---|---|
ParameterError
|
If any parameter is out of range, or if
|
Examples:
>>> model = GARCH11(omega=1e-6, alpha=0.1, beta=0.85, nu=5.0)
>>> round(model.persistence, 6)
0.95
>>> round(model.unconditional_variance * 1e6, 4)
20.0
>>> len(model.simulate(500, seed=1))
500
unconditional_variance
property
¶
omega / (1 - alpha - beta), the long-run variance.
conditional_variances
¶
The variance the model implies at each step, given the data.
The recursion is deterministic once the returns are known, which is what makes a GARCH likelihood computable at all: the volatility is latent but not random given the past.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Sequence[float]
|
The observed series. |
required |
Returns:
| Type | Description |
|---|---|
list[float]
|
One variance per return, starting from the unconditional variance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in heavytails/timeseries.py
log_likelihood
¶
Log likelihood of the series under this model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Sequence[float]
|
The observed series. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The sum over steps of the innovation log density less half the log |
float
|
variance -- the Jacobian of dividing by the volatility. |
Source code in heavytails/timeseries.py
simulate
¶
Simulate n returns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of returns, positive. |
required |
seed
|
int | None
|
Seed for reproducibility. |
None
|
burn_in
|
int
|
Steps discarded so the variance forgets its start. The default is generous; at a persistence of 0.99 a shock takes hundreds of steps to decay and too short a burn-in leaves the series still remembering its initial condition. |
1000
|
Returns:
| Type | Description |
|---|---|
list[float]
|
The simulated returns. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in heavytails/timeseries.py
standardized_residuals
¶
r_t / sigma_t: what is left once the volatility is divided out.
This is the series to estimate a tail index on. The raw returns carry the innovation tail and the volatility dynamics together, and a Hill estimate of them measures a mixture of the two. Dividing by the fitted volatility leaves the innovations, whose tail is the one a conditional question is about.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Sequence[float]
|
The observed series. |
required |
Returns:
| Type | Description |
|---|---|
list[float]
|
The standardised residuals. |
Source code in heavytails/timeseries.py
decluster
¶
Reduce clustered exceedances to one value per cluster.
Exceedances separated by fewer than run_length non-exceedances are
treated as one episode, and the largest is kept. That is what makes the
remaining values approximately independent, which is what the classical
peaks-over-threshold machinery in :mod:heavytails.threshold assumes.
The run length is a choice, and the answer depends on it. That is the
known weakness of the runs method, and the reason
:func:extremal_index uses the intervals estimator instead. Comparing the
cluster count here against extremal_index * n_exceedances is a cheap
way to see whether the choice was reasonable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Sequence[float]
|
The observed series. |
required |
threshold
|
float
|
Exceedances above this define the extremes. |
required |
run_length
|
int
|
Non-exceedances needed to separate two clusters. |
1
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> series = [0.0, 5.0, 6.0, 0.0, 0.0, 0.0, 7.0, 0.0]
>>> result = decluster(series, threshold=1.0, run_length=2)
>>> result["cluster_maxima"]
[6.0, 7.0]
>>> result["n_exceedances"], result["n_clusters"]
(3, 2)
Source code in heavytails/timeseries.py
extremal_index
¶
Estimate the extremal index by the Ferro-Segers intervals method.
Under the usual limiting cluster interpretation, the extremal index is the reciprocal of the effective mean cluster size. The simplest iid extreme value theory quietly sets it to one. A value near 0.5 says the rate of independent extreme episodes is about half what counting exceedances suggests, so a "hundred-year event" happens half as often -- and often brings a companion when it does.
The intervals estimator works from the gaps between exceedances and needs no declustering parameter, which is its advantage over the runs method: a run length has to be chosen, the answer depends on the choice, and there is rarely a principled way to make it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Sequence[float]
|
The observed series. |
required |
threshold
|
float
|
Exceedances above this define the extremes. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer than three exceedances are found, where there are at most two gaps and nothing to estimate from. |
Examples:
Independent data has an extremal index of one, which the estimator recovers:
>>> from heavytails import Pareto
>>> sample = Pareto(alpha=2.0, xm=1.0).rvs(20000, seed=1)
>>> result = extremal_index(sample, threshold=8.0)
>>> result["extremal_index"] > 0.85
True
Source code in heavytails/timeseries.py
fit_garch11
¶
Fit a GARCH(1,1) by maximum likelihood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Sequence[float]
|
The observed series, at least fifty observations. |
required |
innovations
|
str
|
|
't'
|
max_iter
|
int
|
Maximum simplex iterations. |
2000
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the series is too short, has no variation, or
|
Examples:
>>> truth = GARCH11(omega=2e-6, alpha=0.08, beta=0.90, nu=6.0)
>>> fit = fit_garch11(truth.simulate(4000, seed=1))
>>> 0.90 < fit["model"].persistence < 0.995
True