# Monte Carlo Option-Pricing Lab

An interactive browser demo for [`@neutrium/rand`](../README.md). The lab prices a European call option with a Monte Carlo simulation and compares the result with the analytical Black–Scholes price.

The demo is designed to make random-number generation choices visible. It shows how the seed, backend, batch size, and number of parallel workers affect reproducibility and performance without hiding those decisions behind the pricing calculation.

## Run the demo

Open the [hosted lab](https://neutrium.github.io/rand/demo/) on GitHub Pages.

Or locally, from the repository root:

```sh
pnpm install
pnpm demo
```

Open the local URL printed by Vite.

To create and preview a production build:

```sh
pnpm demo:build
pnpm --filter @neutrium/rand-option-lab preview
```

## Use the lab

1. Enter a **seed**. The seed identifies the deterministic random stream.
2. Choose the number of **paths** and **workers**.
3. Set the market assumptions:
   - **Spot price** is the asset price at the start of the simulation.
   - **Strike** is the option's exercise price.
   - **Volatility** is annualised and entered as a decimal, so `0.22` means
     22%.
   - **Duration** is the time to expiry in years.
   - **Risk-free rate** is continuously compounded and entered as a decimal.
4. Choose an **algorithm**. In Recommended mode this follows
   `Random.recommend()` automatically. With an explicit backend, only compatible
   algorithms are enabled.
5. Select **Recommended**, **JavaScript**, **WASM**, or **WebGPU** as the
   random-number backend. Recommended mode chooses both the algorithm and
   backend for the current workload.
6. Select **Run simulation**.

The results show:

- The discounted Monte Carlo option-price estimate.
- The analytical Black–Scholes price.
- The absolute and percentage error.
- A 95% confidence interval for the Monte Carlo estimate.
- A histogram of simulated terminal asset prices.
- The backend and algorithm that actually ran.
- Random-value throughput.
- Separate setup, generation, calculation, and transfer timings.

Use **Copy replay link** to encode the current parameters and seed in the URL. Opening that link restores the experiment. Running the same seed with the same settings produces the same random streams and result.

The form accepts up to 2 million paths. This keeps the illustrative implementation within a bounded memory budget while it retains terminal prices and discounted payoffs for the histogram and confidence interval.

## Pricing model

Under the risk-neutral Geometric Brownian Motion (GBM) model, each terminal asset price is:

```text
S(T) = S(0) × exp((r − σ²/2)T + σ√T Z)
```

Where:
- S(T) is the future stock price
- S(0) is the initial stock price
- r is the risk-free interest rate
- σ is the volatility measure (how much and how quickly stock price fluctuates)
- T is the time of option expiration
- `Z` is a standard normal random variable. The worker converts pairs of
uniform random values to normal values with the Box–Muller transform.


The discounted payoff for one path is:

```text
exp(−rT) × max(S(T) − K, 0)
```

The Monte Carlo price is the sample mean of these payoffs. Its confidence interval is calculated from the sample variance. Black–Scholes provides an analytical benchmark for the same assumptions.

## How it demonstrates `@neutrium/rand`

### Reproducible bulk generation

Each worker creates the selected bulk generator with a public seed and stream
identifier. The optional entry point accepts the same algorithm/backend pair
returned by `Random.recommend()` and delegates CPU/WASM algorithms to the
root implementation:

```ts
import { Random } from "@neutrium/rand/webgpu";

const random = await Random.create({
  algorithm,
  backend,
  seed,
  stream: workerIndex
});

const uniforms = await random.float64_array(pathCount * 2);
await random.destroy_async();
```

`float64_array()` generates the two uniforms needed for each Box–Muller sample
in one bulk operation. Recreating the generator with the same seed, algorithm,
stream, backend, and workload reproduces the sequence.

### Deterministic parallel streams

The main thread divides the paths between Web Workers. Philox and PCG receive
the public seed and a distinct stream number:

```ts
worker.postMessage({
  seed,
  stream: workerIndex,
  count: pathsForThisWorker,
  backend
});
```

Philox is counter-based and PCG exposes explicit streams, so their workers do
not depend on scheduling order. Xoshiro does not expose independent stream
identifiers, so the demo derives a stable worker-specific seed instead. These
strategies keep the result reproducible even when workers finish in a different
order.

The worker count is part of the experiment. Keep it unchanged when replaying an exact result because changing it changes how paths are assigned to streams.

### Compatible algorithm and backend selection

WebGPU remains outside the root package API. Because this demo offers all
backends in one control, it uses the optional entry point's unified creation
method for every selection:

```ts
import { Random } from "@neutrium/rand/webgpu";

const random = await Random.create({
  algorithm,
  backend,
  seed,
  stream
});
```

The controls enforce the library's compatibility rules:

- JavaScript supports all four algorithms.
- WASM supports Philox4x32-10, PCG32, and Xoshiro256++.
- WebGPU supports Philox4x32-10.

In Recommended mode, the demo passes the algorithm and backend returned by
`Random.recommend()` to the workers. The backend and algorithm cards therefore
match the recommendation. For an explicit selection, the lab reports the
backend and algorithm that actually supplied the values.

The WebGPU option requires browser and hardware support. An unavailable explicit backend produces an error instead of silently pretending that it ran. The automatic choice may use an available fallback.

### Live recommendations

The recommendation card calls the optional facade's `Random.recommend()` whenever the form changes. The facade probes adapter and device usability internally; `Random.capabilities()` exposes the same cached result for UI availability controls:

```ts
import { Random } from "@neutrium/rand/webgpu";

const usableWebGpu = (await Random.capabilities()).webgpu;

const recommendation = await Random.recommend({
  call_pattern: "bulk",
  count: paths * 2,
  output_location: gpuResident ? "gpu" : "cpu",
  workers
});
```

The card displays both the proposed algorithm/backend and the library's reason. This demonstrates that the best choice depends on workload shape and data location, not simply on which generator has the highest isolated throughput.

### Performance trade-offs

The demo measures four parts of the workflow separately:

- **Setup** creates and initialises the selected generator.
- **Generation** creates the bulk uniform values.
- **Calculation** transforms the uniforms and evaluates terminal prices and
  payoffs.
- **Transfer** covers the remaining main-thread coordination and worker result
  transfer time.

Throughput is based on generation time and the number of generated uniforms. The separate totals make it possible to see cases where a fast generator does not produce the fastest end-to-end simulation.

### Statistical output

Random values are consumed by a complete numerical workload rather than only printed or benchmarked. The terminal-price histogram, option-price confidence interval, and comparison with Black–Scholes make sampling error visible.

### GPU-resident workloads

Selecting **Model a GPU-resident workload** changes the recommendation request to `output_location: "gpu"`, demonstrating when `Random.recommend()` prefers generating and consuming values on the GPU.

This control models the workload for recommendation purposes; it does not claim that the chart itself is GPU-resident. The chart reads samples back to CPU memory because it needs terminal prices and payoffs for the browser histogram and confidence interval. A production GPU-resident calculation would use the optional facade's `Random.create()` and `generate_buffer()`, bind the returned `GPUBuffer` directly to the pricing compute pipeline, and read back only the final aggregates. See [`docs/examples/gpu-resident.ts`](../docs/examples/gpu-resident.ts) for the zero-readback handoff.

## Source guide

- [`index.html`](index.html) contains the complete interface markup.
- [`src/main.ts`](src/main.ts) initializes the application and coordinates UI
  events.
- [`src/configuration.ts`](src/configuration.ts) owns URL/form parsing,
  recommendations, and compatible algorithm/backend selection.
- [`src/worker-coordinator.ts`](src/worker-coordinator.ts) partitions paths and
  manages worker lifecycles, errors, and timeouts.
- [`src/worker-protocol.ts`](src/worker-protocol.ts) defines the request and
  response messages shared by the main thread and workers.
- [`src/simulation.ts`](src/simulation.ts) contains Black–Scholes, path
  simulation, and summary-statistic calculations.
- [`src/rendering.ts`](src/rendering.ts) updates metrics, timings, and the
  terminal-price histogram.
- [`src/worker.ts`](src/worker.ts) uses `@neutrium/rand` and its optional
  `@neutrium/rand/webgpu` subpath to generate each worker's selected random
  stream and simulate its assigned paths.
- [`src/style.css`](src/style.css) contains the presentation and responsive
  layout.
