Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Paskov-Traub: QMC in high dimension#
Theory said quasi-Monte Carlo should lose its edge beyond a dozen dimensions, since \((\log N)^d / N\) only beats \(N^{-1/2}\) for astronomically large \(N\). Spassimir Paskov and Joseph Traub (1995) priced a Goldman Sachs collateralized mortgage obligation, a 360-dimensional integral, and found Sobol points beat Monte Carlo anyway. The explanation, later formalized as effective dimension: financial integrands depend mostly on a few coordinates and their low-order interactions.
Here: a discretely monitored arithmetic Asian call on 64 dates, a
64-dimensional integral, with Sobol increments driving the steps in
time order (financialkit.integrators.sobol_increments() with
bridge=False).
import matplotlib.pyplot as plt
import numpy as np
from financialkit.integrators import gbm_paths
from financialkit.monte_carlo import gbm_path_estimate
from financialkit.monte_carlo.visualizers import plot_error_convergence
S, K, T, r, sigma, d = 100.0, 100.0, 1.0, 0.05, 0.2, 64
def asian(paths):
return np.maximum(paths[:, 1:].mean(axis=1) - K, 0.0)
Standard error against paths#
Both estimates are replicated eight times with fresh seeds; the standard errors are on the same footing.
ns = 2 ** np.arange(7, 12)
results = {s: [gbm_path_estimate(asian, S, T, r, sigma, d, n, sampler=s, n_replications=8, rng=n) for n in ns] for s in ("pseudo", "sobol")}
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
plot_error_convergence(ns, [x.std_error for x in results["pseudo"]], ax=axes[0], label="Monte Carlo", rates=(0.5,))
plot_error_convergence(ns, [x.std_error for x in results["sobol"]], ax=axes[0], label="Sobol, 64 dimensions", rates=())
axes[0].set_title("arithmetic Asian call, 64 dates")
for s in ("pseudo", "sobol"):
x = results[s][-1]
print(f"{s:7s}: {x.estimate:.4f} +/- {x.std_error:.4f}")

pseudo : 5.8934 +/- 0.0410
sobol : 5.8454 +/- 0.0124
The integrand’s effective dimension is low#
Payoff sensitivity to each Sobol coordinate, by perturbing one normal at a time: the average depends on every increment, but those of the early dates matter most, since they move more of the average.
z = np.random.default_rng(0).standard_normal((4000, d))
base = asian(gbm_paths(S, r, sigma, T, d, 4000, dW=np.sqrt(T / d) * z)[1])
sens = []
for i in range(d):
zi = z.copy()
zi[:, i] += 0.1
sens.append(np.mean(np.abs(asian(gbm_paths(S, r, sigma, T, d, 4000, dW=np.sqrt(T / d) * zi)[1]) - base)))
axes[1].bar(np.arange(1, d + 1), sens)
axes[1].set_xlabel("coordinate (time step)")
axes[1].set_ylabel("mean |payoff change|")
fig.tight_layout()
plt.show()
Total running time of the script: (0 minutes 0.256 seconds)