Skip to content

A European option's value, live

The Black-Scholes value of a European option on a stock paying no dividend, a call's c = S N(d₁) − K e^(−rT) N(d₂) and a put's p = K e^(−rT) N(−d₂) − S N(−d₁), in the money unit of S and K, N the library's normal distribution (lusine.finance.option_value). The volatility is the spread over a period the game states, a year here. Below, a call and a put struck at 50 EUR with a year left, at 5% and a volatility of 20%, are valued on your GPU as you read at spots from 10 EUR to 150 EUR, a euro a step; thirty-two options are held to Black-Scholes in float64; and sixty-four options are drawn each step across moneyness, rates from −2% to 15%, volatilities from 5% to 90% and lives from a week to five years, each valued as a call and as a put.

This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.

What it shows

  • The strip above the controls is green while every check holds, and turns red for good once one fails.
  • The sweep: the first chart draws the call's value and the put's against the spot, a point a step; the second, each value above its bound, the call's max(S − K e^(−rT), 0) and the put's max(K e^(−rT) − S, 0): never below it, highest at the discounted strike, meeting it far from the strike either way, and the two one over the other, as put-call parity has it. Below them, the spot and the two values.
  • The checks: Hull's examples, Example 15.6 (a stock at 42, a strike of 40, 10%, 20%, six months) a call of 4.76 and a put of 0.81, and chapter 19's running example (49, 50, 5%, 20%, twenty weeks) a call of 2.40, each to half a unit in the last digit printed (Hull, Options, Futures, and Other Derivatives, 9th ed.); Black-Scholes in float64, the thirty-two options' values each within 2 × 10⁻⁶ of the spot of Black-Scholes in float64 with scipy's exact normal distribution, at the parameters as binary32 holds them; put-call parity, c − p = S − K e^(−rT) for every option drawn, within 16 binary32 epsilons (2⁻²³) of the spot; in yen, the kernel bound to a spot of 4200 JPY, an integer, and a strike of 4000 JPY giving Example 15.6's 476 and 81 yen to half a yen; above the bounds, both values at or above their bounds across the sweep, to 10⁻⁴ EUR, and at its ends a call far out of the money nothing and far in it its bound, a put the other way round. Each is held at every step.

Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The seed, the sweep's option and its spots, and Hull's examples are the program's settings, which the host may write; this page offers no control to change them yet.

The program

The options, the draws, the sweep and the checks, the panels the page draws, and the one procedure, main, run each step: the spot a euro on, the values at it, at the thirty-two options and at the draws, and the checks.

// The option value's live page: a call and a put swept across the spot beside the bounds they lie above; thirty-two
// options held to Black-Scholes in float64; sixty-four drawn each step held to put-call parity; Hull's examples; and
// the kernel bound to yen, with the page's checks computed here on the GPU and shown by the panels the program
// declares.

namespace option_value;

lusine.version version = { { number = 1 } };

// The reader's settings: the seed the options are drawn from; the sweep's option, its strike, rate, volatility over a
// year and life; and the spot the sweep starts at and its step.
host struct {
  integer seed = 391; float<EUR> strike = 50.0 EUR; float<year^-1> rate = 0.05 year^-1; float volatility = 0.2;
  float<year> years = 1.0 year; float<year> period = 1.0 year; float<EUR> first = 10.0 EUR;
  float<EUR> stride = 1.0 EUR;
} settings;

// Hull's examples: Example 15.6, a stock at 42 EUR, a strike of 40 EUR, 10% a year, a volatility of 20% a year and six
// months left; and chapter 19's running example, 49 EUR, 50 EUR, 5%, 20% and twenty weeks.
host struct {
  float<EUR> spot = 42.0 EUR; float<EUR> strike = 40.0 EUR; float<year^-1> rate = 0.1 year^-1; float volatility = 0.2;
  float<year> years = 0.5 year;
} example;
host struct {
  float<EUR> spot = 49.0 EUR; float<EUR> strike = 50.0 EUR; float<year^-1> rate = 0.05 year^-1; float volatility = 0.2;
  float<year> years = 0.38461538461538464 year;
} running;

// The kernel bound to yen: Example 15.6 at a hundred yen to the euro, the spot an integer.
host struct { integer<JPY> spot = 4200 JPY; float<JPY> strike = 4000.0 JPY; } yen;

// Thirty-two options, calls and puts, each with its value by Black-Scholes in float64 with scipy's exact normal
// distribution, at the parameters as binary32 holds them; the kernel's value, and whether it is within 2e-6 of the
// spot.
struct option {
  bool call; float<EUR> spot; float<EUR> strike; float<year^-1> rate; float volatility; float<year> years;
  float<EUR> reference; float<EUR> value; bool holds;
};
host table<option> options(capacity = 32) = {
  { call = true, spot = 20.886307268226673 EUR, strike = 58.665533887237984 EUR,
    rate = 0.013447202845124347 year^-1, volatility = 0.15902594726357758, years = 4.38507015721194 year,
    reference = 0.005571692493690578 EUR },
  { call = false, spot = 188.59482662705486 EUR, strike = 135.69979791601557 EUR,
    rate = 0.05057192816027696 year^-1, volatility = 0.3115418699091693, years = 4.222032830755394 year,
    reference = 9.85535647796005 EUR },
  { call = true, spot = 118.91243378167061 EUR, strike = 185.2483042029888 EUR,
    rate = 0.09011600873524846 year^-1, volatility = 0.31585264386633055, years = 3.721298802897474 year,
    reference = 23.764120763200843 EUR },
  { call = true, spot = 163.8803382421281 EUR, strike = 27.370130355000377 EUR,
    rate = 0.08699557563034188 year^-1, volatility = 0.3805289170746753, years = 2.9718848042202395 year,
    reference = 142.7548209457505 EUR },
  { call = false, spot = 156.01246541016698 EUR, strike = 98.66825482181102 EUR,
    rate = 0.14785047611735416 year^-1, volatility = 0.7774526364639268, years = 2.9756117175286025 year,
    reference = 17.742401584869214 EUR },
  { call = true, spot = 120.21559443564263 EUR, strike = 39.4609317943744 EUR,
    rate = 0.031629090453178174 year^-1, volatility = 0.41657887517742265, years = 2.621120091497525 year,
    reference = 84.5434860391814 EUR },
  { call = true, spot = 125.27283792580695 EUR, strike = 113.42753373611995 EUR,
    rate = 0.13960486210071893 year^-1, volatility = 0.32822630365082595, years = 0.663718099301309 year,
    reference = 26.078961739406495 EUR },
  { call = true, spot = 111.5146720923226 EUR, strike = 91.70554427769596 EUR,
    rate = 0.09035406599957747 year^-1, volatility = 0.6424737027251702, years = 0.9491809736885719 year,
    reference = 40.00456609628675 EUR },
  { call = true, spot = 104.7904253041208 EUR, strike = 54.552235260291766 EUR,
    rate = 0.035530099141647786 year^-1, volatility = 0.5662054014919639, years = 4.101686172995604 year,
    reference = 68.20684615334216 EUR },
  { call = true, spot = 131.9704638511787 EUR, strike = 199.51590533067628 EUR,
    rate = 0.12150206984240143 year^-1, volatility = 0.07353334528136175, years = 1.013456937268504 year,
    reference = 0.00011467754461548692 EUR },
  { call = true, spot = 189.31412648919076 EUR, strike = 101.79493838011325 EUR,
    rate = 0.1486055774391051 year^-1, volatility = 0.17001756302296653, years = 3.6964262270876262 year,
    reference = 130.54445268816983 EUR },
  { call = false, spot = 184.9460138697643 EUR, strike = 154.73367705909305 EUR,
    rate = 0.06516181953678835 year^-1, volatility = 0.8088997581932528, years = 4.43441302148932 year,
    reference = 59.17841897082681 EUR },
  { call = true, spot = 184.149544077138 EUR, strike = 138.86885284372516 EUR,
    rate = 0.08453121905364037 year^-1, volatility = 0.8815777025835502, years = 4.812131996853199 year,
    reference = 142.01599344069513 EUR },
  { call = false, spot = 179.92226669131634 EUR, strike = 66.09962335996232 EUR,
    rate = 0.09058104655070943 year^-1, volatility = 0.4365392531203102, years = 0.6805432126265991 year,
    reference = 0.016997859814278038 EUR },
  { call = true, spot = 58.98985403872521 EUR, strike = 69.53690348359211 EUR,
    rate = 0.09882627092796568 year^-1, volatility = 0.8984296966945114, years = 4.194961386027854 year,
    reference = 40.463070291943716 EUR },
  { call = false, spot = 70.83381725423173 EUR, strike = 196.1692404299365 EUR,
    rate = 0.048636007021675504 year^-1, volatility = 0.619002303682292, years = 0.44163010411418885 year,
    reference = 121.28605180612945 EUR },
  { call = true, spot = 182.0562557898536 EUR, strike = 173.70967347304358 EUR,
    rate = 0.04764858389327849 year^-1, volatility = 0.6800378619360005, years = 4.203906336381006 year,
    reference = 104.41716879569906 EUR },
  { call = false, spot = 66.23051665247758 EUR, strike = 170.51126513679412 EUR,
    rate = 0.0634771790745104 year^-1, volatility = 0.7740285718864518, years = 2.4297303435717232 year,
    reference = 96.43858515604768 EUR },
  { call = true, spot = 61.040122087469854 EUR, strike = 25.81738810852937 EUR,
    rate = -0.01776675074159802 year^-1, volatility = 0.6865046104255538, years = 4.1593741745940465 year,
    reference = 42.35183699958962 EUR },
  { call = true, spot = 183.22855401336517 EUR, strike = 180.44889574953785 EUR,
    rate = 0.022967575251798763 year^-1, volatility = 0.44718485541057645, years = 4.795045054343254 year,
    reference = 76.05069129694635 EUR },
  { call = false, spot = 55.846083764705995 EUR, strike = 53.78166280056927 EUR,
    rate = 0.12564516571602508 year^-1, volatility = 0.47603203171217895, years = 0.9746002449187416 year,
    reference = 6.016384322280343 EUR },
  { call = false, spot = 49.43203182526905 EUR, strike = 181.98461219516167 EUR,
    rate = 0.12752664626939325 year^-1, volatility = 0.10648053312598907, years = 0.6180760422632282 year,
    reference = 118.75909663265438 EUR },
  { call = true, spot = 155.62101568917203 EUR, strike = 162.04319920333035 EUR,
    rate = 0.10691574785743298 year^-1, volatility = 0.60801310807133, years = 2.030221913104255 year,
    reference = 61.45486688552339 EUR },
  { call = false, spot = 143.56758434849291 EUR, strike = 148.75367003537065 EUR,
    rate = 0.12858898784657127 year^-1, volatility = 0.7002982559148344, years = 4.144197661866689 year,
    reference = 35.35212408415502 EUR },
  { call = false, spot = 197.30127196208014 EUR, strike = 87.76729450260797 EUR,
    rate = 0.13782776344187664 year^-1, volatility = 0.284284922323631, years = 4.1099414619067645 year,
    reference = 0.1555467590000994 EUR },
  { call = true, spot = 176.93386638351492 EUR, strike = 24.762009772194894 EUR,
    rate = 0.002562164252424219 year^-1, volatility = 0.5123741136193225, years = 0.8223266264332635 year,
    reference = 152.2240479231995 EUR },
  { call = true, spot = 22.383447260917606 EUR, strike = 87.65377119357558 EUR,
    rate = 0.12941660432027086 year^-1, volatility = 0.8920939592482549, years = 4.887267819355272 year,
    reference = 12.287773597175024 EUR },
  { call = false, spot = 198.92285038640267 EUR, strike = 28.77192047369769 EUR,
    rate = 0.07350521936785097 year^-1, volatility = 0.259816172475044, years = 4.990816046864923 year,
    reference = 0.00029573259101349676 EUR },
  { call = true, spot = 29.333652993672846 EUR, strike = 155.9661717776625 EUR,
    rate = 0.13608761873657335 year^-1, volatility = 0.7921456835739259, years = 0.5821716906310608 year,
    reference = 0.04973696612897763 EUR },
  { call = true, spot = 96.2014567940601 EUR, strike = 187.00927996286674 EUR,
    rate = 0.028803522459524823 year^-1, volatility = 0.3952282877503409, years = 4.334615968395207 year,
    reference = 15.221810190227234 EUR },
  { call = false, spot = 84.30148792212937 EUR, strike = 34.445458985634986 EUR,
    rate = 0.07128430993582072 year^-1, volatility = 0.24547475493628396, years = 4.191866317697686 year,
    reference = 0.06657010267225516 EUR },
  { call = true, spot = 159.96599103543278 EUR, strike = 135.11847347833998 EUR,
    rate = 0.09059069229451537 year^-1, volatility = 0.11230768482870263, years = 0.8190242915046948 year,
    reference = 34.550096322267066 EUR },
};

