Add publication-ready documentation and reproducible experiment package.
Rewrite the README with secure setup instructions, add dedicated setup/security docs, and include the standalone local-volatility instability experiment materials for reproducible analysis. Made-with: Cursor
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Built Python extension dropped next to qengine/__init__.py for local dev
|
||||
/qengine/*.so
|
||||
/qengine/*.dylib
|
||||
/qengine/__pycache__/
|
||||
|
||||
/skbuild-build/
|
||||
|
||||
/build/
|
||||
/.idea/
|
||||
**/__pycache__/
|
||||
/docs/html/
|
||||
/docs/latex/
|
||||
|
||||
# Local reference tree (optional clone)
|
||||
/CPP-design-pattern-derivatives-pricing/
|
||||
|
||||
# Local environment and secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Local tooling caches
|
||||
/.pycache/
|
||||
/.mplconfig/
|
||||
80
README.md
80
README.md
@@ -1,5 +1,79 @@
|
||||
# pricing
|
||||
# option_pricing
|
||||
|
||||
Monte Carlo pricing of European options under Black–Scholes
|
||||
C++/Python quantitative finance engine for option pricing, implied-volatility analysis, and market-data ingestion.
|
||||
|
||||
### Project structure
|
||||
## What is included
|
||||
|
||||
- `cpp/`: core C++ pricing library (Monte Carlo + Black-Scholes closed form), DB ingestion hooks, and pybind bindings.
|
||||
- `qengine/`: Python package exposing the native extension (`import qengine`).
|
||||
- `src/ImpliedVolatility/`: SVI calibration and implied-volatility tooling.
|
||||
- `src/data/`: data ingestion, SQL schema, and analytics helpers.
|
||||
- `tests/`: C++ unit tests (GoogleTest).
|
||||
- `scripts/`: operational scripts, including PostgreSQL setup.
|
||||
- `docs/`: Doxygen configuration and generated API docs (ignored in git for publication).
|
||||
|
||||
## Quickstart
|
||||
|
||||
### 1) Clone and create a Python environment
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -e .
|
||||
pip install pandas yfinance sqlalchemy psycopg2-binary matplotlib scipy
|
||||
```
|
||||
|
||||
### 2) Configure environment variables
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Then edit `.env` with your local database credentials.
|
||||
|
||||
### 3) Create database and schema
|
||||
|
||||
Use the idempotent setup script:
|
||||
|
||||
```bash
|
||||
source .env
|
||||
python scripts/setup_postgres.py
|
||||
```
|
||||
|
||||
This script creates/updates:
|
||||
- database role (`DB_USER`)
|
||||
- database (`DB_NAME`)
|
||||
- tables/indexes from `src/data/sql/schema.sql`
|
||||
|
||||
### 4) Build C++ extension and run tests
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build -j
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
### 5) Run Yahoo options ingestion
|
||||
|
||||
```bash
|
||||
source .env
|
||||
python src/data/ingestion/ingest_yahoo_options.py
|
||||
```
|
||||
|
||||
`PIPELINE_SYMBOLS` in `.env` controls which symbols are ingested (comma-separated, e.g. `SPY,AAPL,QQQ`).
|
||||
|
||||
## Security and publication notes
|
||||
|
||||
- No credentials are stored in source code.
|
||||
- `.env` files are git-ignored; only `.env.example` is committed.
|
||||
- Before publishing, rotate any credentials that were ever committed in the past.
|
||||
- Prefer least-privilege DB users for runtime ingestion jobs.
|
||||
|
||||
## Generating C++ API docs
|
||||
|
||||
```bash
|
||||
cmake --build build --target docs
|
||||
```
|
||||
|
||||
Generated output goes to `docs/html/` and is ignored in version control.
|
||||
|
||||
27
docs/SECURITY.md
Normal file
27
docs/SECURITY.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Security Checklist
|
||||
|
||||
## Secrets handling
|
||||
|
||||
- Never commit `.env` or any file containing credentials.
|
||||
- Use `.env.example` for non-sensitive defaults only.
|
||||
- Set DB credentials through environment variables.
|
||||
- Rotate credentials if they have ever appeared in git history.
|
||||
|
||||
## Database hardening
|
||||
|
||||
- Use a dedicated runtime user with least required privileges.
|
||||
- Keep administrative users separate from ingestion users.
|
||||
- Restrict DB network access to trusted hosts/VPC/private network.
|
||||
- Enable SSL/TLS for non-local database connections.
|
||||
|
||||
## Publication readiness
|
||||
|
||||
Before making the repository public:
|
||||
|
||||
1. Confirm `git status` has no secret files staged.
|
||||
2. Search for potential secret patterns:
|
||||
- passwords
|
||||
- API keys
|
||||
- tokens
|
||||
3. Verify `.gitignore` includes local secret files (`.env*`).
|
||||
4. Regenerate credentials used during development.
|
||||
60
docs/SETUP.md
Normal file
60
docs/SETUP.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Setup Guide
|
||||
|
||||
This guide describes a clean local setup for development and reproducible runs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- CMake 3.16+
|
||||
- A C++20 compiler
|
||||
- PostgreSQL 14+ (or Docker)
|
||||
- On macOS, Homebrew packages for C++ DB support:
|
||||
- `libpq`
|
||||
- `libpqxx`
|
||||
- `eigen`
|
||||
- `pybind11`
|
||||
|
||||
## Python dependencies
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -e .
|
||||
pip install pandas yfinance sqlalchemy psycopg2-binary matplotlib scipy
|
||||
```
|
||||
|
||||
## Environment configuration
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set:
|
||||
|
||||
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
|
||||
- `PIPELINE_SYMBOLS`
|
||||
- admin credentials used only by setup script (`POSTGRES_ADMIN_*`)
|
||||
|
||||
## Database bootstrap
|
||||
|
||||
```bash
|
||||
source .env
|
||||
python scripts/setup_postgres.py
|
||||
```
|
||||
|
||||
The script is idempotent and safe to rerun.
|
||||
|
||||
## Build and test C++
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build -j
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
## Generate Doxygen docs
|
||||
|
||||
```bash
|
||||
cmake --build build --target docs
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
This folder is intentionally self-contained.
|
||||
|
||||
- No imports from the parent option_pricing package (no qengine, src/, cpp bindings).
|
||||
- Third-party dependencies: numpy, matplotlib (see requirements.txt).
|
||||
- Run: python run_experiment.py [--out lv_rmse.png]
|
||||
- Safe to copy elsewhere or run in isolation.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Gatheral local variance in total-variance / log-moneyness form (practitioner's guide).
|
||||
|
||||
sigma^2 = (d_T w) / ( 1 - (y/w) d_y w
|
||||
+ (1/4)(-1/4 - 1/w + y^2/w^2) (d_y w)^2
|
||||
+ (1/2) d_yy w )
|
||||
|
||||
where w = omega is total implied variance, y is log-moneyness (convention as in the note).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def local_variance_from_derivatives(
|
||||
y: np.ndarray,
|
||||
w: np.ndarray,
|
||||
dy_w: np.ndarray,
|
||||
dyy_w: np.ndarray,
|
||||
dT_w: np.ndarray,
|
||||
*,
|
||||
eps: float = 1e-14,
|
||||
) -> np.ndarray:
|
||||
"""Vectorized Gatheral formula. Invalid / near-singular points become nan."""
|
||||
y = np.asarray(y, dtype=float)
|
||||
w = np.asarray(w, dtype=float)
|
||||
dy_w = np.asarray(dy_w, dtype=float)
|
||||
dyy_w = np.asarray(dyy_w, dtype=float)
|
||||
dT_w = np.asarray(dT_w, dtype=float)
|
||||
|
||||
out = np.full_like(y, np.nan, dtype=float)
|
||||
ok = np.isfinite(w) & (np.abs(w) > eps) & np.isfinite(dy_w) & np.isfinite(dyy_w) & np.isfinite(dT_w)
|
||||
|
||||
denom = np.empty_like(w)
|
||||
denom[ok] = (
|
||||
1.0
|
||||
- (y[ok] / w[ok]) * dy_w[ok]
|
||||
+ 0.25 * (-0.25 - 1.0 / w[ok] + (y[ok] ** 2) / (w[ok] ** 2)) * (dy_w[ok] ** 2)
|
||||
+ 0.5 * dyy_w[ok]
|
||||
)
|
||||
|
||||
ok2 = ok & (np.abs(denom) > eps)
|
||||
out[ok2] = dT_w[ok2] / denom[ok2]
|
||||
return out
|
||||
|
||||
|
||||
def quadratic_total_variance(
|
||||
y: np.ndarray,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
gamma: float,
|
||||
T: float,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""
|
||||
w(y,T) = T * (alpha + beta*y + gamma*y^2), with derivatives as in the note:
|
||||
|
||||
d_T w = alpha + beta*y + gamma*y^2
|
||||
d_y w = T * (beta + 2*gamma*y)
|
||||
d_yy w = 2*gamma*T
|
||||
"""
|
||||
y = np.asarray(y, dtype=float)
|
||||
f = alpha + beta * y + gamma * y ** 2
|
||||
w = T * f
|
||||
dT_w = f
|
||||
dy_w = T * (beta + 2.0 * gamma * y)
|
||||
dyy_w = np.full_like(y, 2.0 * gamma * T)
|
||||
return w, dT_w, dy_w, dyy_w
|
||||
|
||||
|
||||
def analytic_local_variance_quadratic(
|
||||
y: np.ndarray,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
gamma: float,
|
||||
T: float,
|
||||
) -> np.ndarray:
|
||||
"""Closed form from the note (equivalent to plugging derivatives into Gatheral)."""
|
||||
y = np.asarray(y, dtype=float)
|
||||
w, dT_w, dy_w, dyy_w = quadratic_total_variance(y, alpha, beta, gamma, T)
|
||||
return local_variance_from_derivatives(y, w, dy_w, dyy_w, dT_w)
|
||||
|
||||
|
||||
def central_first_derivative_uniform(w: np.ndarray, h: float) -> np.ndarray:
|
||||
"""Interior (w[i+1]-w[i-1])/(2h); endpoints nan."""
|
||||
w = np.asarray(w, dtype=float)
|
||||
out = np.full_like(w, np.nan)
|
||||
out[1:-1] = (w[2:] - w[:-2]) / (2.0 * h)
|
||||
return out
|
||||
|
||||
|
||||
def second_derivative_uniform(w: np.ndarray, h: float) -> np.ndarray:
|
||||
"""Interior second difference / h^2; endpoints nan."""
|
||||
w = np.asarray(w, dtype=float)
|
||||
out = np.full_like(w, np.nan)
|
||||
out[1:-1] = (w[2:] - 2.0 * w[1:-1] + w[:-2]) / (h ** 2)
|
||||
return out
|
||||
|
||||
|
||||
def add_multiplicative_noise(
|
||||
w: np.ndarray,
|
||||
sigma_noise: float,
|
||||
rng: np.random.Generator,
|
||||
) -> np.ndarray:
|
||||
"""tilde w(y_i) = w(y_i) * (1 + eps), eps ~ N(0, sigma_noise^2)."""
|
||||
w = np.asarray(w, dtype=float)
|
||||
eps = rng.normal(0.0, sigma_noise, size=w.shape)
|
||||
return w * (1.0 + eps)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
@@ -0,0 +1,2 @@
|
||||
numpy>=1.20
|
||||
matplotlib>=3.5
|
||||
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Local-volatility instability experiment (Gatheral total variance in log-moneyness).
|
||||
|
||||
We compare the analytic local variance σ²(y) from a quadratic total variance
|
||||
w(y,T) = T(α + βy + γy²) to σ² reconstructed from a noisy discrete surface
|
||||
w̃(y_i) = w(y_i)(1 + ε_i) using finite differences in y, for several levels of
|
||||
multiplicative noise σ_noise. This script only produces the figure: RMSE of the
|
||||
FD reconstruction vs σ_noise (log–log), with a y = σ reference line of slope 1.
|
||||
|
||||
Dependencies: numpy, matplotlib only (see INDEPENDENT_STANDALONE.txt).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
# Prevent accidental imports from the parent repository
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if _REPO_ROOT in sys.path:
|
||||
sys.path.remove(_REPO_ROOT)
|
||||
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from gatheral_local_vol import (
|
||||
add_multiplicative_noise,
|
||||
analytic_local_variance_quadratic,
|
||||
central_first_derivative_uniform,
|
||||
local_variance_from_derivatives,
|
||||
quadratic_total_variance,
|
||||
second_derivative_uniform,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defaults (quadratic total variance, positive w on y ∈ [-0.5, 0.5])
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ALPHA = 0.04
|
||||
BETA = 0.0
|
||||
GAMMA = 0.1
|
||||
T_MATURITY = 1.0
|
||||
Y_MIN = -0.5
|
||||
Y_MAX = 0.5
|
||||
N_GRID = 201
|
||||
|
||||
|
||||
def ensure_parent_dir(path: str) -> None:
|
||||
parent = os.path.dirname(os.path.abspath(path))
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
|
||||
|
||||
def log_uniform_sigma_grid(n_points: int, sigma_min: float, sigma_max: float) -> np.ndarray:
|
||||
"""
|
||||
Return `n_points` values of σ_noise with log₁₀(σ) equally spaced.
|
||||
|
||||
This is the correct sampling for a log–log RMSE plot; it is not linspace(σ_min, σ_max).
|
||||
"""
|
||||
n_points = max(4, n_points)
|
||||
if sigma_min <= 0 or sigma_max <= 0 or sigma_max < sigma_min:
|
||||
raise ValueError("Require 0 < sigma_min <= sigma_max.")
|
||||
return np.logspace(np.log10(sigma_min), np.log10(sigma_max), n_points)
|
||||
|
||||
|
||||
def relative_pointwise_error(
|
||||
sigma2_analytic: np.ndarray, sigma2_fd: np.ndarray, eps: float = 1e-12
|
||||
) -> np.ndarray:
|
||||
return (sigma2_fd - sigma2_analytic) / np.maximum(np.abs(sigma2_analytic), eps)
|
||||
|
||||
|
||||
def rmse_absolute(
|
||||
sigma2_analytic: np.ndarray,
|
||||
sigma2_fd: np.ndarray,
|
||||
interior: slice,
|
||||
) -> float:
|
||||
"""RMSE of (σ²_FD − σ²_analytic) on interior indices."""
|
||||
sa = np.asarray(sigma2_analytic, dtype=float)[interior]
|
||||
sf = np.asarray(sigma2_fd, dtype=float)[interior]
|
||||
m = np.isfinite(sa) & np.isfinite(sf)
|
||||
if not np.any(m):
|
||||
return float("nan")
|
||||
d = sf[m] - sa[m]
|
||||
return float(np.sqrt(np.mean(d * d)))
|
||||
|
||||
|
||||
def rmse_relative(
|
||||
sigma2_analytic: np.ndarray,
|
||||
sigma2_fd: np.ndarray,
|
||||
interior: slice,
|
||||
eps: float = 1e-12,
|
||||
) -> float:
|
||||
"""RMSE over grid points of relative error (σ²_FD − σ²_analytic) / |σ²_analytic|."""
|
||||
re = relative_pointwise_error(sigma2_analytic, sigma2_fd, eps=eps)[interior]
|
||||
m = np.isfinite(re)
|
||||
if not np.any(m):
|
||||
return float("nan")
|
||||
return float(np.sqrt(np.mean(re[m] ** 2)))
|
||||
|
||||
|
||||
def local_variance_one_draw(
|
||||
y: np.ndarray,
|
||||
h: float,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
gamma: float,
|
||||
T: float,
|
||||
sigma_noise: float,
|
||||
rng: np.random.Generator,
|
||||
dT_mode: Literal["exact", "noisy_ratio"],
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""One noisy surface and FD local variance; returns (σ²_analytic, σ²_FD)."""
|
||||
w_true, dT_w_true, _, _ = quadratic_total_variance(y, alpha, beta, gamma, T)
|
||||
sigma2_a = analytic_local_variance_quadratic(y, alpha, beta, gamma, T)
|
||||
|
||||
w_tilde = add_multiplicative_noise(w_true, sigma_noise, rng)
|
||||
dy = central_first_derivative_uniform(w_tilde, h)
|
||||
dyy = second_derivative_uniform(w_tilde, h)
|
||||
|
||||
if dT_mode == "exact":
|
||||
dT = dT_w_true
|
||||
elif dT_mode == "noisy_ratio":
|
||||
dT = w_tilde / T
|
||||
else:
|
||||
raise ValueError(dT_mode)
|
||||
|
||||
sigma2_fd = local_variance_from_derivatives(y, w_tilde, dy, dyy, dT)
|
||||
return sigma2_a, sigma2_fd
|
||||
|
||||
|
||||
def rmse_curves_averaged(
|
||||
y: np.ndarray,
|
||||
h: float,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
gamma: float,
|
||||
T: float,
|
||||
sigma_grid: np.ndarray,
|
||||
rng: np.random.Generator,
|
||||
dT_mode: Literal["exact", "noisy_ratio"],
|
||||
interior: slice,
|
||||
trials_per_sigma: int,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
For each σ in `sigma_grid`, average RMSE (relative and absolute) over
|
||||
`trials_per_sigma` independent noise draws.
|
||||
"""
|
||||
rel: list[float] = []
|
||||
abs_: list[float] = []
|
||||
trials_per_sigma = max(1, trials_per_sigma)
|
||||
|
||||
for sig in sigma_grid:
|
||||
tr: list[float] = []
|
||||
ta: list[float] = []
|
||||
for _ in range(trials_per_sigma):
|
||||
sa, sf = local_variance_one_draw(
|
||||
y, h, alpha, beta, gamma, T, float(sig), rng, dT_mode
|
||||
)
|
||||
tr.append(rmse_relative(sa, sf, interior))
|
||||
ta.append(rmse_absolute(sa, sf, interior))
|
||||
rel.append(float(np.nanmean(tr)))
|
||||
abs_.append(float(np.nanmean(ta)))
|
||||
|
||||
return np.asarray(rel, dtype=float), np.asarray(abs_, dtype=float)
|
||||
|
||||
|
||||
def plot_rmse_vs_noise(
|
||||
sigma_grid: np.ndarray,
|
||||
rmse_rel: np.ndarray,
|
||||
rmse_abs: np.ndarray,
|
||||
*,
|
||||
h: float,
|
||||
T: float,
|
||||
dT_mode: str,
|
||||
trials_per_sigma: int,
|
||||
) -> mpl.figure.Figure:
|
||||
"""
|
||||
Log–log plot: RMSE (relative and absolute in σ²) vs σ_noise, reference y = σ.
|
||||
"""
|
||||
fig, ax = plt.subplots(figsize=(5.8, 3.8), constrained_layout=True)
|
||||
|
||||
x = np.asarray(sigma_grid, dtype=float)
|
||||
pos = x > 0
|
||||
n = len(x)
|
||||
ms = 3.5 if n > 50 else 4.5
|
||||
|
||||
ax.loglog(
|
||||
x[pos],
|
||||
rmse_rel[pos],
|
||||
"o-",
|
||||
ms=ms,
|
||||
lw=1.25,
|
||||
label=r"RMSE of relative error $(\sigma^2_{\mathrm{FD}}-\sigma^2_{\mathrm{nat}})/|\sigma^2_{\mathrm{nat}}|$",
|
||||
zorder=3,
|
||||
)
|
||||
ax.loglog(
|
||||
x[pos],
|
||||
rmse_abs[pos],
|
||||
"s--",
|
||||
ms=ms - 1,
|
||||
lw=1.0,
|
||||
alpha=0.9,
|
||||
label=r"RMSE of $\sigma^2$ error $|\sigma^2_{\mathrm{FD}}-\sigma^2_{\mathrm{nat}}|$",
|
||||
zorder=2,
|
||||
)
|
||||
|
||||
s_lo, s_hi = float(x[pos].min()), float(x[pos].max())
|
||||
ax.loglog([s_lo, s_hi], [s_lo, s_hi], ":", color="0.4", lw=2.0, zorder=1, label=r"reference slope 1: $y=\sigma_{\mathrm{noise}}$")
|
||||
|
||||
ax.set_xlabel(r"$\sigma_{\mathrm{noise}}$ (multiplicative noise on $\tilde{w}$)")
|
||||
ax.set_ylabel("RMSE (interior $y$)")
|
||||
subtitle = f"$T={T}$, $h={h:.4f}$, $\\partial_T w$: {dT_mode}"
|
||||
if trials_per_sigma > 1:
|
||||
subtitle += f", mean over {trials_per_sigma} draws per $\\sigma$"
|
||||
ax.set_title("FD local variance: RMSE vs noise\n" + subtitle, fontsize=10)
|
||||
ax.grid(True, which="both", alpha=0.35)
|
||||
ax.legend(loc="best", fontsize=8, framealpha=0.95)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def configure_matplotlib_style() -> None:
|
||||
"""Conservative defaults suitable for print."""
|
||||
mpl.rcParams.update(
|
||||
{
|
||||
"figure.dpi": 120,
|
||||
"savefig.dpi": 300,
|
||||
"font.size": 10,
|
||||
"axes.labelsize": 10,
|
||||
"axes.titlesize": 10,
|
||||
"legend.fontsize": 8,
|
||||
"axes.grid": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
configure_matplotlib_style()
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="RMSE of finite-difference local variance vs multiplicative noise (single figure).",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=42, help="RNG seed.")
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
type=str,
|
||||
default="lv_rmse.png",
|
||||
help="Output image path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dT-mode",
|
||||
choices=("exact", "noisy_ratio"),
|
||||
default="exact",
|
||||
help="Treatment of ∂_T w when w is replaced by noisy w̃ on the grid.",
|
||||
)
|
||||
parser.add_argument("--rmse-points", type=int, default=35, help="Number of σ_noise values (log-uniform).")
|
||||
parser.add_argument("--rmse-sigma-min", type=float, default=1e-5, help="Smallest σ_noise.")
|
||||
parser.add_argument("--rmse-sigma-max", type=float, default=5e-4, help="Largest σ_noise.")
|
||||
parser.add_argument(
|
||||
"--rmse-trials",
|
||||
type=int,
|
||||
default=50,
|
||||
help="Independent noisy surfaces per σ_noise; RMSE is averaged.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
rng = np.random.default_rng(args.seed)
|
||||
y = np.linspace(Y_MIN, Y_MAX, N_GRID)
|
||||
h = float(y[1] - y[0])
|
||||
interior = slice(1, -1)
|
||||
|
||||
sigma_grid = log_uniform_sigma_grid(args.rmse_points, args.rmse_sigma_min, args.rmse_sigma_max)
|
||||
rmse_rel, rmse_abs = rmse_curves_averaged(
|
||||
y,
|
||||
h,
|
||||
ALPHA,
|
||||
BETA,
|
||||
GAMMA,
|
||||
T_MATURITY,
|
||||
sigma_grid,
|
||||
rng,
|
||||
args.dT_mode,
|
||||
interior,
|
||||
args.rmse_trials,
|
||||
)
|
||||
|
||||
fig = plot_rmse_vs_noise(
|
||||
sigma_grid,
|
||||
rmse_rel,
|
||||
rmse_abs,
|
||||
h=h,
|
||||
T=T_MATURITY,
|
||||
dT_mode=args.dT_mode,
|
||||
trials_per_sigma=args.rmse_trials,
|
||||
)
|
||||
|
||||
ensure_parent_dir(args.out)
|
||||
fig.savefig(args.out, bbox_inches="tight")
|
||||
print(f"Wrote {args.out}")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user