The data, and the best normal
The series is the one of case studies 5.3.5 and 5.4.6: 1,000 invented daily log returns. They were drawn with seed 50 from a Student t with 4 degrees of freedom and scaled to a volatility near 1%. They are illustrative, not market data.
Work in percent: . Then the scale is near 1, like the degrees of freedom, which suits an optimiser and a single Hessian bump (lesson 4.3.1). The desk notes below show what changes in decimals.
Lesson 5.9.1 gives the normal's maximum likelihood estimates in closed form: the mean and the standard deviation dividing by . Here they are and . That is case study 5.3.5's volatility: it was the maximum likelihood estimate all along.
At the peak the normal's log-likelihood is . If the days really were normal, lesson 5.9.2 would give standard errors of for the mean and for the volatility.
Try it
The cell regenerates the shared returns exactly, then fits the normal by lesson 5.9.1's formulas.
The log-likelihood is computed twice, by summing norm.logpdf and by the closed form above. The last line holds the two standard errors.
The Student t model
The model takes a standard t with degrees of freedom (lesson 5.4.4). It stretches it by a scale and shifts it to a location : . Its density is the t's, changed as lesson 5.6.1 changes a density under :
Here is the t's normalising constant. SciPy computes as t.logpdf(x, nu, m, s), so the log-likelihood is one sum.
Rule: The scaled t
is in percent, in percent and has no units. The scale is not the volatility: the volatility is , larger than .
Case study 5.4.6 fixed and chose , so that the variance matched the sample's. Here all three parameters are free, and the likelihood picks them.
Fitting by maximum likelihood
There is no formula for the peak, so minimise the negative log-likelihood numerically, as in lesson 5.9.1. The scale and the degrees of freedom must stay positive, so search over and . Start from the normal fit and .
| location | scale | log-likelihood | ||
|---|---|---|---|---|
| normal | 1.0073% (volatility) | none | ||
| t of case study 5.4.6 | 0.7123% | 4 | ||
| t by maximum likelihood | 0.0221% | 0.7386% | 4.353 |
scipy.stats.t.fit finds the same peak to the digits shown. The fitted t's volatility is , close to the sample's 1.0073%.
The hand-chosen t of case study 5.4.6 sits only 1.03 below the peak. The data barely tell from ; the standard error below says how barely.
Why the location moved
The fitted location is , while the sample mean is . In the normal's log-likelihood a day scales from the centre costs ; in the t's it costs , which grows only like a log.
A day 5 scales out costs 12.5 under the normal but 5.11 under the fitted t. So the extreme days pull the t's location much less. Here that lands it nearer the recipe's , though one sample proves nothing.
Try it
nll takes and the data; minimize starts from the normal fit. Then t.fit, which returns the degrees of freedom first: (df, loc, scale).
The last lines are the log-likelihoods of the normal and of case study 5.4.6's t, and the fit's gain over each. Then the fitted volatility and the two costs of a 5-scale day.
Standard errors from the Hessian
Lesson 5.9.2's recipe: the Hessian of the negative log-likelihood at the peak is the observed information . The standard errors are the square roots of the diagonal of . Take the Hessian in the natural parameters , by lesson 4.3.1's central differences.
| location | scale | ||
|---|---|---|---|
| estimate | 0.0221% | 0.7386% | 4.353 |
| standard error | 0.0274% | 0.0284% | 0.597 |
| 95% interval | to 0.076 | 0.683 to 0.794 | 3.18 to 5.52 |
The interval for contains 4, the value the recipe used and case study 5.4.6 chose. It stays well clear of 2, below which the fitted t would have no variance.
The location's standard error, 0.0274%, is below the normal's . When the data really are t, the t's location is the more precise estimate, because it does not lean on a few wild days.
The scale and the degrees of freedom trade off
The estimates of and have correlation . A larger means thinner tails and a smaller factor , so a larger is needed for the same spread. As in lesson 5.9.2's line, the two move together.
So 1 over the square root of the diagonal entry for gives 0.45, a quarter below the correct 0.597. Always invert the whole matrix.
Try it
The fit is repeated so the cell runs on its own. hessian is lesson 4.3.1's function, applied to the negative log-likelihood in at the peak.
The lines print , the standard errors, the diagonal-only error for , the correlation of and , and the three intervals.
A profile likelihood for the degrees of freedom
The interval for rests on the quadratic copy of the log-likelihood. Lesson 5.9.2's likelihood-ratio interval needs no copy.
For each fixed , maximise over and . The result is the profile log-likelihood : how well the best t with that explains the data.
| 3 | 3.5 | 4 | 4.353 | 5 | 6 | 8 | 20 | |
|---|---|---|---|---|---|---|---|---|
| drop below the peak | 4.38 | 1.4 | 0.2 | 0 | 0.48 | 2.35 | 7.35 | 29.63 |
The 95% likelihood-ratio interval keeps every within 1.92 of the peak: 3.38 to 5.81. It is lopsided, reaching 1.45 above and 0.97 below it. A change of matters less to the shape when is large, so the profile falls more slowly to the right.
The Hessian interval, 3.18 to 5.52, is symmetric by construction and misses that lean. For a tail parameter the likelihood-ratio interval is the one to report.
The left tail of the returns against the two fitted models, and the profile log-likelihood of . The normal's tail falls far below the sample's; the fitted t stays close, a little under the worst few days. The profile is lopsided, so the likelihood-ratio interval for leans right.
Try it
profile(nu) maximises over and with held fixed. The loop prints the table; brentq (lesson 1.3.2) finds where the profile crosses 1.92 below the peak.
This cell runs about 30 small fits, so it takes a few seconds.
Is the t really better? Comparing log-likelihoods
The t gains 69.58 in log-likelihood over the normal, with one more parameter. The normal is the t with (lesson 5.4.4), so the question is whether letting be finite helps by more than chance.
The likelihood-ratio test compares with the 0.95-quantile of , 3.841, the same number as the interval's 1.92 doubled. In the language of lesson 5.8.3, the p-value is about : the normal is rejected.
One caution: is the edge of the range, where the rule is only rough. With a statistic 36 times the threshold, the verdict does not depend on it.
Try it
The two peaks of the log-likelihood come from the closed form for the normal and from t.fit for the t. Then the statistic, the threshold, the p-value from chi2.sf, and the likelihood ratio .
The fitted tails against the sample's
A fitted model is only as good as its tails, where risk limits bite. Case study 5.3.5 read value at risk (VaR) per $1,000,000 from the sample and from a normal. The fitted t gives a third answer, from all 1,000 days at once.
| VaR, $ thousands | 95% | 97.5% | 99% | 99.5% | 99.9% |
|---|---|---|---|---|---|
| sample (case study 5.3.5) | 15.5 | 20.4 | 26.2 | 36.3 | 52.3 |
| normal | 16.5 | 19.6 | 23.2 | 25.7 | 30.7 |
| fitted t | 15.1 | 19.5 | 25.9 | 31.5 | 47.5 |
At 99% the fitted t gives about $25,900, close to the sample's $26,212, where the normal says $23,221. At 99.9% the normal is far too small. The t is below the sample there, but the sample's figure rests on the worst one or two days.
Counts of large days tell the same story. Case study 5.4.6 set them against the normal and its hand-chosen t; the fitted t joins them.
| days beyond volatilities | 2 | 3 | 4 | 5 |
|---|---|---|---|---|
| in the sample | 41 | 13 | 6 | 4 |
| normal expects | 45.5 | 2.7 | 0.063 | 0.001 |
| t of case study 5.4.6 expects | 47.4 | 13.2 | 4.81 | 2.11 |
| fitted t expects | 48 | 12.6 | 4.28 | 1.77 |
The two t's agree with each other and with the sample to within the noise of a few days. The normal expects 0.063 days beyond 4 volatilities, where the sample has 6.
Try it
var is case study 5.3.5's dollar VaR, , with the model's quantile in decimals, printed in thousands. The loop prints the VaR table.
The counts use case study 5.4.6's standardised days, . The fitted t's chance of such a day is its probability outside .
Try it
The first line is the log-likelihood in decimals, then and , and the fitted t's excess kurtosis. Then the four windows of 250 days, and a fit to normal data.
Fit a Student t and read its value at risk
Write two functions. Both take r, a NumPy array of daily log returns in decimals.
fit_t(r)returns(loc, scale, nu, loglik): the maximum likelihood estimates of the scaled t, in the units ofr, and the log-likelihood at the peak.- Raise
ValueErrorifrhas fewer than 20 values or any value that is not finite (np.isfinite). - Your own
minimizeorscipy.stats.t.fitmay do the search. Fitting in percent and converting back is a good idea.
- Raise
t_var(r, level=0.99, value=1_000_000)fits the t and returns the one-day VaR in dollars, , with the fitted t's quantile at .- Raise
ValueErroriflevelis not strictly between 0 and 1.
- Raise
The tests use this case study's numbers, the same days in percent, normal data, and the bad inputs above.
See the tests
import numpy as np
def _returns():
rng = np.random.default_rng(50)
return 0.0004 + 0.01 * rng.standard_t(4, size=1000) / np.sqrt(2)
def test_shared_returns():
"""The shared daily returns fit a t with nu about 4.353, scale about 0.007386 and location about 0.000221"""
loc, scale, nu, loglik = fit_t(_returns())
expect_close(nu, 4.352924, what="nu", rel=1e-3)
expect_close(scale, 0.0073859132, what="scale", rel=1e-4)
expect_close(loc, 0.00022087671, what="loc", rel=0, abs_tol=2e-7)
def test_loglik_decimals():
"""Their log-likelihood at the peak, in decimals, is about 3248.4946"""
expect_close(fit_t(_returns())[3], 3248.4946329553, what="loglik", rel=0, abs_tol=1e-3)
def test_percent():
"""In percent the fit has location 0.0221, scale 0.7386 and log-likelihood -1356.6756"""
loc, scale, nu, loglik = fit_t(100 * _returns())
expect_close(loc, 0.0220876745, what="loc in percent", rel=0, abs_tol=2e-5)
expect_close(scale, 0.7385913, what="scale in percent", rel=1e-4)
expect_close(nu, 4.352924, what="nu in percent", rel=1e-3)
expect_close(loglik, -1356.6755530328, what="loglik in percent", rel=0, abs_tol=1e-3)
def test_units():
"""Changing units scales the location and scale by 100 and leaves nu alone"""
r = _returns()
a, b = fit_t(r), fit_t(100 * r)
expect_close(b[1], 100 * a[1], what="scale in percent / 100 * scale in decimals", rel=1e-4)
expect_close(b[2], a[2], what="nu in percent against nu in decimals", rel=1e-3)
expect_close(a[3] - b[3], 1000 * np.log(100), what="loglik difference, n ln 100", rel=0, abs_tol=2e-3)
def test_one_year():
"""The first 250 days alone give nu about 3.37"""
expect_close(fit_t(_returns()[:250])[2], 3.3666, what="nu from 250 days", rel=1e-2)
def test_normal_data():
"""On 1,000 seeded normal returns nu comes out above 30: no fat tails"""
z = np.random.default_rng(594).normal(0, 0.01, 1000)
nu = fit_t(z)[2]
assert nu > 30, f"expected nu above 30 for normal data, got {nu}"
def test_bad_returns():
"""Fewer than 20 returns, or a nan or inf among them, raises ValueError"""
r = _returns()
bad = [r[:19], np.append(r[:100], np.nan), np.append(r[:100], np.inf)]
for x in bad:
try:
fit_t(x)
except ValueError:
continue
raise AssertionError(f"expected ValueError for an input of {len(x)} values ending in {x[-1]}")
def test_var_99():
"""The 99% one-day VaR per $1,000,000 from the fitted t is about $25,912.55"""
expect_close(t_var(_returns()), 25912.548, what="t_var(r)", rel=1e-4)
def test_var_levels():
"""At 99.9% it is about $47,465 and at 95% about $15,051"""
r = _returns()
expect_close(t_var(r, 0.999), 47465.04, what="t_var(r, 0.999)", rel=1e-4)
expect_close(t_var(r, 0.95), 15050.61, what="t_var(r, 0.95)", rel=1e-4)
def test_var_value():
"""A $2,000,000 position has twice the VaR"""
r = _returns()
expect_close(t_var(r, 0.99, 2_000_000), 2 * 25912.548, what="t_var(r, 0.99, 2_000_000)", rel=1e-4)
def test_bad_level():
"""A level of 0, 1 or 1.5 raises ValueError"""
r = _returns()
for level in [0, 1, 1.5]:
try:
t_var(r, level)
except ValueError:
continue
raise AssertionError(f"expected ValueError for level={level}")
Summary card
- The normal's maximum likelihood fit is the mean and the standard deviation dividing by : for the shared returns and 1.0073%, with log-likelihood .
- The scaled t, with , has no formula for its fit: minimise over . Here , , , log-likelihood ;
t.fitagrees, returning (df, loc, scale). - Standard errors from the inverse Hessian of : 0.0274%, 0.0284% and 0.597. The interval for , 3.18 to 5.52, contains case study 5.4.6's 4; the likelihood-ratio interval, 3.38 to 5.81, leans right.
- The t beats the normal by 69.58 in log-likelihood; is far beyond 's 3.841.
- The fitted t's 99% VaR per $1,000,000 is about $25,900, against $26,212 from the sample and $23,221 from the normal. It expects 4.28 days beyond 4 volatilities in 1,000, against 6 seen and 0.063 for the normal.
- Log-likelihoods depend on units; from one year of data is unstable; independent days are an assumption.