// Sixty-four options drawn each step: a call's value and a put's, and whether their difference is the spot less the
// discounted strike, within 16 binary32 epsilons (2^-23) of the spot.
struct drawn {
  integer index; bool call; float<EUR> spot; float<EUR> strike; float<year^-1> rate; float volatility;
  float<year> years; float<EUR> called; float<EUR> put; bool holds;
};
table<drawn> draws(capacity = 64) = {
  { index = 0 }, { index = 1 }, { index = 2 }, { index = 3 }, { index = 4 }, { index = 5 }, { index = 6 },
  { index = 7 }, { index = 8 }, { index = 9 }, { index = 10 }, { index = 11 }, { index = 12 }, { index = 13 },
  { index = 14 }, { index = 15 }, { index = 16 }, { index = 17 }, { index = 18 }, { index = 19 }, { index = 20 },
  { index = 21 }, { index = 22 }, { index = 23 }, { index = 24 }, { index = 25 }, { index = 26 }, { index = 27 },
  { index = 28 }, { index = 29 }, { index = 30 }, { index = 31 }, { index = 32 }, { index = 33 }, { index = 34 },
  { index = 35 }, { index = 36 }, { index = 37 }, { index = 38 }, { index = 39 }, { index = 40 }, { index = 41 },
  { index = 42 }, { index = 43 }, { index = 44 }, { index = 45 }, { index = 46 }, { index = 47 }, { index = 48 },
  { index = 49 }, { index = 50 }, { index = 51 }, { index = 52 }, { index = 53 }, { index = 54 }, { index = 55 },
  { index = 56 }, { index = 57 }, { index = 58 }, { index = 59 }, { index = 60 }, { index = 61 }, { index = 62 },
  { index = 63 },
};

