Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions slides/Deep_American_Option_Parfun.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "47b881bd-482c-4e44-9378-01ee80f8c0cf",
"metadata": {},
"source": [
"# Heavy American Option Pricing with Longstaff-Schwartz (LSM) and parfun\n",
"\n",
"This notebook replaces the simple binomial tree iwth a **Longstaff-Schwartz Monte Carlo (LSM)** mode. LSM introduces:\n",
"- A full **stochastic process simulation** (GBM paths)\n",
"- **Cross-sectional regressions** at every exercise data\n",
"- Much higher computational cost\n",
"\n",
"This is representative of **production American option engines** used for long-dated and complex payoffs.\n"
]
},
{
"cell_type": "markdown",
"id": "c73d2b8d-e3c1-4346-8c82-4a3333a32d95",
"metadata": {},
"source": [
"# Imports and Environment\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "8e4e027c-3e04-4d88-88cb-a7af60f9e062",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import sys\n",
"import numpy as np\n",
"import time\n",
"\n",
"sys.stderr = open(os.devnull, \"w\")"
]
},
{
"cell_type": "markdown",
"id": "98538b4e-24c7-4bdd-85ef-602a78fc0a7b",
"metadata": {},
"source": [
"# Longstaff-Schwartz Monte Carlo (American Put)\n",
"\n",
"This implementation follows the classical Longstaff-Schwartz (2001) algorithm.\n",
"\n",
"Computational cost:\n",
"- Path simulation: O(paths x steps)\n",
"- Regression per step: O(paths x $basis^2$ x steps)\n",
"This quickly grows into **multi-minute workloads**."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "7303eb76-db11-44b8-ba21-cffde2732fa7",
"metadata": {},
"outputs": [],
"source": [
"def american_put_lsm(S, K, r, sigma, T, steps, paths, degree=6, seed=None):\n",
" if seed is not None:\n",
" np.random.seed(seed)\n",
"\n",
" dt = T / steps\n",
" disc = np.exp(-r * dt)\n",
"\n",
" Z = np.random.normal(size=(paths, steps))\n",
" S_paths = np.empty((paths, steps + 1))\n",
" S_paths[:, 0] = S\n",
"\n",
" for t in range(steps):\n",
" S_paths[:, t + 1] = S_paths[:, t] * np.exp(\n",
" (r - 0.5 * sigma **2) * dt + sigma * np.sqrt(dt) * Z[:, t]\n",
" )\n",
"\n",
" # Payoff at maturity\n",
" cashflows = np.maximum(K - S_paths[:, -1], 0.0)\n",
"\n",
" # Backward inducion\n",
" for t in range(steps - 1, 0, -1):\n",
" itm = S_paths[:, t] < K\n",
" X = S_paths[itm, t]\n",
" Y = cashflows[itm] * disc\n",
"\n",
" if len(X) > degree:\n",
" coeffs = np.polyfit(X, Y, degree)\n",
" continuation = np.polyval(coeffs, X)\n",
" else:\n",
" continuation = np.zeros_like(X)\n",
"\n",
" exercise = K - X\n",
" exercise_now = exercise > continuation\n",
"\n",
" cashflows[itm] = np.where(\n",
" exercise_now,\n",
" exercise,\n",
" Y\n",
" )\n",
"\n",
" return cashflows.mean() * disc\n",
" "
]
},
{
"cell_type": "markdown",
"id": "161a01a5-18ac-4833-8328-b3a4772c4297",
"metadata": {},
"source": [
"# Heavy Sequetial Workload\n",
"\n",
"We price the same option under many volatility scenarios and repeat this across a large batch. This mimics real-world **scenario analysis / stress testing** workloads."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "33354277-c135-482e-ad05-d00cff5ec6a4",
"metadata": {},
"outputs": [],
"source": [
"# Heavy parameters\n",
"PATHS = 80_000 # Monte Carlo paths (increase to push single run runtime)\n",
"STEPS = 252 # daily exercise dates\n",
"DEGREE = 6 # regression basis complexity; increases single run time\n",
"BATCH_SIZE = 4 # portfolio size. It increases\n",
"SCENARIOS = np.linspace(0.15, 0.35, 20)\n",
"\n",
"base_param = (100.0, 100.0, 0.05, 0.2, 1.0, STEPS, PATHS)\n",
"BATCH = [base_param] * BATCH_SIZE\n",
"\n",
"\n",
"def price_with_scenarios(p, scenarios):\n",
" S, K, r, _, T, St, N = p\n",
" acc = [american_put_lsm(S, K, r, vol, T, St, N, DEGREE) for vol in scenarios]\n",
" return sum(acc)\n",
"\n",
"\n",
"def batch_price_with_scenarios(tasks, scenarios):\n",
" return [price_with_scenarios(p, scenarios)/len(scenarios) for p in tasks]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "73b809f9-2aba-40eb-90d7-1366b42a2d8b",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"results_seq = batch_price_with_scenarios(BATCH, SCENARIOS)\n",
"seq_time = time.time() - start\n",
"\n",
"print(f\"Sequential runtime: {seq_time / 60:.2f} minutes\")"
]
},
{
"cell_type": "markdown",
"id": "e1b10f72-527d-4154-a608-93bb893bb84e",
"metadata": {},
"source": [
"# Parallel Execution with parfun\n",
"\n",
"We now parallellize the outer batch loop using **parfun**. Only a decorator and a function call change are required."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eb1d234c-d74f-46d3-81a4-b0a280af72c2",
"metadata": {},
"outputs": [],
"source": [
"\n",
"# Requires: pip install opengris-parfun\n",
"import parfun as pf\n",
"from typing import List, Tuple\n",
"\n",
"@pf.parfun(split=pf.per_argument(tasks=pf.py_list.by_chunk), combine_with=pf.py_list.concat, fixed_partition_size=1)\n",
"def batch_price_with_scenarios_w_parfun(tasks, scenarios):\n",
" return [price_with_scenarios(p, scenarios)/len(scenarios) for p in tasks]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "76c24a6b-e3ab-4aa5-8b36-4a1ef0f7bbc1",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"with pf.set_parallel_backend_context(\"scaler_local\", n_workers=4):\n",
" results_par = batch_price_with_scenarios_w_parfun(BATCH, SCENARIOS)\n",
"par_time = time.time() - start\n",
"\n",
"print(f\"Parallel runtime: {par_time / 60:.2f} minutes\")\n",
"print(f\"Speedup: {seq_time / par_time:.2f}x\")"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running within the notebook, the speedup is consistently around 1.2x; while running locally on a 4 core machine gives a consistent speedup of 1.5x.

The aforementioned 4 core machine a.k.a my laptop has a past history of not able to correctly showcase the performance improvement with parfun.

]
},
{
"cell_type": "markdown",
"id": "33443fa8-af01-49d3-8d8e-680c9e933c6d",
"metadata": {},
"source": [
"# Interpretation\n",
"- The sequential run should take **~10 minutes** on a single core (machine-dependent).\n",
"- the parfun version distributes work across available cores automatically.\n",
"- Speedup should approach the number of physical cores for this embarrasingly parallel workload.\n",
"\n",
"This pattern closely matches real-world **risk, stress testing, and model validation** workloads."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading