Pricing
Module: Shoals.Pricing.
This module provides the Black-Scholes call and put in closed form,
vectorized price tensors over a set of spots, gradient-derived sensitivity
vectors, and a Monte Carlo call pricer that carries the Random effect.
The standard normal cumulative distribution is computed through
Nautilus.Special.erfc as 0.5 * erfc(-x / sqrt(2)).
Closed-form scalars
Section titled “Closed-form scalars”def bs_call_scalar(s: f32, k: f32, r: f32, sigma: f32, t: f32) -> f32def bs_put_scalar(s: f32, k: f32, r: f32, sigma: f32, t: f32) -> f32bs_call_scalar and bs_put_scalar price a European call and put on a
non-dividend-paying underlying. The arguments are spot s, strike k,
the continuously compounded risk-free rate r, volatility sigma, and
time to maturity t in years.
From tests/pricing.ch, a one-year at-the-money call and put:
px = bs_call_scalar(cast(100.0, f32), cast(100.0, f32), cast(0.05, f32), cast(0.2, f32), cast(1.0, f32))// px is approximately 10.4506
pp = bs_put_scalar(cast(100.0, f32), cast(100.0, f32), cast(0.05, f32), cast(0.2, f32), cast(1.0, f32))// pp is approximately 5.5735These two satisfy put-call parity: c - p == s - k * exp(-r * t).
Vectorized prices
Section titled “Vectorized prices”def call_prices[n](spots: tensor[n, f32], k: f32, r: f32, sigma: f32, t: f32) -> tensor[n, f32]def put_prices[n](spots: tensor[n, f32], k: f32, r: f32, sigma: f32, t: f32) -> tensor[n, f32]def call_total[n](spots: tensor[n, f32], k: f32, r: f32, sigma: f32, t: f32) -> f32def put_total[n](spots: tensor[n, f32], k: f32, r: f32, sigma: f32, t: f32) -> f32call_prices and put_prices map the scalar pricer over a tensor of
spots, holding strike, rate, volatility, and maturity fixed. call_total
and put_total sum the resulting prices to a single f32.
From tests/pricing.ch:
spots = to_tensor([cast(80.0, f32), cast(100.0, f32), cast(120.0, f32)])prices = call_prices(spots, cast(100.0, f32), cast(0.05, f32), cast(0.2, f32), cast(1.0, f32))// prices are approximately 1.8594, 10.4506, 26.169call_total(spots, ...) equals the sum of the entries of
call_prices(spots, ...).
Gradient-derived sensitivities
Section titled “Gradient-derived sensitivities”def deltas_call[n](spots: tensor[n, f32], k: f32, r: f32, sigma: f32, t: f32) -> tensor[n, f32]def deltas_put[n](spots: tensor[n, f32], k: f32, r: f32, sigma: f32, t: f32) -> tensor[n, f32]def vegas_call[n](spots: tensor[n, f32], k: f32, r: f32, sigma: f32, t: f32) -> tensor[n, f32]deltas_call and deltas_put differentiate the total call or put price
with respect to the spot vector using grad, yielding a delta per spot.
vegas_call differentiates the total call price with respect to a vector
of volatilities, yielding a vega per spot. These are the gradient-derived
sensitivity paths; the Greeks chapter documents the
finite-difference and analytic Greeks that the test suite uses directly.
Monte Carlo call price
Section titled “Monte Carlo call price”def mc_call_price[n](template: tensor[n, f32], s0: f32, k: f32, r: f32, sigma: f32, t: f32) -> f32 ! { Random }mc_call_price simulates terminal prices under geometric Brownian motion,
takes the discounted mean of the call payoff, and returns the Monte Carlo
estimate. The number of paths is the length of the template tensor. The
function carries the Random effect and must run inside a with seed(...)
block.
From tests/pricing.ch, a twenty-thousand-path estimate of the ATM call:
template = to_tensor(map(fn (i: int64) -> cast(0.0, f32), range(cast(0, int64), cast(20000, int64))))mc_px = with seed(42) { mc_call_price(template, cast(100.0, f32), cast(100.0, f32), cast(0.05, f32), cast(0.2, f32), cast(1.0, f32))}The estimate lands within two percent of bs_call_scalar at this path
count. Running the same call twice under the same literal seed returns the
identical value.