// The sweep: the step, from 0 to 140 and round again; the spot, from 10 EUR to 150 EUR; the call's value and the put's;
// the strike discounted over the life; and the bounds, the call's max(S - K e^(-rT), 0) and the put's max(K e^(-rT) -
// S, 0); and, for the chart, each value above its bound.
struct {
  integer step = -1; float<EUR> spot; float<EUR> called; float<EUR> put; float<EUR> discounted; float<EUR> call_bound;
  float<EUR> put_bound; float<EUR> call_above; float<EUR> put_above;
} sweep;

// The checks: Hull's examples, Example 15.6's call 4.76 EUR and put 0.81 EUR and chapter 19's call 2.40 EUR, each to
// half a unit in the last digit printed; the thirty-two options each within 2e-6 of the spot of the float64 value; c -
// p = S - K e^(-rT) for every option drawn, within 16 epsilons of the spot; Example 15.6 in yen, 476 JPY and 81 JPY to
// half a yen; and across the sweep both values above their bounds, to 1e-4 EUR, a call nothing far below the strike and
// a put nothing far above it, each else its bound.
struct { bool hull = true; bool reference = true; bool parity = true; bool yen = true; bool bounds = true; } checks;

// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);

vector<float, 4> lamp_colour(bool holds)
{
  return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}

float eps() { return 1.0 / 8388608.0; }

integer failing(table<bool> holds) { return count(holds) where !holds; }

float uniform_of(integer seed, integer step, bits key, integer index)
{
  return lusine.error.uniform(drawn = lusine.error.draw(seed = bits(seed), step = bits(step), key,
                                                        index = bits(index)));
}

// An option drawn: a call or a put, a spot and a strike from 20 EUR to 200 EUR, a rate from -2% to 15% a year, a
// volatility from 5% to 90% a year, and a life from a week to five years.
(bool, float<EUR>, float<EUR>, float<year^-1>, float, float<year>) drawing(integer index, integer seed, integer step)
{
  return (uniform_of(seed, step, key = fnv1a("call"), index) < 0.5,
          20.0 EUR + uniform_of(seed, step, key = fnv1a("spot"), index) * 180.0 EUR,
          20.0 EUR + uniform_of(seed, step, key = fnv1a("strike"), index) * 180.0 EUR,
          -0.02 year^-1 + uniform_of(seed, step, key = fnv1a("rate"), index) * 0.17 year^-1,
          0.05 + 0.85 * uniform_of(seed, step, key = fnv1a("volatility"), index),
          0.02 year + uniform_of(seed, step, key = fnv1a("life"), index) * 4.98 year);
}

bool near(float<EUR> value, float<EUR> reference, float<EUR> spot)
{
  return abs(value - reference) <= 0.000002 * spot;
}

bool parity(float<EUR> called, float<EUR> put, float<EUR> spot, float<EUR> strike, float<year^-1> rate,
            float<year> years)
{
  return abs(called - put - (spot - strike * exp(-rate * years))) <= 16.0 * eps() * spot;
}

bool hull(float<EUR> spot, float<EUR> strike, float<year^-1> rate, float volatility, float<year> years,
          float<year> period, float<EUR> running_spot, float<EUR> running_strike, float<year^-1> running_rate,
          float<year> running_years)
{
  let c = lusine.finance.option_value(call = true, spot, strike, rate, volatility, period, years);
  let p = lusine.finance.option_value(call = false, spot, strike, rate, volatility, period, years);
  let g = lusine.finance.option_value(call = true, spot = running_spot, strike = running_strike, rate = running_rate,
                                      volatility, period, years = running_years);
  return abs(c - 4.76 EUR) <= 0.005 EUR && abs(p - 0.81 EUR) <= 0.005 EUR && abs(g - 2.40 EUR) <= 0.005 EUR;
}

bool in_yen(integer<JPY> spot, float<JPY> strike, float<year^-1> rate, float volatility, float<year> years,
            float<year> period)
{
  let c = lusine.finance.option_value(call = true, spot, strike, rate, volatility, period, years);
  let p = lusine.finance.option_value(call = false, spot, strike, rate, volatility, period, years);
  return abs(c - 476.0 JPY) <= 0.5 JPY && abs(p - 81.0 JPY) <= 0.5 JPY;
}

// Both values above their bounds; at the sweep's ends, a call far out of the money nothing and a put its bound, a call
// far in the money its bound and a put nothing.
bool bounded(integer step, float<EUR> called, float<EUR> put, float<EUR> call_bound, float<EUR> put_bound)
{
  let above = called >= call_bound - 0.0001 EUR && put >= put_bound - 0.0001 EUR;
  if (step == 0) return above && called < 0.000001 EUR && abs(put - put_bound) < 0.0001 EUR;
  if (step == 140) return above && abs(called - call_bound) < 0.0001 EUR && put < 0.0001 EUR;
  return above;
}

float<EUR> floor_of(float<EUR> value) { return value > 0.0 EUR ? value : 0.0 EUR; }

