5.1 — A Bayesian Model of Risk and Reward: Priors, Likelihood, Posterior of the Sharpe Ratio

Chapter 5 · Rethinking Statistics with Bayesian Methods

Prof. Xuhu Wan

Section 5.1 · Chapter 5 · Learning Statistics with Python

A Bayesian Model of Risk and Reward: Priors, Likelihood, Posterior of the Sharpe Ratio

Rethinking Statistics with Bayesian Methods

Prof. Xuhu Wan

ISOM, HKUST Business School · 2026 Edition

A Bayesian Model of Risk and Reward

You will build the posterior distribution of GOOG’s Sharpe ratio three ways — on a grid, then with a sampler you write yourself — and discover why the classical estimate is too small. Predict every number before you run it.

Which object does a Bayesian actually compute?

\[\underbrace{p(\theta \mid X)}_{\text{posterior}} \;\propto\; \underbrace{p(X \mid \theta)}_{\text{likelihood}}\;\underbrace{p(\theta)}_{\text{prior}}\]

Classical statistics fixes \(\theta\) and asks how the data would vary. Bayes fixes the data and asks how \(\theta\) varies.

After seeing 1 258 daily returns, which object lets you say “P(Sharpe > 1) = 0.99”?

  • The likelihood \(p(X \mid \theta)\)
  • The prior \(p(\theta)\)
  • The posterior \(p(\theta \mid X)\)
  • The p-value of a t-test on the mean

Start with the classical number

GOOG daily returns, October 2015 to October 2020 — the notebook’s data. The annualised Sharpe ratio is \(\sqrt{252}\,\bar r / s\).

Mean daily return ≈ 0.00080, daily sd ≈ 0.01644. What does the classical Sharpe ratio print (3 dp)?

round(np.sqrt(252) * r.mean() / r.std(), 3)

0.771

Sharpe 0.771, excess kurtosis 6.72. A normal has kurtosis 0 — these returns have far fatter tails. The model must say so.

Why Student-t, not normal?

\[r_t \sim \text{StudentT}(\nu,\ \mu,\ \sigma)\]

Three parameters: location \(\mu\), scale \(\sigma\), and degrees of freedom \(\nu\) that set the tail weight.

Which choice of \(\nu\) produces the fattest tails?

  • \(\nu = 100\) — essentially normal
  • \(\nu = 3\)
  • \(\nu = 30\)
  • Tail weight does not depend on \(\nu\)

See the tails

On a log scale the normal’s tail is a parabola falling off a cliff; the t(3) tail is almost a straight line. A 4-sigma event: 6.3e-05 under the normal, 0.028 under t(3) — about 440 times more likely.

How to choose priors

The notebook’s choices, one per parameter:

Parameter Prior Why
\(\mu\) \(N(\bar r,\ s)\) centred on the sample mean, very wide (sd = one daily sd)
\(\sigma\) \(\text{Uniform}(s/1000,\ 1000\,s)\) positive, essentially flat
\(\nu\) \(2 + \text{Exponential}(\lambda = 1/29)\) mean 31, but mass near 2 keeps fat tails possible; \(\nu > 2\) guarantees a finite variance

What does the prior \(\nu = 2 + \text{Exp}(1/29)\) force?

  • Only that \(\nu > 2\); the data decide the rest
  • That \(\nu \approx 31\)
  • That the tails are normal
  • That \(\nu\) is an integer

Check it: expon.cdf(8, scale=29) = 0.241 — the prior gives only 24 % to \(\nu < 10\). It allows fat tails without insisting on them.

The model in PyMC (Colab only)

In Colab

PyMC is not available in the browser. This is the notebook’s model; the next slides reproduce its posterior with numpy and scipy.

with pm.Model() as sr_model:
    mean = pm.Normal("mean", mu=rmean, sigma=rstd)
    std  = pm.Uniform("std", lower=rstd/1000, upper=rstd*1000)
    df   = pm.Exponential("df", 1/29, initval=5) + 2.0
    returns = pm.StudentT("returns", nu=df, mu=mean, sigma=std,
                          observed=google["return"])
    pm.Deterministic("sharpe", np.sqrt(252) * mean / std)
    trace = pm.sample(tune=1500, draws=1000, chains=2, random_seed=8888)

Four lines of priors, one likelihood, one derived quantity. Everything else is the sampler — which we now build by hand.

Warm-up: a posterior on a grid

With one or two parameters you do not need a sampler at all: evaluate prior × likelihood on a grid and normalise. This is Bayes’ rule made literal.

One parameter: the mean of the S&P 500

Same five years, S&P 500. Treat \(\sigma\) as known (the sample sd). Prior \(\mu \sim N(0,\ 0.001^2)\). For a normal likelihood the posterior has a closed form — predict what the grid should reproduce.

Posterior precision = prior precision + \(n/\sigma^2\). With \(n = 1258\), \(\sigma = 0.01186\), prior sd 0.001, what is the posterior sd (6 dp)?

round(np.sqrt(1 / (1/0.001**2 + 1258/0.01186**2)), 6)

0.000317

What the grid told you

Grid 0.000497 0.000317, closed form 0.000497 0.000317 — identical to six decimals.

  • log_post - log_post.max() before exp is not cosmetic: 1 258 log-densities sum to about 3 800, and exp of that overflows.
  • The posterior mean 0.000497 sits between the prior mean 0 and the sample mean 0.00055: a precision-weighted average. The data’s precision (\(n/\sigma^2 \approx 8.9\) million) dwarfs the prior’s (1 million), so the data win — mostly.

Two parameters? A 20 × 20 grid is still only 400 points. Let’s do \(\mu\) and \(\sigma\) for GOOG under a Student-t likelihood with \(\nu = 4\).

Two parameters: μ and σ on a 20 × 20 grid

Posterior mean σ = 0.0108 — well below the sample sd 0.0164. The t-scale is not the standard deviation; fat tails inflate the sd. And Sharpe 2.035 with P(Sharpe > 1) = 0.972. Hold that thought.

Why not a grid for everything?

A 400-point grid per parameter and a 3-parameter model (\(\mu, \sigma, \nu\)). How many likelihood evaluations?

  • 1 200
  • 160 000
  • 4 800
  • 64 000 000

The curse of dimensionality is why we need Markov chain Monte Carlo: instead of visiting every point, wander through parameter space so that time spent in a region is proportional to its posterior probability.

Metropolis: the sampler behind PyMC

Propose a random step, accept it with probability \(\min(1, p^\star / p_t)\), otherwise stay. That is the whole algorithm — and it never needs the normalising constant.

Predict the acceptance rule

At \(\theta_t\) the posterior density is \(p_t\); you propose \(\theta^\star\) with density \(p^\star\).

\(p^\star = 0.3\,p_t\). What does Metropolis do?

  • Reject — the proposal is worse
  • Accept with probability 0.3
  • Accept — proposals are always accepted
  • Accept only if 0.3 is larger than the prior

In code: if log(u) < log_post(prop) - log_post(current). Work in logs; the difference of two logs is a ratio of densities, so the unknown normaliser \(p(X)\) cancels.

The log-posterior, vectorised

Sample \(\theta = (\mu, \log\sigma, \log(\nu - 2))\) so the chain can never leave the allowed region. The extra ls and le terms are the Jacobians of those transforms.

3438.4 at the sample sd, 3443.2 at half of it. The likelihood already prefers a scale smaller than the sample sd — the tails are being handled by \(\nu\), not by \(\sigma\).

Twelve lines of Metropolis

Acceptance 0.47 — inside the healthy 0.2–0.5 band. \(\nu \approx 2.7\): GOOG’s tails are heavier than a t(3) (the notebook’s PyMC run found 2.79). \(\sigma = 0.0097\), six-tenths of the sample sd.

Read the trace before you trust it

What does a healthy trace look like?

  • A flat, fuzzy band with no trend
  • A smooth upward drift
  • Long horizontal segments with occasional jumps
  • A single spike

The posterior of the Sharpe ratio

Posterior mean 2.38, 95 % credible interval [1.30, 3.59], P(Sharpe > 1) = 0.99. The notebook’s PyMC run gave 2.35 and [1.31, 3.42] — the same answer from a twelve-line sampler. The classical 0.77 lies outside the credible interval. “The Sharpe ratio is under-estimated by classical statistics.”

Why is the Bayesian Sharpe larger?

The posterior mean Sharpe is 2.38 against a classical 0.77. The main reason?

  • The prior on \(\mu\) pushes the mean up
  • MCMC is biased upward
  • The t-scale \(\sigma\) is smaller than the sample sd because the tails are absorbed by \(\nu\)
  • The classical formula forgets \(\sqrt{252}\)

Be honest about what σ means

The t-distribution’s standard deviation is \(\sigma\sqrt{\nu/(\nu-2)}\), which for \(\nu \approx 2.7\) is almost twice \(\sigma\). Re-computing Sharpe on that basis gives a posterior mean of about 1.18. The “under-estimation” is a statement about ordinary-day risk. Decide which risk you are pricing before you quote a Sharpe.

Credible interval vs confidence interval

Lo (2002): the classical standard error of an annualised Sharpe is \(\sqrt{252\,(1 + \tfrac12 \widehat{SR}_d^2)/n}\).

Which interval lets you say “there is a 95 % probability the true Sharpe lies in here”?

  • The confidence interval [-0.11, 1.65]
  • The credible interval [1.30, 3.59]
  • Both — they mean the same thing
  • Neither — probability statements about parameters are impossible

Note the classical interval includes zero: a t-test cannot reject “GOOG earned nothing”. The posterior says P(Sharpe > 0.5) = 0.999.

Your turn: AAPL and NVDA Sharpe posteriors

The notebook’s practice. The starter runs the sampler on AAPL (2 515 returns). Switch r_p to NVDA from nv (2023–2024, 500 returns) and compare posterior mean and classical Sharpe for each.

What you discovered

  • Posterior ∝ likelihood × prior; on a grid that is literally two arrays multiplied — and it matched the closed form to six decimals.
  • Grids die of dimensionality (64 million points for three parameters); Metropolis wanders instead, needing only ratios of densities.
  • Twelve lines of numpy gave acceptance 0.47, \(\nu \approx 2.7\) and a Sharpe posterior with mean 2.38 against a classical 0.77 — matching the notebook’s PyMC run.
  • The gap comes from what σ means: t-scale vs standard deviation. Say which you are quoting.
  • A credible interval is a probability statement about the parameter; a confidence interval is a statement about the procedure — and here the classical one could not even exclude zero.

Next: §5.2 — regression as a normal distribution, and how to check a model before fitting it.