Skip to content

walters-labs/PINN

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PINN

Physics-Informed Neural Networks (PINNs) for American-Style Option Pricing

Overview

A Physics-Informed Neural Network (PINN) is a neural architecture that embeds known physical or financial laws directly into its training process.
Instead of learning purely from data, the PINN minimizes a composite loss function that enforces the governing partial differential equation (PDE) — in this case, the Black–Scholes equation — as a soft constraint.

This approach allows the network to:

  • Learn the structure of option prices under no-arbitrage conditions,
  • Generalize better than purely data-driven models, and
  • Provide continuous, differentiable pricing surfaces across strike, time, and volatility domains.

The Black–Scholes PDE

For a European-style call option with price $V(S, t)$, spot price $S$, time $t$, constant risk-free rate $r$, and volatility $\sigma$, the Black–Scholes equation is:

$$\frac{\partial V}{\partial t} + \frac{1}{2} \sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + r S \frac{\partial V}{\partial S} - r V = 0$$

subject to the terminal condition at maturity $T$:

$$V(S, T) = \max(S - K, 0)$$

for strike price $K$.

The PINN learns a function $\hat{V}_\theta(S, t, \sigma, K, r)$ whose derivatives (computed via automatic differentiation) are constrained to satisfy this PDE throughout the training domain.


Loss Function Design

The total training loss combines three key components:

$$\mathcal{L} = \lambda_{\text{PDE}} \cdot \mathcal{L}_{\text{PDE}} + \lambda_{\text{terminal}} \cdot \mathcal{L}_{\text{terminal}} + \lambda_{\text{data}} \cdot \mathcal{L}_{\text{data}}$$

where:

  • PDE residual loss enforces the Black–Scholes differential constraint:

    \mathcal{L}_{\text{PDE}} = \mathbb{E}\left[\left(
    V_t + \tfrac{1}{2}\sigma^2 S^2 V_{SS} + r S V_S - r V
    \right)^2\right]
    
  • Terminal loss enforces the payoff condition $V(S, T) = \max(S-K, 0)$

  • Data loss (optional) fits observed market or synthetic prices

Training is performed using stochastic gradient descent (e.g., Adam), with all spatial and temporal derivatives computed via PyTorch’s autograd.


American Options Extension

For American-style options, early exercise introduces a free boundary condition:

$$V(S, t) \geq \max(S - K, 0)$$

and the PDE becomes an inequality-constrained problem (a complementarity formulation):

$$\min\left( \frac{\partial V}{\partial t} + \frac{1}{2} \sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + r S \frac{\partial V}{\partial S} - r V,\; V - (S - K) \right) = 0$$

In the PINN framework, this condition is typically handled by:

  • Adding a penalty term that enforces $V \geq \text{payoff}$,
  • Or training two subnetworks (one for the continuation region, one for the exercise region),
  • Or using a ReLU barrier to ensure nonnegative exercise premium.

Support for this is currently in development.


Visualization and Validation

After training, we visualize the PINN's predictions against analytic Black–Scholes solutions (for European-style options) under a constant volatility assumption:

plt.plot(S_grid, V_true, label="Black–Scholes (analytic)")
plt.plot(S_grid, V_pred, '--', label="PINN prediction")
plt.xlabel("Underlying Price S")
plt.ylabel("Option Price V(S, t)")
plt.legend()
plt.grid(True)

European-Style Options (PINN) — Explanation of the Code

This section describes how the repository implements a Physics-Informed Neural Network (PINN) for European-style option pricing, and explains the key pieces of the reference implementation (model, losses, training loop, and evaluation).

1 — What the PINN learns

The model approximates the option pricing function $V(S, t; K, r, \sigma)$ where:

  • S is the underlying asset price (spot),
  • t is the current time (measured in years),
  • K is the option strike,
  • r is the risk-free rate,
  • σ is volatility (constant or per-sample).

We train a neural network V̂_θ that takes (scaled) inputs and outputs the option price.


2 — Governing PDE (enforced in the loss)

The Black–Scholes PDE (used for the PDE residual term) is rendered using the math fenced block below:

$$\frac{\partial V}{\partial t} + \tfrac{1}{2}\sigma^{2} S^{2} \frac{\partial^{2} V}{\partial S^{2}} + r S \frac{\partial V}{\partial S} - r V = 0$$

The PINN minimizes the mean-square of this PDE residual over randomly sampled collocation points (S, t) in the training domain.


3 — Loss components

The overall loss is a weighted sum of three terms:

  • PDE residual loss
    Mean squared PDE residual across collocation points:

    L_pde = mean( (V_t + 0.5 * sigma^2 * S^2 * V_SS + r*S*V_S - r*V)^2 )
    
  • Terminal (payoff) loss
    Enforce the terminal condition at maturity T:

    $$V(S, T) = \max(S - K, 0)$$

    Practically:

    L_term = mean( (V(S, T) - max(S - K, 0))**2 )
  • Data loss (optional)
    Fit observed market prices (or synthetic BS prices) at sampled (S, t):

    L_data = mean( (V_pred - market_price)**2 )

Final loss:

Loss = λ_pde * L_pde + λ_term * L_term + λ_data * L_data

4 — Key implementation details (what the code does)

Below are the important functions and design choices from the reference implementation.

Model

A fully connected feed-forward network that accepts scaled inputs (S_scaled, t_scaled, sigma_scaled) and outputs the scalar price:

