-
Notifications
You must be signed in to change notification settings - Fork 959
Add Black-Scholes example with documentation #2056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
d115dff
8b0baa8
4a31f1b
b6f1ccc
ca56697
f34fd2f
6416302
c2c29e2
bfc13bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| Black-Scholes Equation | ||
| ====================== | ||
|
|
||
| Problem Setup | ||
| ------------- | ||
|
|
||
| We solve the Black-Scholes partial differential equation for pricing a European call option. | ||
| The problem is formulated in terms of **time-to-maturity** :math:`\tau = T - t`. | ||
|
|
||
| The PDE is given by: | ||
|
|
||
| .. math:: | ||
| \frac{\partial V}{\partial \tau} = \frac{1}{2}\sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + r S \frac{\partial V}{\partial S} - r V, \qquad S \in [0, S_{max}], \quad \tau \in [0, T] | ||
|
|
||
| where: | ||
| - :math:`V(S, \tau)` is the option price | ||
| - :math:`S` is the underlying asset price | ||
| - :math:`\tau` is the time to maturity | ||
| - :math:`\sigma=0.2` is the volatility | ||
| - :math:`r=0.05` is the risk-free interest rate | ||
|
|
||
| The boundary and initial conditions are: | ||
|
|
||
| 1. **Initial Condition** (at maturity :math:`\tau=0`): | ||
|
|
||
| .. math:: V(S, 0) = \max(S - K, 0) | ||
|
|
||
| where :math:`K=50` is the strike price. | ||
|
|
||
| 2. **Boundary Conditions**: | ||
|
|
||
| .. math:: | ||
| V(0, \tau) &= 0 \\ | ||
| V(S_{max}, \tau) &= S_{max} - K e^{-r\tau} | ||
|
|
||
| Implementation | ||
| -------------- | ||
|
|
||
| This example uses a Physics-Informed Neural Network (PINN) to solve the equation. | ||
|
|
||
| First, we import DeepXDE and define the problem parameters: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import deepxde as dde | ||
| import numpy as np | ||
| from scipy.stats import norm | ||
|
|
||
| K = 50.0 # Strike price | ||
| r = 0.05 # Risk-free rate | ||
| sigma = 0.2 # Volatility | ||
| T = 1.0 # Time to maturity | ||
| S_max = 150.0 # Maximum stock price | ||
|
|
||
| We define the computational geometry. We use ``GeometryXTime`` to combine the asset price interval :math:`[0, 150]` and the time domain :math:`[0, 1]`: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| geom = dde.geometry.Interval(0, S_max) | ||
| timedomain = dde.geometry.TimeDomain(0, T) | ||
| geomtime = dde.geometry.GeometryXTime(geom, timedomain) | ||
|
|
||
| The PDE residual is defined below. Here ``x[:, 0]`` is the asset price | ||
| :math:`S`, ``x[:, 1]`` is the time to maturity :math:`\tau`, and ``y`` | ||
| is the option price :math:`V(S, \tau)` output by the network. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| def pde(x, y): | ||
| # x[:, 0]: asset price S | ||
| # x[:, 1]: time to maturity tau | ||
| # y: option price V(S, tau) | ||
| S = x[:, 0:1] | ||
| dV_tau = dde.grad.jacobian(y, x, i=0, j=1) | ||
| dV_S = dde.grad.jacobian(y, x, i=0, j=0) | ||
| dV_SS = dde.grad.hessian(y, x, i=0, j=0) | ||
| return dV_tau - (0.5 * sigma**2 * S**2 * dV_SS + r * S * dV_S - r * y) | ||
|
|
||
| For the boundary conditions, we define the payoff function at maturity (:math:`\tau=0`) and the Dirichlet conditions at :math:`S=0` and :math:`S=S_{max}`: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| ic = dde.icbc.IC( | ||
| geomtime, | ||
| lambda x: np.maximum(x[:, 0:1] - K, 0.0), | ||
| lambda _, on_initial: on_initial, | ||
| ) | ||
| bc_lower = dde.icbc.DirichletBC( | ||
| geomtime, | ||
| lambda x: np.zeros((len(x), 1)), | ||
| lambda x, on_boundary: on_boundary and np.isclose(x[0], 0.0), | ||
| ) | ||
| bc_upper = dde.icbc.DirichletBC( | ||
| geomtime, | ||
| lambda x: x[:, 0:1] - K * np.exp(-r * x[:, 1:2]), | ||
| lambda x, on_boundary: on_boundary and np.isclose(x[0], S_max), | ||
| ) | ||
|
|
||
| **Sampling Strategy**: | ||
| To improve domain coverage and training stability, we use the **Sobol sequence** for sampling points. We select sample sizes as powers of 2 to avoid Sobol sequence warnings (2048 domain points, 64 boundary points, 128 initial points): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user might not know what sobol sequence is and hence have no idea what you're talking about when you say powers of two for sobol sequence warnings. Linking to a source or directly explaining would help.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the suggestion — I've updated the Sampling Strategy section to briefly Please let me know if you'd prefer a more explicit explanation for first-time readers. |
||
|
|
||
| .. code-block:: python | ||
|
|
||
| data = dde.data.TimePDE( | ||
| geomtime, | ||
| pde, | ||
| [ic, bc_lower, bc_upper], | ||
| num_domain=2047, # 2048 - 1 center point | ||
| num_boundary=63, | ||
| num_initial=127, | ||
| num_test=2047, | ||
| train_distribution="Sobol", | ||
| ) | ||
|
|
||
| **Network Architecture**: | ||
| We use a fully connected neural network with **4 hidden layers** and **28 neurons** per layer, using ``tanh`` activation and ``Glorot normal`` initialization: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| net = dde.nn.FNN([2] + [28] * 4 + [1], "tanh", "Glorot normal") | ||
|
|
||
| **Training Strategy**: | ||
| We employ a two-stage training process: first using the **Adam** optimizer for 20,000 iterations, followed by **L-BFGS** to fine-tune the solution and achieve lower residual error. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| model = dde.Model(data, net) | ||
| model.compile("adam", lr=1e-3) | ||
| model.train(iterations=20000) | ||
| model.compile("L-BFGS") | ||
| losshistory, train_state = model.train() | ||
|
|
||
| Validation | ||
| ---------- | ||
|
|
||
| The model is validated against the analytical Black-Scholes formula. We expect an L2 relative error around ``1e-3`` and a mean PDE residual around ``0.05``. | ||
|
|
||
| References | ||
| ---------- | ||
|
|
||
| .. [1] Tanios, R. (2021). "Physics Informed Neural Networks in Computational Finance: High Dimensional Forward & Inverse Option Pricing". ETH Zurich Master's Thesis. https://doi.org/10.3929/ethz-b-000491555 | ||
|
|
||
| Complete code | ||
| ------------- | ||
|
|
||
| .. literalinclude:: ../../../examples/pinn_forward/black_scholes_call.py | ||
| :language: python | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| """Backend supported: tensorflow.compat.v1, tensorflow, pytorch, paddle | ||
|
|
||
| Black-Scholes PDE for European call option pricing using PINN. | ||
|
|
||
| This example solves the Black-Scholes PDE in time-to-maturity form: | ||
| dV/dtau = 0.5*sigma^2*S^2*d^2V/dS^2 + r*S*dV/dS - r*V | ||
|
|
||
| where: | ||
| S : underlying asset price (x[:, 0]) | ||
| tau : time to maturity (x[:, 1]) | ||
| V : option price (network output y) | ||
|
|
||
| with boundary conditions: | ||
| V(S, 0) = max(S - K, 0) [Initial condition at maturity] | ||
| V(0, tau) = 0 [Lower boundary] | ||
| V(S_max, tau) = S_max - K*exp(-r*tau) [Upper boundary] | ||
|
|
||
| Reference: | ||
| Tanios, R. (2021). "Physics Informed Neural Networks in Computational Finance: | ||
| High Dimensional Forward & Inverse Option Pricing". ETH Zurich Master's Thesis. | ||
| https://doi.org/10.3929/ethz-b-000491555 | ||
| """ | ||
|
|
||
| import deepxde as dde | ||
| import numpy as np | ||
| from scipy.stats import norm | ||
|
|
||
|
|
||
| # Problem parameters | ||
| K = 50.0 # Strike price | ||
| r = 0.05 # Risk-free rate | ||
| sigma = 0.2 # Volatility | ||
| T = 1.0 # Time to maturity | ||
| S_max = 150.0 # Maximum stock price | ||
|
|
||
|
|
||
| def pde(x, y): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you are on board with the comment on R67 (same place in docs), change for consistency
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, updated consistently with the changes in the |
||
| """Black-Scholes PDE residual. | ||
|
|
||
| Args: | ||
| x: Input tensor. | ||
| x[:, 0]: asset price S | ||
| x[:, 1]: time to maturity tau | ||
| y: Network output representing the option price V(S, tau). | ||
|
|
||
| Returns: | ||
| PDE residual: dV/dtau - (0.5*sigma^2*S^2*d^2V/dS^2 + r*S*dV/dS - r*V) | ||
| """ | ||
| S = x[:, 0:1] | ||
| dV_tau = dde.grad.jacobian(y, x, i=0, j=1) | ||
| dV_S = dde.grad.jacobian(y, x, i=0, j=0) | ||
| dV_SS = dde.grad.hessian(y, x, i=0, j=0) | ||
| return dV_tau - (0.5 * sigma**2 * S**2 * dV_SS + r * S * dV_S - r * y) | ||
|
|
||
|
|
||
| def black_scholes_call(S, tau): | ||
| """Analytical Black-Scholes formula.""" | ||
| tau = np.maximum(tau, 1e-8) | ||
| S = np.maximum(S, 1e-10) | ||
| d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * tau) / (sigma * np.sqrt(tau)) | ||
| d2 = d1 - sigma * np.sqrt(tau) | ||
| return S * norm.cdf(d1) - K * np.exp(-r * tau) * norm.cdf(d2) | ||
|
|
||
|
|
||
| def gen_testdata(): | ||
| """Generate test data with analytical solution.""" | ||
| S = np.linspace(0, S_max, 256).reshape(256, 1) | ||
| tau = np.linspace(0, T, 201).reshape(201, 1) | ||
| SS, TT = np.meshgrid(S, tau) | ||
| X = np.vstack((np.ravel(SS), np.ravel(TT))).T | ||
| y = np.array([black_scholes_call(X[i, 0], X[i, 1]) for i in range(len(X))]) | ||
| return X, y[:, None] | ||
|
|
||
|
|
||
| # Computational geometry | ||
| geom = dde.geometry.Interval(0, S_max) | ||
| timedomain = dde.geometry.TimeDomain(0, T) | ||
| geomtime = dde.geometry.GeometryXTime(geom, timedomain) | ||
|
|
||
| # Boundary and initial conditions | ||
| ic = dde.icbc.IC( | ||
| geomtime, | ||
| lambda x: np.maximum(x[:, 0:1] - K, 0.0), | ||
| lambda _, on_initial: on_initial, | ||
| ) | ||
| bc_lower = dde.icbc.DirichletBC( | ||
| geomtime, | ||
| lambda x: np.zeros((len(x), 1)), | ||
| lambda x, on_boundary: on_boundary and np.isclose(x[0], 0.0), | ||
| ) | ||
| bc_upper = dde.icbc.DirichletBC( | ||
| geomtime, | ||
| lambda x: x[:, 0:1] - K * np.exp(-r * x[:, 1:2]), | ||
| lambda x, on_boundary: on_boundary and np.isclose(x[0], S_max), | ||
| ) | ||
|
|
||
| # Define PDE problem | ||
| data = dde.data.TimePDE( | ||
| geomtime, | ||
| pde, | ||
| [ic, bc_lower, bc_upper], | ||
| num_domain=2047, | ||
| num_boundary=63, | ||
| num_initial=127, | ||
| num_test=2047, | ||
| train_distribution="Sobol", | ||
| ) | ||
|
|
||
| # Neural network | ||
| net = dde.nn.FNN([2] + [28] * 4 + [1], "tanh", "Glorot normal") | ||
| model = dde.Model(data, net) | ||
|
|
||
| # Training | ||
| model.compile("adam", lr=1e-3) | ||
| model.train(iterations=20000) | ||
| model.compile("L-BFGS") | ||
| losshistory, train_state = model.train() | ||
|
|
||
| # Plot and save results | ||
| dde.saveplot(losshistory, train_state, issave=True, isplot=True) | ||
|
|
||
| # Validation | ||
| X, y_true = gen_testdata() | ||
| y_pred = model.predict(X) | ||
| f = model.predict(X, operator=pde) | ||
| print("Mean residual:", np.mean(np.absolute(f))) | ||
| print("L2 relative error:", dde.metrics.l2_relative_error(y_true, y_pred)) | ||
| np.savetxt("test.dat", np.hstack((X, y_true, y_pred))) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this area, it is probably helpful to denote that x[:, 0] is stock prices, x[:, 1] are taus, y is stock prices.
Furthermore, it is likely best to write dV instead of dy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the suggestion! I have updated the notation for clarity and consistency.
To clarify: in this example,
yrepresents the option priceV(S, tau),not the stock price — I believe that was a small typo in the review comment.
The variables are now annotated as follows:
x[:, 0]: asset priceSx[:, 1]: time to maturitytauy: option priceV(S, tau)(network output)I also renamed
dy_*todV_*in both the Python example and thecorresponding
.rstdocumentation.