Skip to content

Commit

Permalink
Release/0.12.3 (#323)
Browse files Browse the repository at this point in the history
* ENH: Add `ncdf`, `npdf`, `d1`, `d2` to functional.py (#315)

* ENH: Add `Variance` feature (close #269) (#319)

* ENH: Add `autogreek.vega` (close #97) (#320)

* MAINT: Refine typing (#316) (#317)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
simaki and github-actions[bot] committed Sep 6, 2021
1 parent df163bc commit 0207be5
Show file tree
Hide file tree
Showing 27 changed files with 407 additions and 138 deletions.
12 changes: 12 additions & 0 deletions docs/source/autogreek.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,17 @@ pfhedge.autogreek

.. currentmodule:: pfhedge.autogreek

Delta
-----

.. autofunction:: delta

Gamma
-----

.. autofunction:: gamma

Vega
----

.. autofunction:: vega
4 changes: 4 additions & 0 deletions docs/source/nn.functional.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ Other Functions
.. autofunction:: realized_variance
.. autofunction:: realized_volatility
.. autofunction:: terminal_value
.. autofunction:: ncdf
.. autofunction:: npdf
.. autofunction:: d1
.. autofunction:: d2
24 changes: 23 additions & 1 deletion pfhedge/_utils/parse.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from numbers import Real
from typing import Optional
from typing import Union

import torch
from torch import Tensor


def _as_optional_tensor(input: Optional[Union[Tensor, Real]]) -> Optional[Tensor]:
return torch.as_tensor(input) if input is not None else input


def parse_spot(
*,
spot: Optional[Tensor] = None,
Expand All @@ -12,11 +18,27 @@ def parse_spot(
log_moneyness: Optional[Tensor] = None,
**kwargs
) -> Tensor:
spot = _as_optional_tensor(spot)
strike = _as_optional_tensor(strike)
moneyness = _as_optional_tensor(moneyness)
log_moneyness = _as_optional_tensor(log_moneyness)

if spot is not None:
return spot
elif moneyness is not None and strike is not None:
return moneyness * strike
elif log_moneyness is not None and strike is not None:
return log_moneyness.exp() * strike
else:
raise ValueError("Insufficient parameters to parse `spot`")
raise ValueError("Insufficient parameters to parse spot")


def parse_volatility(
*, volatility: Optional[Tensor] = None, variance: Optional[Tensor] = None, **kwargs
) -> Tensor:
if volatility is not None:
return volatility
elif variance is not None:
return variance.clamp(min=0.0).sqrt()
else:
raise ValueError("Insufficient parameters to parse volatility")
5 changes: 5 additions & 0 deletions pfhedge/_utils/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Union

from torch import Tensor

TensorOrScalar = Union[Tensor, float, int]
73 changes: 67 additions & 6 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_volatility


def delta(
Expand All @@ -25,9 +26,9 @@ def delta(
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.
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:
Expand Down Expand Up @@ -96,7 +97,6 @@ def delta(
if parameter not in signature(pricer).parameters.keys():
del params[parameter]

assert spot.requires_grad
price = pricer(**params)
return torch.autograd.grad(
price,
Expand Down Expand Up @@ -124,8 +124,9 @@ def gamma(
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.
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:
Expand Down Expand Up @@ -170,3 +171,63 @@ def gamma(
grad_outputs=torch.ones_like(tensor_delta),
create_graph=create_graph,
)[0]


def vega(
pricer: Callable[..., Tensor], *, create_graph: bool = False, **params
) -> Tensor:
"""Computes and returns vega of a derivative using automatic differentiation.
Vega 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:
- ``volatility``
- ``variance``
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:
Vega of a European option can be evaluated as follows.
>>> import pfhedge.autogreek as autogreek
>>> from pfhedge.nn import BSEuropeanOption
>>>
>>> pricer = BSEuropeanOption().price
>>> autogreek.vega(
... pricer,
... log_moneyness=torch.zeros(3),
... time_to_maturity=torch.ones(3),
... volatility=torch.tensor([0.18, 0.20, 0.22]),
... )
tensor([0.3973, 0.3970, 0.3965])
"""
volatility = parse_volatility(**params).requires_grad_()
params["volatility"] = volatility
params["variance"] = volatility.pow(2)

# 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)
return torch.autograd.grad(
price,
inputs=volatility,
grad_outputs=torch.ones_like(price),
create_graph=create_graph,
)[0]
1 change: 1 addition & 0 deletions pfhedge/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
from .features import Moneyness
from .features import PrevHedge
from .features import TimeToMaturity
from .features import Variance
from .features import Volatility
from .features import Zeros
2 changes: 2 additions & 0 deletions pfhedge/features/_getter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .features import Moneyness
from .features import PrevHedge
from .features import TimeToMaturity
from .features import Variance
from .features import Volatility
from .features import Zeros

Expand All @@ -23,6 +24,7 @@
MaxMoneyness,
Moneyness,
PrevHedge,
Variance,
Volatility,
Zeros,
]
Expand Down
11 changes: 11 additions & 0 deletions pfhedge/features/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ def __getitem__(self, time_step: Optional[int]) -> Tensor:
return self.derivative.ul().volatility[:, index].unsqueeze(-1)


class Variance(StateIndependentFeature):
"""Variance of the underlier of the derivative."""

def __str__(self) -> str:
return "variance"

def __getitem__(self, time_step: Optional[int]) -> Tensor:
index = [time_step] if isinstance(time_step, int) else ...
return self.derivative.ul().variance[:, index].unsqueeze(-1)


class PrevHedge(Feature):
"""Previous holding of underlier."""

Expand Down
4 changes: 2 additions & 2 deletions pfhedge/instruments/derivative/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
from pfhedge._utils.doc import _set_attr_and_docstring
from pfhedge._utils.doc import _set_docstring
from pfhedge._utils.str import _addindent
from pfhedge._utils.typing import TensorOrScalar

from ..base import Instrument
from ..primary.base import Primary

T = TypeVar("T", bound="Derivative")
TensorOrFloat = Union[Tensor, float]


class Derivative(Instrument):
Expand Down Expand Up @@ -60,7 +60,7 @@ def device(self) -> Optional[torch.device]:
return self.underlier.device

def simulate(
self, n_paths: int = 1, init_state: Optional[Tuple[TensorOrFloat, ...]] = None
self, n_paths: int = 1, init_state: Optional[Tuple[TensorOrScalar, ...]] = None
) -> None:
"""Simulate time series associated with the underlier.
Expand Down
6 changes: 3 additions & 3 deletions pfhedge/instruments/primary/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

from pfhedge._utils.doc import _set_attr_and_docstring
from pfhedge._utils.doc import _set_docstring
from pfhedge._utils.typing import TensorOrScalar

from ..base import Instrument

T = TypeVar("T", bound="Primary")
TensorOrFloat = Union[float, Tensor]


class Primary(Instrument):
Expand Down Expand Up @@ -52,7 +52,7 @@ def __init__(self) -> None:
self.register_buffer("spot", None)

@property
def default_init_state(self) -> Tuple[TensorOrFloat, ...]:
def default_init_state(self) -> Tuple[TensorOrScalar, ...]:
"""Returns the default initial state of simulation."""

# TODO(simaki): Remove @no_type_check once BrownianStock and HestonStock get
Expand All @@ -63,7 +63,7 @@ def simulate(
self,
n_paths: int,
time_horizon: float,
init_state: Optional[Tuple[TensorOrFloat, ...]] = None,
init_state: Optional[Tuple[TensorOrScalar, ...]] = None,
) -> None:
"""Simulate time series associated with the instrument and add them as buffers.
Expand Down
5 changes: 2 additions & 3 deletions pfhedge/instruments/primary/brownian.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
from pfhedge._utils.doc import _set_attr_and_docstring
from pfhedge._utils.doc import _set_docstring
from pfhedge._utils.str import _format_float
from pfhedge._utils.typing import TensorOrScalar
from pfhedge.stochastic import generate_geometric_brownian

from .base import Primary

TensorOrFloat = Union[Tensor, float]


class BrownianStock(Primary):
"""A stock of which spot prices follow the geometric Brownian motion.
Expand Down Expand Up @@ -104,7 +103,7 @@ def simulate(
self,
n_paths: int = 1,
time_horizon: float = 20 / 250,
init_state: Optional[Tuple[TensorOrFloat]] = None,
init_state: Optional[Tuple[TensorOrScalar]] = None,
) -> None:
"""Simulate the spot price and add it as a buffer named ``spot``.
Expand Down
5 changes: 2 additions & 3 deletions pfhedge/instruments/primary/cir.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@
from pfhedge._utils.doc import _set_attr_and_docstring
from pfhedge._utils.doc import _set_docstring
from pfhedge._utils.str import _format_float
from pfhedge._utils.typing import TensorOrScalar
from pfhedge.stochastic import generate_cir

from .base import Primary

TensorOrFloat = Union[Tensor, float]


class CIRRate(Primary):
"""A rate which follow the CIR process.
Expand Down Expand Up @@ -83,7 +82,7 @@ def simulate(
self,
n_paths: int = 1,
time_horizon: float = 20 / 250,
init_state: Optional[Tuple[TensorOrFloat, ...]] = None,
init_state: Optional[Tuple[TensorOrScalar, ...]] = None,
) -> None:
"""Simulate the spot rate and add it as a buffer named ``spot``.
Expand Down
5 changes: 2 additions & 3 deletions pfhedge/instruments/primary/heston.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@
from pfhedge._utils.doc import _set_attr_and_docstring
from pfhedge._utils.doc import _set_docstring
from pfhedge._utils.str import _format_float
from pfhedge._utils.typing import TensorOrScalar
from pfhedge.stochastic import generate_heston

from .base import Primary

TensorOrFloat = Union[Tensor, float]


class HestonStock(Primary):
"""A stock of which spot price and variance follow Heston process.
Expand Down Expand Up @@ -104,7 +103,7 @@ def simulate(
self,
n_paths: int = 1,
time_horizon: float = 20 / 250,
init_state: Optional[Tuple[TensorOrFloat, ...]] = None,
init_state: Optional[Tuple[TensorOrScalar, ...]] = None,
) -> None:
"""Simulate the spot price and add it as a buffer named ``spot``.
Expand Down
Loading

0 comments on commit 0207be5

Please sign in to comment.