class PINN(nn.Module):
    def __init__(self, input_dim=3, width=64, depth=3):
        super().__init__()
        layers = []
        layers.append(nn.Linear(input_dim, width))
        layers.append(nn.Tanh())
        for _ in range(depth - 1):
            layers.append(nn.Linear(width, width))
            layers.append(nn.Tanh())
        layers.append(nn.Linear(width, 1))
        self.net = nn.Sequential(*layers)

    def forward(self, S, t, sigma):
        x = torch.cat([S, t, sigma], dim=1)  # (N, 3)
        return self.net(x)                   # (N, 1)

Note: scaling (S/K and t/T) is applied before passing inputs to the network. Scaling stabilizes training.

PDE residual (autograd)

We compute derivatives using torch.autograd.grad. The residual function returns the PDE residual for a batch of (S,t):

def pde_residual(model, S, t, sigma, r):
    S_req = S.clone().detach().requires_grad_(True)
    t_req = t.clone().detach().requires_grad_(True)
    sigma_in = sigma.clone().detach()

    V = model(S_req_scaled, t_req_scaled, sigma_scaled)  # model expects scaled inputs

    V_S = torch.autograd.grad(V, S_req, grad_outputs=torch.ones_like(V), create_graph=True)[0]
    V_t = torch.autograd.grad(V, t_req, grad_outputs=torch.ones_like(V), create_graph=True)[0]
    V_SS = torch.autograd.grad(V_S, S_req, grad_outputs=torch.ones_like(V_S), create_graph=True)[0]

    residual = V_t + 0.5 * (sigma_in ** 2) * (S_req ** 2) * V_SS + r * S_req * V_S - r * V
    return residual

In practice, the code wraps the model so that raw physical variables S and t are scaled internally before being passed to the neural net.

Terminal condition loss

Evaluate the network at t = T and penalize deviation from the intrinsic payoff:

def terminal_loss(model, S_terminal, K, r, sigma, T):
    t_T = torch.full_like(S_terminal, T)
    V_pred_T = model(S_terminal_scaled, t_T_scaled, sigma_scaled)
    payoff = torch.maximum(S_terminal - K, torch.zeros_like(S_terminal))
    return torch.mean( (V_pred_T - payoff)**2 )

5 — Training loop (high-level skeleton)

The training loop alternates the computation of the three loss terms and backpropagates the weighted sum:

for epoch in range(n_epochs):
    optimizer.zero_grad()

    # collocation samples (S_coll, t_coll)
    residual = pde_residual(wrapped_model, S_coll, t_coll, sigma_coll, r)
    L_pde = torch.mean(residual**2)

    # terminal loss
    L_term = terminal_loss(wrapped_model_T, S_terminal, K_term, r, sigma_term, T_maturity)

    # optional data loss
    V_pred_data = model(S_data_scaled, t_data_scaled, sigma_data_scaled)
    L_data = torch.mean( (V_pred_data - price_data)**2 )

    loss = pde_weight * L_pde + term_weight * L_term + data_weight * L_data
    loss.backward()
    optimizer.step()

Practical notes:

  • Use a relatively large term_weight (terminal loss) to ensure the network respects the payoff.
  • Train first with Adam, optionally fine-tune with LBFGS for better convergence.
  • Sample collocation points broadly across the S and t domain (e.g., S ∈ [0, 3K] and t ∈ [0, T]).

6 — Evaluation & visualization

After training:

  • Evaluate on a grid S_grid × t_grid and compare to analytic Black–Scholes results (when σ is constant) to validate correctness.
  • Typical plots:
    • V_PINN(S, t=0) vs. analytic V_BS(S, t=0).
    • Training history (total loss and components).
    • Residual heatmap |PDE_residual(S,t)|.

Example plotting code snippet:

# Evaluate on a grid at t = 0
S_grid = torch.linspace(0.01, 2*K_val, 300).view(-1,1).to(device)
t_zero = torch.zeros_like(S_grid).to(device)
# scale inputs and run model...
V_pred = model(S_grid_scaled, t_zero_scaled, sigma_grid_scaled).cpu().numpy().flatten()

# analytic Black–Scholes
V_true = bs_price_call_torch(S_grid.cpu(), K_grid.cpu(), t_zero.cpu(), T_maturity, r_val, sigma_const).cpu().numpy().flatten()

plt.plot(S_grid.cpu().numpy(), V_true, label='Black–Scholes (analytic)')
plt.plot(S_grid.cpu().numpy(), V_pred, '--', label='PINN prediction')
plt.xlabel('Underlying price S')
plt.ylabel('Option price V(S,t=0)')
plt.legend(); plt.grid(True)

7 — How to plug in real market data

To use actual option-market data:

  1. Build tensors of (S, t, K, sigma, market_price) where:
    • S = current spot (or historical spot when price snapshot was taken)
    • t = current time (or the time corresponding to the market price), or time-to-maturity
    • sigma = implied volatility (optional — can be used as input) or leave as constant
    • market_price = mid-price (or other observed price)
  2. Replace the mock S_data, t_data, K_data, sigma_data, price_data used in the demo with your actual tensors (all shaped (N,1)).
  3. Keep the PDE residual and terminal loss components — they act as regularizers that enforce no-arbitrage structure.

8 — Tips & troubleshooting

  • Scaling: Always scale S and t before passing to the network (e.g. S_scaled = S/K, t_scaled = t/T). This makes training stable.
  • Small maturities: For extremely small time-to-maturity, guard against numerical issues in the analytic price and in tau = T - t (clamp with eps).
  • Weights: Tune λ_pde, λ_term, and λ_data. If the network reproduces market prices but violates the PDE, increase λ_pde.
  • Local volatility: To learn a σ(S,t), replace sigma as an input with a small subnetwork sigma_net(S_scaled, t_scaled) that outputs a positive volatility (e.g. via softplus). Train both subnets jointly with the PDE residual.

About

physics-informed neural networks to solve various PDEs

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors