SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 5.7.1 The Markov, Chebyshev and Jensen inequalitiesConcept · 35 min
      2. 5.7.2 The law of large numbersConcept · 30 min
      3. 5.7.3 The central limit theoremConcept · 35 min
      4. 5.7.4 Practice set: inequalities and limit theoremsPractice set · 25 min
      5. 5.7.5 Case study: the error of a Monte Carlo priceCase study · 35 min

Level 5, Probability and statistics 5.7 Inequalities and limit theorems Lesson 5 of 5

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

Case study: the error of a Monte Carlo price

The price is a mean

The inputs are those of case studies 2.4.6, 2.6.5 and 5.6.5. The stock costs S0=100S_0 = 100 dollars, the strike is K=100K = 100, the expiry T=0.5T = 0.5 years. The interest rate is r=0.03r = 0.03 and the volatility σ=0.2\sigma = 0.2, both annual decimals.

Case study 5.6.5 wrote the price at expiry as ST=S0em+vZS_T = S_0e^{m + vZ}, with ZZ standard normal, m=(r−σ22)Tm = (r - \frac{\sigma^2}{2})T and v=σTv = \sigma\sqrt{T}. Case study 2.6.5 priced the call as an integral over zz weighted by φ(z)\varphi(z). That integral is an expected value: the mean of the discounted payoff

Y=e−rTmax⁡(ST−K, 0)Y = e^{-rT}\max(S_T - K,\ 0)

So the call's price is c=E[Y]c = \mathbb{E}[Y]. Draw NN independent copies of ZZ, compute each YiY_i, and average. By the law of large numbers (lesson 5.7.2) the average settles on cc.

Rule: Monte Carlo price

c^N=1N∑i=1NYiYi=e−rTmax⁡ ⁣(S0em+vZi−K, 0)\begin{gathered} \hat{c}_N = \frac{1}{N}\sum_{i=1}^{N} Y_i \\[4pt] Y_i = e^{-rT}\max\!\left(S_0e^{m + vZ_i} - K,\ 0\right) \end{gathered}

Z1,…,ZNZ_1, \ldots, Z_N are independent standard normal draws. The method needs no formula for the price, only a way to draw STS_T. That is why desks use it for payoffs that have no formula.

Try it

The first line is the Black–Scholes formula of case study 2.4.6. The second is case study 2.6.5's integral by quad, from the corner z∗=−d2z^* = -d_2 where the call starts to pay. The upper limit 12 stands in for ∞\infty: the weight φ\varphi is negligible there.

Then 10,000 seeded draws and their average, the $6.2713 of the hook.

⌘/Ctrl + Enter runs the cell

The payoff is lopsided

Before measuring the error of the average, look at what is being averaged. The call pays nothing when ST≤KS_T \le K, which has probability 1−N(d2)≈0.48591 - N(d_2) \approx 0.4859. When it pays, the payoff can be large.

The same integrals give its spread. E[Y2]≈130.2379\mathbb{E}[Y^2] \approx 130.2379, so the standard deviation of one discounted payoff is

σY=130.2379−6.37102≈9.4683\begin{aligned} \sigma_Y &= \sqrt{130.2379 - 6.3710^2} \\ &\approx 9.4683 \end{aligned}

That is about 1.49 times the price. The skewness (lesson 5.3.3) is 1.83: a long right tail.

The seeded sample looks the same. Of its 10,000 payoffs, 0.4903 are zero, the median is $0.3174, the largest is $66.43 and the sample skewness is 1.78. One payoff is nothing like normal; the central limit theorem (lesson 5.7.3) is about their mean.

Try it

quad integrates YY, Y2Y^2 and Y3Y^3 against φ\varphi. The first line prints P(Y=0)P(Y = 0), E[Y2]\mathbb{E}[Y^2], σY\sigma_Y, its ratio to the price, and the skewness from the three moments.

The second line describes the seeded sample: the share of zero payoffs, the median, the largest payoff and the skewness.

⌘/Ctrl + Enter runs the cell

The standard error and a 95% interval

The average of NN independent payoffs has standard deviation σYN\frac{\sigma_Y}{\sqrt{N}} (lesson 5.7.2): 9.4683100≈0.0947\frac{9.4683}{100} \approx 0.0947 for N=10,000N = 10{,}000. On a real problem σY\sigma_Y is unknown, since a formula for it would usually give the price too.

