Skip to content

Commit

Permalink
Release/0.15.0 (#427)
Browse files Browse the repository at this point in the history
* ENH: Support vega and theta for BS modules (#424) (#412)

* MAINT: Miscellaneous maintenance (#407) (#408)

* MAINT: torch requirement changed to 1.9.0 (#425)

* TEST: Add workflow to test that examples work (close #108) (#409)

Co-authored-by: Masanori HIRANO <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 23, 2021
1 parent 4d7ff17 commit bc4ae30
Show file tree
Hide file tree
Showing 21 changed files with 587 additions and 14 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Examples

env:
PROJECT_NAME: pfhedge
PYTHON_VERSION: '3.9'

on:
workflow_dispatch:

jobs:

examples:

name: Examples

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v2

- uses: actions/setup-python@v2
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install
run: |
pip install poetry
poetry install
poetry run pip install -r examples/requirements.txt
- name: Run examples
run: cd examples && find . -name '*.py' -exec poetry run python {} \;
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
poetry.lock
docs/source/generated/
examples/output/
examples/output/.gitkeep
docs/source/generated
examples/output/*
!examples/output/.gitkeep

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
File renamed without changes.
9 changes: 9 additions & 0 deletions pfhedge/_utils/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ def parse_volatility(
return variance.clamp(min=0.0).sqrt()
else:
raise ValueError("Insufficient parameters to parse volatility")


def parse_time_to_maturity(
*, time_to_maturity: Optional[Tensor] = None, **kwargs
) -> Tensor:
if time_to_maturity is not None:
return time_to_maturity
else:
raise ValueError("Insufficient parameters to parse time_to_maturity")
60 changes: 60 additions & 0 deletions pfhedge/autogreek.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from torch import Tensor

from ._utils.parse import parse_spot
from ._utils.parse import parse_time_to_maturity
from ._utils.parse import parse_volatility


Expand Down Expand Up @@ -231,3 +232,62 @@ def vega(
grad_outputs=torch.ones_like(price),
create_graph=create_graph,
)[0]


def theta(
pricer: Callable[..., Tensor], *, create_graph: bool = False, **params
) -> Tensor:
"""Computes and returns theta of a derivative using automatic differentiation.
Theta is a differentiation of a derivative price with respect to
a variance of underlying instrument.
Note:
The keyword argument ``**params`` should contain at least one of the
following parameters:
- ``time_to_maturity``
Args:
pricer (callable): Pricing formula of a derivative.
create_graph (bool, default=False): If ``True``,
graph of the derivative will be constructed,
allowing to compute higher order derivative products.
**params: Parameters passed to ``pricer``.
Returns:
torch.Tensor
Examples:
Theta of a European option can be evaluated as follows.
>>> import pfhedge.autogreek as autogreek
>>> from pfhedge.nn import BSEuropeanOption
>>>
>>> pricer = BSEuropeanOption().price
>>> autogreek.theta(
... pricer,
... log_moneyness=torch.zeros(3),
... time_to_maturity=torch.tensor([0.1, 0.2, 0.3]),
... volatility=torch.tensor([0.20, 0.20, 0.20]),
... )
tensor([-0.1261, -0.0891, -0.0727])
"""
time_to_maturity = parse_time_to_maturity(**params).requires_grad_()
params["time_to_maturity"] = time_to_maturity

# Delete parameters that are not in the signature of pricer to avoid
# TypeError: <pricer> got an unexpected keyword argument '<parameter>'
for parameter in list(params.keys()):
if parameter not in signature(pricer).parameters.keys():
del params[parameter]

price = pricer(**params)
# Note: usually theta is calculated reversely (\partial{S}/\partial{T} = \partial{S}/\partial{-time_to_maturity})
return -torch.autograd.grad(
price,
inputs=time_to_maturity,
grad_outputs=torch.ones_like(price),
create_graph=create_graph,
)[0]
9 changes: 9 additions & 0 deletions pfhedge/nn/modules/bs/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ def vega(self, **kwargs) -> Tensor:
"""
return autogreek.vega(self.price, **kwargs)

@no_type_check
def theta(self, **kwargs) -> Tensor:
"""Returns delta of the derivative.
Returns:
torch.Tensor
"""
return autogreek.theta(self.price, **kwargs)

def inputs(self) -> List[str]:
"""Returns the names of input features.
Expand Down
73 changes: 73 additions & 0 deletions pfhedge/nn/modules/bs/american_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,79 @@ def gamma(
volatility=volatility,
)

@torch.enable_grad()
def vega(
self,
log_moneyness: Tensor,
max_log_moneyness: Tensor,
time_to_maturity: Tensor,
volatility: Tensor,
) -> Tensor:
"""Returns vega of the derivative.
Args:
log_moneyness (torch.Tensor): Log moneyness of the underlying asset.
max_log_moneyness (torch.Tensor): Cumulative maximum of the log moneyness.
time_to_maturity (torch.Tensor): Time to expiry of the option.
volatility (torch.Tensor): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)` where
:math:`*` means any number of additional dimensions.
- max_log_moneyness: :math:`(N, *)`
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Returns:
torch.Tensor
"""
return super().vega(
strike=self.strike,
log_moneyness=log_moneyness,
max_log_moneyness=max_log_moneyness,
time_to_maturity=time_to_maturity,
volatility=volatility,
)

@torch.enable_grad()
def theta(
self,
log_moneyness: Tensor,
max_log_moneyness: Tensor,
time_to_maturity: Tensor,
volatility: Tensor,
) -> Tensor:
"""Returns theta of the derivative.
Args:
log_moneyness (torch.Tensor): Log moneyness of the underlying asset.
max_log_moneyness (torch.Tensor): Cumulative maximum of the log moneyness.
time_to_maturity (torch.Tensor): Time to expiry of the option.
volatility (torch.Tensor): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)` where
:math:`*` means any number of additional dimensions.
- max_log_moneyness: :math:`(N, *)`
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Note:
Risk-free rate is set to zero.
Returns:
torch.Tensor
"""
return super().theta(
strike=self.strike,
log_moneyness=log_moneyness,
max_log_moneyness=max_log_moneyness,
time_to_maturity=time_to_maturity,
volatility=volatility,
)

def implied_volatility(
self,
log_moneyness: Tensor,
Expand Down
58 changes: 54 additions & 4 deletions pfhedge/nn/modules/bs/european.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,67 @@ def gamma(
Returns:
torch.Tensor
"""
if not self.call:
raise ValueError(
f"{self.__class__.__name__} for a put option is not yet supported."
)

s, t, v = broadcast_all(log_moneyness, time_to_maturity, volatility)
price = self.strike * s.exp()
gamma = npdf(d1(s, t, v)) / (price * v * t.sqrt())

return gamma

def vega(
self, log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor
) -> Tensor:
"""Returns vega of the derivative.
Args:
log_moneyness: (torch.Tensor): Log moneyness of the underlying asset.
time_to_maturity (torch.Tensor): Time to expiry of the option.
volatility (torch.Tensor): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)` where
:math:`*` means any number of additional dimensions.
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Returns:
torch.Tensor
"""
s, t, v = broadcast_all(log_moneyness, time_to_maturity, volatility)
price = self.strike * s.exp()
vega = npdf(d1(s, t, v)) * price * t.sqrt()

return vega

def theta(
self, log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor
) -> Tensor:
"""Returns theta of the derivative.
Args:
log_moneyness: (torch.Tensor): Log moneyness of the underlying asset.
time_to_maturity (torch.Tensor): Time to expiry of the option.
volatility (torch.Tensor): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)` where
:math:`*` means any number of additional dimensions.
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Note:
Risk-free rate is set to zero.
Returns:
torch.Tensor
"""
s, t, v = broadcast_all(log_moneyness, time_to_maturity, volatility)
price = self.strike * s.exp()
theta = -npdf(d1(s, t, v)) * price * v / (2 * t.sqrt())
return theta

def price(
self, log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor
) -> Tensor:
Expand Down
57 changes: 57 additions & 0 deletions pfhedge/nn/modules/bs/european_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,63 @@ def gamma(
volatility=volatility,
)

def vega(
self, log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor
) -> Tensor:
"""Returns vega of the derivative.
Args:
log_moneyness (torch.Tensor): Log moneyness of the underlying asset.
time_to_maturity (torch.Tensor): Time to expiry of the option.
volatility (torch.Tensor): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)`
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Returns:
torch.Tensor
"""
# TODO: Directly compute theta.
return super().vega(
strike=self.strike,
log_moneyness=log_moneyness,
time_to_maturity=time_to_maturity,
volatility=volatility,
)

def theta(
self, log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor
) -> Tensor:
"""Returns theta of the derivative.
Args:
log_moneyness (torch.Tensor): Log moneyness of the underlying asset.
time_to_maturity (torch.Tensor): Time to expiry of the option.
volatility (torch.Tensor): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)`
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Note:
Risk-free rate is set to zero.
Returns:
torch.Tensor
"""
# TODO: Directly compute theta.
return super().theta(
strike=self.strike,
log_moneyness=log_moneyness,
time_to_maturity=time_to_maturity,
volatility=volatility,
)

def implied_volatility(
self,
log_moneyness: Tensor,
Expand Down
Loading

0 comments on commit bc4ae30

Please sign in to comment.