← Back to archive

1. The Illusion of Point Estimates

Every backtest report presents strategy parameters as single numbers: the Sharpe ratio is 1.2, the annualised return is 18%, the maximum drawdown is 14%. These point estimates create a false sense of precision. A Sharpe ratio of 1.2 estimated from 24 months of data could easily come from a strategy whose true Sharpe is anywhere between 0.3 and 2.1. Bayesian inference addresses this by producing a posterior distribution — a complete probability distribution over all plausible parameter values given the data.

2. The Bayesian Model

We model daily strategy returns as draws from a Student-t distribution with unknown mean μ, scale σ, and degrees of freedom ν. The Student-t likelihood accommodates fat tails without requiring a separate tail model. our priors are weakly informative: μ ~ Normal(0, 0.1) centred at zero expressing scepticism, σ ~ HalfCauchy(0.02), and ν ~ Exponential(1/30) + 2 to ensure finite variance.

import pymc as pm
import numpy as np

def bayesian_strategy_model(returns, n_samples=5000):
    """Bayesian estimation of strategy parameters via MCMC."""
    with pm.Model() as model:
        mu = pm.Normal('mu', mu=0, sigma=0.1/np.sqrt(252))
        sigma = pm.HalfCauchy('sigma', beta=0.02/np.sqrt(252))
        nu = pm.Exponential('nu_minus_2', lam=1/30) + 2
        obs = pm.StudentT('returns', nu=nu, mu=mu,
                          sigma=sigma, observed=returns)
        ann_ret = pm.Deterministic('ann_return', mu * 252)
        ann_vol = pm.Deterministic('ann_vol', sigma * np.sqrt(252))
        sharpe = pm.Deterministic('sharpe', ann_ret / ann_vol)
        trace = pm.sample(n_samples, chains=4, cores=4,
                         target_accept=0.95,
                         return_inferencedata=True)
    return trace

3. Credible Intervals vs. Confidence Intervals

A 90% Bayesian credible interval states: given the data and our prior beliefs, there is a 90% probability the true parameter lies within this interval. This is not what a frequentist confidence interval means. For practitioners making capital allocation decisions, the Bayesian interpretation is far more useful. The width depends on sample size, prior strength, and return distribution heaviness.

4. Application: Two-Year Track Record

ParameterPoint Est.Posterior Mean90% CI Lower90% CI Upper
Ann. Return18.0%16.8%7.2%26.9%
Ann. Volatility15.0%15.3%13.8%17.1%
Sharpe Ratio1.201.100.441.78
Degrees of Freedom4.04.33.16.2

Table 1: Point estimates vs. Bayesian posterior for observed Sharpe = 1.2 over 504 days.

The posterior mean Sharpe (1.10) is lower than the point estimate (1.20) due to Bayesian shrinkage toward the sceptical prior. We can compute threshold probabilities: P(Sharpe > 0) = 0.98, P(Sharpe > 0.5) = 0.89, P(Sharpe > 1.0) = 0.58. There is a 98% probability of positive risk-adjusted returns but only a 58% probability of exceeding the institutional threshold of 1.0.

5. Comparing Two Strategies

The framework extends to strategy comparison. Strategy A has observed Sharpe 1.2 over 504 days; Strategy B has 0.9 over 756 days. Despite A’s higher point estimate, the posterior probability that A is truly better is only 0.61. B’s longer track record provides tighter credible intervals, partially offsetting its lower point estimate. A naive comparison strongly favours A; the Bayesian comparison correctly recognises the substantial uncertainty in both estimates.

def bayesian_sharpe_comparison(returns_a, returns_b):
    """P(strategy A has higher true Sharpe than B)."""
    with pm.Model() as model:
        mu_a = pm.Normal('mu_a', mu=0, sigma=0.005)
        sigma_a = pm.HalfCauchy('sigma_a', beta=0.01)
        nu_a = pm.Exponential('nu_a_m2', lam=1/30) + 2
        pm.StudentT('obs_a', nu=nu_a, mu=mu_a,
                    sigma=sigma_a, observed=returns_a)
        mu_b = pm.Normal('mu_b', mu=0, sigma=0.005)
        sigma_b = pm.HalfCauchy('sigma_b', beta=0.01)
        nu_b = pm.Exponential('nu_b_m2', lam=1/30) + 2
        pm.StudentT('obs_b', nu=nu_b, mu=mu_b,
                    sigma=sigma_b, observed=returns_b)
        sr_a = pm.Deterministic('sr_a',
            (mu_a*252)/(sigma_a*np.sqrt(252)))
        sr_b = pm.Deterministic('sr_b',
            (mu_b*252)/(sigma_b*np.sqrt(252)))
        diff = pm.Deterministic('sr_diff', sr_a - sr_b)
        trace = pm.sample(5000, chains=4,
                         return_inferencedata=True)
    samples = trace.posterior['sr_diff'].values.flatten()
    return trace, np.mean(samples > 0)

6. Prior Sensitivity and MCMC Diagnostics

We test sceptical, neutral, and informative priors. For a 2-year track record, the prior shifts the posterior mean Sharpe by ±0.1–0.2. For records exceeding 5 years, the prior has negligible effect. Essential MCMC diagnostics include R-hat (below 1.01), effective sample size (at least 400 per chain), and trace plot inspection. The NUTS sampler handles most pathological cases, but divergent transitions must be addressed by reparameterisation or increasing target acceptance rate.

7. Conclusion

Point estimates convey false precision. Bayesian estimation via MCMC produces credible intervals that honestly represent uncertainty. For a two-year track record with observed Sharpe 1.2, the 90% credible interval spans [0.4, 1.8]. The computational tools are mature. The barrier to adoption is cultural: the industry’s preference for clean single numbers over the more honest picture that Bayesian uncertainty quantification provides.

References

  1. Kruschke, J.K. (2013). "Bayesian Estimation Supersedes the t-Test." J. Experimental Psychology: General, 142(2), 573–603.
  2. Gelman, A., Carlin, J.B., Stern, H.S. et al. (2013). Bayesian Data Analysis. 3rd ed., CRC Press.
  3. Lo, A.W. (2002). "The Statistics of Sharpe Ratios." Financial Analysts Journal, 58(4), 36–52.
  4. Salvatier, J., Wiecki, T. and Fonnesbeck, C. (2016). "Probabilistic Programming in Python Using PyMC3." PeerJ Computer Science, 2, e55.
  5. Wiecki, T., Campbell, A. and Lent, J. (2016). "All That Glitters Is Not Gold." Journal of Investing, 25(3), 69–80.