So estimate it from the same draws, with the sample standard deviation ss, dividing by N−1N - 1 (lesson 5.5.2). By the central limit theorem (lesson 5.7.3), c^N\hat{c}_N is close to normal. So it lies within 1.96 standard errors of cc about 95% of the time.

Rule: Standard error and 95% interval

SE=sNc^N±1.96 SE\begin{gathered} \text{SE} = \frac{s}{\sqrt{N}} \\[4pt] \hat{c}_N \pm 1.96\,\text{SE} \end{gathered}

ss is the sample standard deviation of the NN discounted payoffs. For a share, s2s^2 is close to p^(1−p^)\hat{p}(1 - \hat{p}), and this is the standard error of case study 4.7.5.

For the seeded run, s≈9.3272s \approx 9.3272, so SE≈0.0933\text{SE} \approx 0.0933. The interval runs from 6.0885 to 6.4541 and covers 6.3710. The miss, −0.0998-0.0998, is 1.07 standard errors below the price: ordinary, as the hook said.

Try it

Y.std(ddof=1) divides by N−1N - 1. The first line prints the estimate, ss and the standard error; the second the 95% interval.

The last line is the miss, the miss in standard errors, and the standard error from the exact σY\sigma_Y.

⌘/Ctrl + Enter runs the cell

What one more digit costs

Carry the same seeded run on to a million draws. The first 10,000 are the ones above.

draws NN1001,00010,000100,0001,000,000
estimate5.10666.50556.27136.36856.3765
standard error0.83480.29240.09330.02990.0095

Every interval covers 6.3710. Each tenfold step divides the standard error by about 10≈3.16\sqrt{10} \approx 3.16 (lesson 5.7.3); case study 4.7.5 saw the same slope for a probability.

To pin the price to the cent with 95% confidence, a half-width of 0.01, needs N=(1.96×9.46830.01)2N = \left(\frac{1.96 \times 9.4683}{0.01}\right)^2: about 3.44 million draws. A tenth of a cent needs about 344 million. Each extra digit costs a hundred times the draws.

One seeded run's estimate and its 95% interval as the draws grow. The readouts give the estimate and the half-width 1.96 SE1.96\,\text{SE}; the band holds 6.3710 throughout and narrows like 1N\frac{1}{\sqrt{N}}.

Try it

One array of a million payoffs; Y[:N] keeps the first N. Each line prints NN, the estimate, the standard error, and whether the 95% interval covers the price.

The last loop gives the draws for half-widths of 0.01 and 0.001.

⌘/Ctrl + Enter runs the cell

When the 95% can be trusted

One run covering the price proves little. Repeat the whole simulation many times and count how often the interval covers 6.3710.

draws NN201002,000
runs10,00010,0001,000
interval covers 6.37100.90340.94020.946
interval wholly below 6.37100.08730.04460.025
interval wholly above 6.37100.00930.01520.029

With 2,000 draws the coverage is close to 95%, and the misses fall about evenly on each side. With 20 draws it is only 90%, and most misses are low.

The lopsided payoff is the reason. The mean of 20 payoffs still has skewness about 1.8320≈0.41\frac{1.83}{\sqrt{20}} \approx 0.41 (lesson 5.7.3).

A small sample often misses the rare large payoffs. Then both its average and its ss come out too small: the interval sits low and is too narrow.

The interval needs enough draws for the theorem to apply, and more when the payoff is lopsided.

Try it

For each NN, every row of Z is one complete simulation. The columns are NN, the share of intervals covering the price, the share lying wholly below it and the share wholly above.

The last line is the skewness of a mean of 20 payoffs.

⌘/Ctrl + Enter runs the cell
Check yourself
A Monte Carlo price from 40,000 draws has standard error 0.047. How many draws give a standard error of about 0.0047?

Antithetic draws

The standard error is σYN\frac{\sigma_Y}{\sqrt{N}}, so there are two ways to shrink it: more draws, or a smaller σY\sigma_Y. Methods that take the second way are called variance reduction. The simplest uses antithetic draws.

If ZZ is standard normal, so is −Z-Z. Draw N2\frac{N}{2} values of ZZ and use each twice, as ZZ and as −Z-Z.

A high ZZ gives a large payoff and its mirror a small one, so the two payoffs of a pair are negatively correlated. Their average varies less than two independent payoffs would.

With 10,000 payoffs in 5,000 pairs, one seeded run gives $6.3704 with standard error 0.0694. Plain draws give about 0.0947 for the same 10,000 payoffs. The correlation within a pair was −0.4569-0.4569; the variance of a pair's average is σY2(1+ρ)2\frac{\sigma_Y^2(1 + \rho)}{2}, with ρ\rho that correlation.

Two traps come with the method.

  • The standard error must come from the pairs. The 10,000 payoffs are not independent, so s10,000\frac{s}{\sqrt{10{,}000}} over all of them, 0.0942 here, is wrong. Use the 5,000 pair averages: their sample standard deviation over 5,000\sqrt{5{,}000}.
  • It helps only when the payoff moves one way with ZZ. A straddle pays ∣ST−K∣|S_T - K|, high at both ends. Its two payoffs in a pair have correlation +0.8915+0.8915: near duplicates. An estimate from N2\frac{N}{2} pairs then has 1+ρ≈1.891 + \rho \approx 1.89 times the variance of one from NN plain draws, so antithetic draws make it worse.

Try it

call and straddle compute discounted payoffs for an array of draws. The first line is the antithetic estimate, its standard error from the 5,000 pair averages, and the correlation within a pair.

The second line is the wrong standard error, treating all 10,000 payoffs as independent. The last is the straddle's correlation within a pair and 1+ρ1 + \rho.

⌘/Ctrl + Enter runs the cell
Check yourself
A colleague prices a call with 5,000 antithetic pairs and reports the standard error of all 10,000 payoffs, s10,000\frac{s}{\sqrt{10{,}000}}. What is wrong?

On the desk

Try it

mc_call returns the estimate, the standard error and the share of draws that pay; drift replaces rr in the exponent. bs_call is the formula.

The three lines are the strike of 150, then the drift of 8% beside its exact value, then σ=0\sigma = 0. There the standard error is 0 up to rounding.

⌘/Ctrl + Enter runs the cell
Exercise

A Monte Carlo price with its error

Write mc_call(S0, K, T, r, sigma, n, seed, antithetic=False). It returns the pair (price, se) for a European call, as in this case study.

  1. Draw with np.random.default_rng(seed).standard_normal(...), and set ST=S0em+vZS_T = S_0e^{m + vZ} with m=(r−σ22)Tm = (r - \frac{\sigma^2}{2})T and v=σTv = \sigma\sqrt{T}.
  2. Without antithetic draws: n draws, price the mean of the discounted payoffs, se their sample standard deviation (dividing by n−1n - 1) over n\sqrt{n}.
  3. With antithetic=True: draw n // 2 values zz and average the payoffs of zz and −z-z in each pair. Compute price and se from the n // 2 pair averages.
  4. Raise ValueError if S0≤0S_0 \le 0, if KK, σ\sigma or TT is negative, if n<2n < 2, or if antithetic draws get an odd nn or fewer than 4.

The tests check this case study's seeded run (6.2713, standard error 0.0933) and its antithetic run (6.3704 and 0.0694). They also check the 1n\frac{1}{\sqrt{n}} scale, σ=0\sigma = 0, T=0T = 0 and a deep out-of-the-money strike.

See the tests
import math

BS = 6.371028  # Black–Scholes price of the shared call (case study 2.4.6)


def test_seeded_run():
    """Seed 575 with 10,000 draws gives 6.2713 with standard error 0.0933, as in the case study"""
    price, se = mc_call(100, 100, 0.5, 0.03, 0.2, 10_000, 575)
    expect_close(price, 6.271264328239705, what="price", rel=1e-9)
    expect_close(se, 0.09327231109608519, what="standard error (sample sd with n - 1, over sqrt(n))", rel=1e-9)


def test_closes_on_black_scholes():
    """With 400,000 draws the estimate is within 3 standard errors of 6.3710"""
    price, se = mc_call(100, 100, 0.5, 0.03, 0.2, 400_000, 11)
    assert abs(price - BS) <= 3 * se, f"expected a price within 3 standard errors ({3 * se:.4f}) of {BS}, got {price:.4f}"


def test_standard_error_size():
    """The standard error is close to 9.4683 / sqrt(n), the payoff's standard deviation over sqrt(n)"""
    _, se = mc_call(100, 100, 0.5, 0.03, 0.2, 400_000, 12)
    expect_close(se, 9.4683 / math.sqrt(400_000), what="standard error with 400,000 draws", rel=0.02)