output table<lusine.widget> panels = {
  lusine.series { { panel = "the sweep", chart = "value", quantity = sweep.called, steps = 141, against = sweep.spot,
                    label = "call" } },
  lusine.series { { panel = "the sweep", chart = "value", quantity = sweep.put, steps = 141, against = sweep.spot,
                    label = "put" } },
  lusine.series { { panel = "the sweep", chart = "value above its bound", quantity = sweep.call_above, steps = 141,
                    against = sweep.spot, label = "call" } },
  lusine.series { { panel = "the sweep", chart = "value above its bound", quantity = sweep.put_above, steps = 141,
                    against = sweep.spot, label = "put" } },
  lusine.value { { panel = "the sweep", quantity = sweep.spot, label = "spot" } },
  lusine.value { { panel = "the sweep", quantity = sweep.called, label = "call" } },
  lusine.value { { panel = "the sweep", quantity = sweep.put, label = "put" } },
  lusine.value { { panel = "the checks", quantity = checks.hull, label = "Hull's examples" } },
  lusine.value { { panel = "the checks", quantity = checks.reference, label = "Black-Scholes in float64" } },
  lusine.value { { panel = "the checks", quantity = checks.parity, label = "put-call parity" } },
  lusine.value { { panel = "the checks", quantity = checks.yen, label = "in yen" } },
  lusine.value { { panel = "the checks", quantity = checks.bounds, label = "above the bounds" } },
};

void main()
{
  sweep.step = sweep.step >= 140 ? 0 : sweep.step + 1;
  sweep.spot = settings.first + float(sweep.step) * settings.stride;
  sweep.called = lusine.finance.option_value(call = true, sweep.spot, settings.strike, settings.rate,
                                             settings.volatility, settings.period, settings.years);
  sweep.put = lusine.finance.option_value(call = false, sweep.spot, settings.strike, settings.rate,
                                          settings.volatility, settings.period, settings.years);
  sweep.discounted = settings.strike * exp(-settings.rate * settings.years);
  sweep.call_bound = floor_of(value = sweep.spot - sweep.discounted);
  sweep.put_bound = floor_of(value = sweep.discounted - sweep.spot);
  sweep.call_above = sweep.called - sweep.call_bound;
  sweep.put_above = sweep.put - sweep.put_bound;
  options.value = lusine.finance.option_value(options.call, options.spot, options.strike, options.rate,
                                              options.volatility, settings.period, options.years);
  options.holds = near(options.value, options.reference, options.spot);
  (draws.call, draws.spot, draws.strike, draws.rate, draws.volatility, draws.years)
    = drawing(draws.index, settings.seed, step = sweep.step);
  draws.called = lusine.finance.option_value(call = true, draws.spot, draws.strike, draws.rate, draws.volatility,
                                             settings.period, draws.years);
  draws.put = lusine.finance.option_value(call = false, draws.spot, draws.strike, draws.rate, draws.volatility,
                                          settings.period, draws.years);
  draws.holds = parity(draws.called, draws.put, draws.spot, draws.strike, draws.rate, draws.years);
  checks.hull = checks.hull && hull(example.spot, example.strike, example.rate, example.volatility, example.years,
                                    settings.period, running_spot = running.spot, running_strike = running.strike,
                                    running_rate = running.rate, running_years = running.years);
  checks.reference = checks.reference && failing(holds = options.holds) == 0;
  checks.parity = checks.parity && failing(holds = draws.holds) == 0;
  checks.yen = checks.yen && in_yen(yen.spot, yen.strike, example.rate, example.volatility, example.years,
                                    settings.period);
  checks.bounds = checks.bounds && bounded(sweep.step, sweep.called, sweep.put, sweep.call_bound, sweep.put_bound);
  lamps.colour = lamp_colour(holds = checks.hull && checks.reference && checks.parity && checks.yen && checks.bounds);
}

The kernel's other proof (a thousand options against Black-Scholes in float64) is on its proof page.