SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 5.9.1 Likelihood and the maximum likelihood estimatorConcept · 35 min
      2. 5.9.2 Fisher information and standard errorsConcept · 35 min
      3. 5.9.3 Practice set: maximum likelihoodPractice set · 25 min
      4. 5.9.4 Case study: fitting a Student t to daily returnsCase study · 35 min

Level 5, Probability and statistics 5.9 Maximum likelihood Lesson 4 of 4

Case studyLesson 5.9.4 in Level 5, Probability and statistics. About 35 minutes.

Case study: fitting a Student t to daily returns

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: xt=100 rtx_t = 100\,r_t. 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 nn. Here they are μ^=−0.0061%\hat{\mu} = -0.0061\% and σ^=1.0073%\hat{\sigma} = 1.0073\%. 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 −n2(ln⁡(2πσ^2)+1)=−1426.25-\frac{n}{2}\big(\ln(2\pi\hat{\sigma}^2) + 1\big) = -1426.25. If the days really were normal, lesson 5.9.2 would give standard errors of σ^n=0.0319%\frac{\hat{\sigma}}{\sqrt{n}} = 0.0319\% for the mean and σ^2n=0.0225%\frac{\hat{\sigma}}{\sqrt{2n}} = 0.0225\% 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.

⌘/Ctrl + Enter runs the cell

The Student t model

The model takes a standard t with ν\nu degrees of freedom (lesson 5.4.4). It stretches it by a scale ss and shifts it to a location mm: X=m+sTX = m + sT. Its density is the t's, changed as lesson 5.6.1 changes a density under aX+baX + b:

f(x;m,s,ν)=cνs(1+z2ν)−(ν+1)/2z=x−ms\begin{gathered} f(x; m, s, \nu) = \frac{c_\nu}{s}\left(1 + \frac{z^2}{\nu}\right)^{-(\nu + 1)/2} \\[6pt] z = \frac{x - m}{s} \end{gathered}

Here cνc_\nu is the t's normalising constant. SciPy computes ln⁡f\ln f as t.logpdf(x, nu, m, s), so the log-likelihood is one sum.

Rule: The scaled t

X=m+sT,T∼tνVar⁡(X)=s2 νν−2(ν>2)\begin{gathered} X = m + sT, \quad T \sim t_\nu \\[4pt] \operatorname{Var}(X) = s^2\,\frac{\nu}{\nu - 2} \quad (\nu > 2) \end{gathered}

mm is in percent, ss in percent and ν\nu has no units. The scale is not the volatility: the volatility is sν/(ν−2)s\sqrt{\nu/(\nu - 2)}, larger than ss.

Case study 5.4.6 fixed ν=4\nu = 4 and chose s=σ^/2s = \hat{\sigma}/\sqrt{2}, 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 ln⁡s\ln s and ln⁡ν\ln\nu. Start from the normal fit and ν=5\nu = 5.

location mmscale ssν\nulog-likelihood
normal−0.0061%-0.0061\%1.0073% (volatility)none−1426.25-1426.25
t of case study 5.4.6−0.0061%-0.0061\%0.7123%4−1357.7-1357.7
t by maximum likelihood0.0221%0.7386%4.353−1356.68-1356.68

scipy.stats.t.fit finds the same peak to the digits shown. The fitted t's volatility is 0.7386×4.353/2.353=1.0046%0.7386 \times \sqrt{4.353/2.353} = 1.0046\%, 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 ν=4\nu = 4 from ν=4.35\nu = 4.35; the standard error below says how barely.

Why the location moved

The fitted location is +0.0221%+0.0221\%, while the sample mean is −0.0061%-0.0061\%. In the normal's log-likelihood a day zz scales from the centre costs z22\frac{z^2}{2}; in the t's it costs ν+12ln⁡(1+z2ν)\frac{\nu + 1}{2}\ln(1 + \frac{z^2}{\nu}), 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 0.04%0.04\%, though one sample proves nothing.

Try it

nll takes (m,ln⁡s,ln⁡ν)(m, \ln s, \ln\nu) 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.

⌘/Ctrl + Enter runs the cell

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 JJ. The standard errors are the square roots of the diagonal of J−1J^{-1}. Take the Hessian in the natural parameters (m,s,ν)(m, s, \nu), by lesson 4.3.1's central differences.

location mmscale ssν\nu
estimate0.0221%0.7386%4.353
standard error0.0274%0.0284%0.597
95% interval−0.032-0.032 to 0.0760.683 to 0.7943.18 to 5.52

The interval for ν\nu 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 σ^n=0.0319%\frac{\hat{\sigma}}{\sqrt{n}} = 0.0319\%. 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 ss and ν\nu have correlation 0.6570.657. A larger ν\nu means thinner tails and a smaller factor νν−2\frac{\nu}{\nu - 2}, so a larger ss 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 ν\nu 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 (m,s,ν)(m, s, \nu) at the peak.

The lines print JJ, the standard errors, the diagonal-only error for ν\nu, the correlation of ss and ν\nu, and the three intervals.

⌘/Ctrl + Enter runs the cell

A profile likelihood for the degrees of freedom

The interval for ν\nu rests on the quadratic copy of the log-likelihood. Lesson 5.9.2's likelihood-ratio interval needs no copy.

For each fixed ν\nu, maximise over mm and ss. The result is the profile log-likelihood ℓp(ν)\ell_p(\nu): how well the best t with that ν\nu explains the data.

ν\nu33.544.35356820
drop below the peak4.381.40.200.482.357.3529.63

The 95% likelihood-ratio interval keeps every ν\nu within 1.92 of the peak: 3.38 to 5.81. It is lopsided, reaching 1.45 above ν^\hat{\nu} and 0.97 below it. A change of ν\nu matters less to the shape when ν\nu 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 ν\nu. 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 ν\nu leans right.

Try it

profile(nu) maximises over mm and ln⁡s\ln s with ν\nu 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.

⌘/Ctrl + Enter runs the cell

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 ν→∞\nu \to \infty (lesson 5.4.4), so the question is whether letting ν\nu be finite helps by more than chance.

The likelihood-ratio test compares 2×69.58=139.152 \times 69.58 = 139.15 with the 0.95-quantile of χ12\chi^2_1, 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 4×10−324 \times 10^{-32}: the normal is rejected.

One caution: ν=∞\nu = \infty is the edge of the range, where the χ12\chi^2_1 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 e69.58e^{69.58}.

⌘/Ctrl + Enter runs the cell
Check yourself
The fitted t's location is +0.0221%+0.0221\%, but the sample mean, the normal's location, is −0.0061%-0.0061\%. Why do the two fits disagree?

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, $ thousands95%97.5%99%99.5%99.9%
sample (case study 5.3.5)15.520.426.236.352.3
normal16.519.623.225.730.7
fitted t15.119.525.931.547.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 kk volatilities2345
in the sample411364
normal expects45.52.70.0630.001
t of case study 5.4.6 expects47.413.24.812.11
fitted t expects4812.64.281.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, V(1−eq)V(1 - e^{q}), with qq 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, ∣x−μ^∣>kσ^|x - \hat{\mu}| > k\hat{\sigma}. The fitted t's chance of such a day is its probability outside [μ^−kσ^,μ^+kσ^][\hat{\mu} - k\hat{\sigma}, \hat{\mu} + k\hat{\sigma}].

⌘/Ctrl + Enter runs the cell
Check yourself
A colleague fits the same t to the same days, in decimals rather than percent, and reports a log-likelihood of 3248.49 instead of −1356.68-1356.68. What happened?

Try it

The first line is the log-likelihood in decimals, then 1000ln⁡1001000\ln 100 and −1356.68+1000ln⁡100-1356.68 + 1000\ln 100, and the fitted t's excess kurtosis. Then the four windows of 250 days, and a fit to normal data.

⌘/Ctrl + Enter runs the cell
Check yourself
Reading the standard error of ν^\hat{\nu} as 1/J331/\sqrt{J_{33}}, from the diagonal entry alone, gives 0.45. The inverse of the whole matrix gives 0.597. Which should you report?
Exercise

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.

  1. fit_t(r) returns (loc, scale, nu, loglik): the maximum likelihood estimates of the scaled t, in the units of r, and the log-likelihood at the peak.
    • Raise ValueError if r has fewer than 20 values or any value that is not finite (np.isfinite).
    • Your own minimize or scipy.stats.t.fit may do the search. Fitting in percent and converting back is a good idea.
  2. t_var(r, level=0.99, value=1_000_000) fits the t and returns the one-day VaR in dollars, V(1−eq)V(1 - e^{q}), with qq the fitted t's quantile at 1−level1 - \text{level}.
    • Raise ValueError if level is not strictly between 0 and 1.

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 nn: for the shared returns −0.0061%-0.0061\% and 1.0073%, with log-likelihood −1426.25-1426.25.
  • The scaled t, X=m+sTX = m + sT with T∼tνT \sim t_\nu, has no formula for its fit: minimise −ℓ-\ell over (m,ln⁡s,ln⁡ν)(m, \ln s, \ln\nu). Here m^=0.0221%\hat{m} = 0.0221\%, s^=0.7386%\hat{s} = 0.7386\%, ν^=4.353\hat{\nu} = 4.353, log-likelihood −1356.68-1356.68; t.fit agrees, returning (df, loc, scale).
  • Standard errors from the inverse Hessian of −ℓ-\ell: 0.0274%, 0.0284% and 0.597. The interval for ν\nu, 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; 2×69.58=139.152 \times 69.58 = 139.15 is far beyond χ12\chi^2_1'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; ν^\hat{\nu} from one year of data is unstable; independent days are an assumption.

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.