def test_four_times_the_draws():
    """Four times the draws halve the standard error"""
    small = mc_call(100, 100, 0.5, 0.03, 0.2, 10_000, 3)[1]
    large = mc_call(100, 100, 0.5, 0.03, 0.2, 40_000, 3)[1]
    expect_close(large / small, 0.5, what="ratio of standard errors for 40,000 and 10,000 draws", rel=0.03)


def test_antithetic():
    """Antithetic pairs, seed 5753 and 10,000 payoffs: 6.3704 with standard error 0.0694, from the 5,000 pair averages"""
    price, se = mc_call(100, 100, 0.5, 0.03, 0.2, 10_000, 5753, antithetic=True)
    expect_close(price, 6.370379263137437, what="antithetic price", rel=1e-9)
    expect_close(se, 0.0694189616918534, what="antithetic standard error", rel=1e-9)
    plain = mc_call(100, 100, 0.5, 0.03, 0.2, 10_000, 5753)[1]
    assert se < plain, f"expected the antithetic standard error {se:.4f} to be below the plain one {plain:.4f}"


def test_no_volatility():
    """With sigma = 0 every payoff is 100 - 100e^(-0.015) = 1.4888, and the standard error is 0"""
    price, se = mc_call(100, 100, 0.5, 0.03, 0.0, 1_000, 1)
    expect_close(price, 100 - 100 * math.exp(-0.015), what="price with sigma = 0", rel=1e-9)
    assert abs(se) < 1e-9, f"expected a standard error of 0, got {se}"


def test_at_expiry():
    """With T = 0 the call is worth max(S0 - K, 0): 5 for S0 = 105 and K = 100"""
    price, se = mc_call(105, 100, 0, 0.03, 0.2, 1_000, 1)
    expect_close(price, 5.0, what="price at expiry", rel=1e-12)
    assert abs(se) < 1e-9, f"expected a standard error of 0, got {se}"


def test_deep_out_of_the_money():
    """Strike 150: the standard error is more than 30% of the price with 10,000 draws"""
    price, se = mc_call(100, 150, 0.5, 0.03, 0.2, 10_000, 5754)
    assert price > 0, f"expected a positive price, got {price}"
    assert se / price > 0.3, f"expected a relative error above 0.3, got {se / price:.3f}"


def test_bad_inputs():
    """S0 <= 0, a negative sigma or T, n below 2, or an odd n with antithetic draws raises ValueError"""
    cases = [
        ("S0 = 0", lambda: mc_call(0, 100, 0.5, 0.03, 0.2, 100, 1)),
        ("sigma = -0.2", lambda: mc_call(100, 100, 0.5, 0.03, -0.2, 100, 1)),
        ("T = -1", lambda: mc_call(100, 100, -1, 0.03, 0.2, 100, 1)),
        ("n = 1", lambda: mc_call(100, 100, 0.5, 0.03, 0.2, 1, 1)),
        ("odd n with antithetic", lambda: mc_call(100, 100, 0.5, 0.03, 0.2, 101, 1, antithetic=True)),
    ]
    for name, call in cases:
        try:
            call()
        except ValueError:
            continue
        raise AssertionError(f"expected ValueError for {name}")

Summary card

  • The call's price is a mean, c=E[e−rTmax⁡(ST−K,0)]c = \mathbb{E}[e^{-rT}\max(S_T - K, 0)]; the average c^N\hat{c}_N of NN simulated discounted payoffs settles on it (lesson 5.7.2).
  • Its standard error is sN\frac{s}{\sqrt{N}}, with ss the sample standard deviation of the payoffs; here σY≈9.4683\sigma_Y \approx 9.4683, so 10,000 draws give about 0.095.
  • c^N±1.96 SE\hat{c}_N \pm 1.96\,\text{SE} is a 95% interval (lesson 5.7.3) once NN is large enough for the lopsided payoff. Coverage was 90% at 20 draws and close to 95% at 2,000.
  • One more digit costs 100 times the draws: the cent needs about 3.44 million.
  • Antithetic pairs ZZ, −Z-Z cut the standard error when the payoff moves one way with ZZ: 0.0694 against 0.0947 for 10,000 payoffs. They hurt a straddle. Take the standard error from the pair averages.
  • Report the error, the draws and the seed; simulate with rr; expect large relative errors deep out of the money.

Free further reading

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