Getting started
Shoals is a reef package. It depends on the Chelis compiler and on the
upstream libraries it builds upon: chelis-std (the standard library),
nautilus (distributions, special functions, statistics, interpolation),
and coral (the dataframe runtime). The dependency wiring is declared in
reef.toml at the repository root.
Building
Section titled “Building”The package gate is owned by the compiler:
# resolve the manifest and lower the packagechelis reef buildThe in-tree test suite exercises the finance invariants against the reference oracles:
# run the runtime test suitechelis test tests/ --timeout 180 --jobs auto
# serial run, useful when debugging a single failurechelis test tests/ --timeout 180 --jobs 1The formatter checks one file per invocation:
chelis fmt --check src/pricing.chThe local acceptance gate at scripts/run_local_gate.py runs the formatter
check over the .ch sources, the linter, chelis reef build, and the
runtime suite. Continuous integration runs the same steps.
A first price
Section titled “A first price”Black-Scholes is the smallest useful call. bs_call_scalar takes spot,
strike, the risk-free rate, volatility, and time to maturity, all f32,
and returns the call price. This call, drawn from tests/pricing.ch,
prices a one-year at-the-money call:
import Shoals.Pricing (bs_call_scalar)
def example() -> f32 = bs_call_scalar( cast(100.0, f32), cast(100.0, f32), cast(0.05, f32), cast(0.2, f32), cast(1.0, f32))The result is approximately 10.4506. The companion put,
bs_put_scalar, takes the same arguments and returns approximately
5.5735 for the same inputs.
Pricing a vector of spots
Section titled “Pricing a vector of spots”Most pricers have a vectorized form that maps over a tensor of spots. The
following example, from tests/pricing.ch, prices a call at three spot
levels at once:
import Shoals.Pricing (call_prices)
def example() -> tensor[3, f32] = { spots = to_tensor([cast(80.0, f32), cast(100.0, f32), cast(120.0, f32)]) call_prices(spots, cast(100.0, f32), cast(0.05, f32), cast(0.2, f32), cast(1.0, f32))}The three prices are approximately 1.8594, 10.4506, and 26.169.
A Monte Carlo price
Section titled “A Monte Carlo price”The Monte Carlo engine carries the Random effect, so it runs inside a
with seed(...) block that fixes the random stream. The number of paths is
the length of a template tensor you pass in. This example, from
tests/pricing.ch, prices the same call with twenty thousand paths:
import Shoals.Pricing (mc_call_price)
def example() -> f32 ! { Random } = { template = to_tensor(map(fn (i: int64) -> cast(0.0, f32), range(cast(0, int64), cast(20000, int64)))) 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 Monte Carlo estimate converges to the closed-form price as the path count grows. The next chapter explains the tensor and effect idioms these examples use.