From 45b2a81a150106525e877a540d2422bb4790b628 Mon Sep 17 00:00:00 2001 From: simaki Date: Tue, 17 Aug 2021 21:03:53 +0900 Subject: [PATCH] Release/0.9.0 (#215) * BUG: Fix `time_to_maturity` mismatch (close #211) (#214) * ENH: Add `VarianceSwap` (close #127) (#207) * DOC: Fix code blocks in docstrings (#199) * MAINT: Use `disable` in `tqdm` (#209) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: GitHub Actions --- README.md | 2 +- docs/source/instruments.rst | 1 + docs/source/nn.functional.rst | 2 + pfhedge/_utils/bisect.py | 4 +- pfhedge/_utils/doc.py | 2 +- pfhedge/_utils/hook.py | 2 +- pfhedge/_utils/lazy.py | 1 - pfhedge/autogreek.py | 19 ++-- pfhedge/features/_base.py | 2 +- pfhedge/features/_getter.py | 1 - pfhedge/features/features.py | 31 +++--- pfhedge/instruments/__init__.py | 1 + pfhedge/instruments/base.py | 24 ++--- .../instruments/derivative/american_binary.py | 10 +- pfhedge/instruments/derivative/base.py | 33 +++---- pfhedge/instruments/derivative/european.py | 14 +-- .../instruments/derivative/european_binary.py | 10 +- pfhedge/instruments/derivative/lookback.py | 12 +-- .../instruments/derivative/variance_swap.py | 97 +++++++++++++++++++ pfhedge/instruments/primary/base.py | 8 +- pfhedge/instruments/primary/brownian.py | 29 +++--- pfhedge/instruments/primary/heston.py | 32 +++--- pfhedge/nn/functional.py | 75 +++++++++++--- pfhedge/nn/modules/bs/_base.py | 5 +- pfhedge/nn/modules/bs/american_binary.py | 4 +- pfhedge/nn/modules/bs/black_scholes.py | 6 +- pfhedge/nn/modules/bs/european.py | 4 +- pfhedge/nn/modules/bs/european_binary.py | 4 +- pfhedge/nn/modules/bs/lookback.py | 4 +- pfhedge/nn/modules/clamp.py | 8 +- pfhedge/nn/modules/hedger.py | 32 +++--- pfhedge/nn/modules/loss.py | 9 +- pfhedge/nn/modules/mlp.py | 14 +-- pfhedge/nn/modules/naked.py | 2 +- pfhedge/nn/modules/ww.py | 7 +- pfhedge/stochastic/brownian.py | 29 +++--- pfhedge/stochastic/cir.py | 13 +-- pfhedge/stochastic/heston.py | 12 +-- pyproject.toml | 2 +- tests/features/test_features.py | 34 +++++-- tests/instruments/derivative/test_european.py | 45 +++++++-- .../derivative/test_variance_swap.py | 41 ++++++++ tests/instruments/primary/test_brownian.py | 9 ++ tests/nn/bs/test_european_binary.py | 2 +- tests/nn/modules/test_ww.py | 21 ++++ tests/nn/test_functional.py | 30 ++++++ 46 files changed, 527 insertions(+), 222 deletions(-) create mode 100644 pfhedge/instruments/derivative/variance_swap.py create mode 100644 tests/instruments/derivative/test_variance_swap.py diff --git a/README.md b/README.md index 73844118..5d8f522f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ $ pip install pfhedge Financial instruments are provided in [`pfhedge.instruments`](https://pfnet-research.github.io/pfhedge/instruments.html) and classified into two types: * **`Primary` instruments**: A primary instrument is a basic financial instrument that is traded on a market, and therefore their prices are accessible as the market prices. Examples include stocks, bonds, commodities, and currencies. -* **`Derivative` instruments**: A derivative is a financial instrument whose payoff is contingent on a primary instrument. An (over-the-counter) derivative is not traded on the market, and therefore the price is not directly accessible. Examples include [`EuropeanOption`](https://en.wikipedia.org/wiki/Option_style#American_and_European_options), [`LookbackOption`](https://en.wikipedia.org/wiki/Lookback_option), and so forth. +* **`Derivative` instruments**: A derivative is a financial instrument whose payoff is contingent on a primary instrument. An (over-the-counter) derivative is not traded on the market, and therefore the price is not directly accessible. Examples include [`EuropeanOption`](https://en.wikipedia.org/wiki/Option_style#American_and_European_options), [`LookbackOption`](https://en.wikipedia.org/wiki/Lookback_option), [`VarianceSwap`](https://en.wikipedia.org/wiki/Variance_swap), and so forth. We consider a [`BrownianStock`](https://pfnet-research.github.io/pfhedge/generated/pfhedge.instruments.BrownianStock.html), which is a stock following the [geometric Brownian motion](https://en.wikipedia.org/wiki/Geometric_Brownian_motion), and a [`EuropeanOption`](https://pfnet-research.github.io/pfhedge/generated/pfhedge.instruments.EuropeanOption.html) which is contingent on it. We assume that the stock has a transaction cost of 1 basis point. diff --git a/docs/source/instruments.rst b/docs/source/instruments.rst index 3a512be8..276a0d67 100644 --- a/docs/source/instruments.rst +++ b/docs/source/instruments.rst @@ -31,3 +31,4 @@ Derivative Instruments instruments.LookbackOption instruments.EuropeanBinaryOption instruments.AmericanBinaryOption + instruments.VarianceSwap diff --git a/docs/source/nn.functional.rst b/docs/source/nn.functional.rst index 3633bc9e..a877e3e9 100644 --- a/docs/source/nn.functional.rst +++ b/docs/source/nn.functional.rst @@ -27,3 +27,5 @@ Other Functions .. autofunction:: leaky_clamp .. autofunction:: clamp .. autofunction:: topp +.. autofunction:: realized_variance +.. autofunction:: realized_volatility diff --git a/pfhedge/_utils/bisect.py b/pfhedge/_utils/bisect.py index ae5f2510..4b206ec5 100644 --- a/pfhedge/_utils/bisect.py +++ b/pfhedge/_utils/bisect.py @@ -35,7 +35,7 @@ def bisect( torch.Tensor Raises: - RuntimeError: If the number of iteration exceeds `max_iter`. + RuntimeError: If the number of iteration exceeds ``max_iter``. Examples: @@ -62,7 +62,7 @@ def bisect( raise ValueError("condition lower < upper should be satisfied.") if (function(lower) > function(upper)).all(): - # If `function` is a decreasing function + # If function is a decreasing function mf = lambda input: -function(input) return bisect(mf, -target, lower, upper, precision=precision, max_iter=max_iter) diff --git a/pfhedge/_utils/doc.py b/pfhedge/_utils/doc.py index 7b21e32b..1a373c9b 100644 --- a/pfhedge/_utils/doc.py +++ b/pfhedge/_utils/doc.py @@ -1,5 +1,5 @@ def set_docstring(object: object, name: str, value: object) -> None: - # so that `object.name.__doc__ == value.__doc__` + # so that object.name.__doc__ == value.__doc__ setattr(getattr(object, name), "__doc__", value.__doc__) diff --git a/pfhedge/_utils/hook.py b/pfhedge/_utils/hook.py index c3606598..0eb1c244 100644 --- a/pfhedge/_utils/hook.py +++ b/pfhedge/_utils/hook.py @@ -7,7 +7,7 @@ def save_prev_output( module: Module, input: Optional[Tensor], output: Optional[Tensor] ) -> None: - """A hook to save previous output as a buffer named `prev_output`. + """A hook to save previous output as a buffer named ``prev_output``. Examples: diff --git a/pfhedge/_utils/lazy.py b/pfhedge/_utils/lazy.py index a9b13ad4..ac80f21f 100644 --- a/pfhedge/_utils/lazy.py +++ b/pfhedge/_utils/lazy.py @@ -3,5 +3,4 @@ def has_lazy(module: Module) -> bool: - """Returns `True` if a module has any `UninitializedParameter`.""" return any(map(is_lazy, module.parameters())) diff --git a/pfhedge/autogreek.py b/pfhedge/autogreek.py index 43f7c4c5..3a6c018d 100644 --- a/pfhedge/autogreek.py +++ b/pfhedge/autogreek.py @@ -17,9 +17,10 @@ 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. - **kwargs: Parameters passed to `pricer`. + create_graph (bool, default=False): If ``True``, graph of the derivative + will be constructed, allowing to compute higher order derivative + products. + **kwargs: Parameters passed to ``pricer``. Returns: torch.Tensor @@ -58,9 +59,9 @@ def delta( >>> >>> _ = torch.manual_seed(42) >>> - >>> derivative = EuropeanOption(BrownianStock(cost=1e-4)) + >>> derivative = EuropeanOption(BrownianStock(cost=1e-4)).to(torch.float64) >>> model = WhalleyWilmott(derivative) - >>> hedger = Hedger(model, model.inputs()) + >>> hedger = Hedger(model, model.inputs()).to(torch.float64) >>> >>> def pricer(spot): ... return hedger.price( @@ -68,7 +69,7 @@ def delta( ... ) >>> >>> autogreek.delta(pricer, spot=torch.tensor(1.0)) - tensor(0.52...) + tensor(0.5...) """ if kwargs.get("strike") is None and kwargs.get("spot") is None: # Since delta does not depend on strike, @@ -81,7 +82,7 @@ def delta( kwargs["moneyness"] = spot / kwargs["strike"] kwargs["log_moneyness"] = (spot / kwargs["strike"]).log() - # Delete parameters that are not in the signature of `pricer` to avoid + # Delete parameters that are not in the signature of pricer to avoid # TypeError: got an unexpected keyword argument '' for parameter in list(kwargs.keys()): if parameter not in signature(pricer).parameters.keys(): @@ -107,9 +108,9 @@ def gamma( Args: pricer (callable): Pricing formula of a derivative. - create_graph (bool, default=False): If `True`, graph of the derivative will be + create_graph (bool, default=False): If ``True``, graph of the derivative will be constructed, allowing to compute higher order derivative products. - **kwargs: Parameters passed to `pricer`. + **kwargs: Parameters passed to ``pricer``. Returns: torch.Tensor diff --git a/pfhedge/features/_base.py b/pfhedge/features/_base.py index 283143d3..5e73c837 100644 --- a/pfhedge/features/_base.py +++ b/pfhedge/features/_base.py @@ -28,7 +28,7 @@ def __getitem__(self, i: int) -> Tensor: """ def of(self: T, derivative=None, hedger=None) -> T: - """Set `derivative` and `hedger` to the attributes of `self`. + """Set ``derivative`` and ``hedger`` to the attributes of ``self``. Args: derivative (Derivative, optional): The derivative to compute features. diff --git a/pfhedge/features/_getter.py b/pfhedge/features/_getter.py index 08483815..89204a8f 100644 --- a/pfhedge/features/_getter.py +++ b/pfhedge/features/_getter.py @@ -45,5 +45,4 @@ def get_feature(feature: Union[str, Feature]) -> Feature: feature = dict_features[feature] elif not isinstance(feature, Feature): raise TypeError(f"{feature} is not an instance of Feature.") - # If `feature` is Feature object, pass it through. return feature diff --git a/pfhedge/features/features.py b/pfhedge/features/features.py index 46e38bab..6e7f4e36 100644 --- a/pfhedge/features/features.py +++ b/pfhedge/features/features.py @@ -18,7 +18,7 @@ class Moneyness(Feature): """Moneyness of the underlying instrument of the derivative. Args: - log (bool, default=False): If `True`, represents log moneyness. + log (bool, default=False): If ``True``, represents log moneyness. """ def __init__(self, log: bool = False) -> None: @@ -30,9 +30,9 @@ def __str__(self) -> str: def __getitem__(self, i: int) -> Tensor: if self.log: - return self.derivative.log_moneyness(i).unsqueeze(-1) + return self.derivative.log_moneyness(i) else: - return self.derivative.moneyness(i).unsqueeze(-1) + return self.derivative.moneyness(i) class LogMoneyness(Moneyness): @@ -49,7 +49,7 @@ def __str__(self) -> str: return "expiry_time" def __getitem__(self, i: int) -> Tensor: - return self.derivative.time_to_maturity(i).unsqueeze(-1) + return self.derivative.time_to_maturity(i) class Volatility(Feature): @@ -74,14 +74,14 @@ def __getitem__(self, i: int) -> Tensor: class Barrier(Feature): """A feature which signifies whether the price of the underlier have reached - the barrier. The output `1.0` means that the price have touched the barrier, - and `0` otherwise. + the barrier. The output 1.0 means that the price have touched the barrier, + and 0.0 otherwise. Args: threshold (float): The price level of the barrier. - up (bool, default True): If `True`, signifies whether the price has exceeded + up (bool, default True): If ``True``, signifies whether the price has exceeded the barrier upward. - If `False`, signifies whether the price has exceeded the barrier downward. + If ``False``, signifies whether the price has exceeded the barrier downward. """ def __init__(self, threshold: float, up: bool = True) -> None: @@ -122,7 +122,7 @@ class MaxMoneyness(Feature): """Cumulative maximum of moneyness. Args: - log (bool, default=False): If `True`, represents log moneyness. + log (bool, default=False): If ``True``, represents log moneyness. """ def __init__(self, log: bool = False) -> None: @@ -147,15 +147,16 @@ def __init__(self) -> None: class ModuleOutput(Feature, Module): - """The feature computed as an output of a `torch.nn.Module`. + """The feature computed as an output of a ``torch.nn.Module``. Args: module (torch.nn.Module): Module to compute the value of the feature. - The input and output shapes should be `(N, *, H_in) -> (N, *, 1)`, - where `N` stands for the number of Monte Carlo paths of the underlier of - the derivative, `H_in` stands for the number of input features - (namely, `H_in = len(inputs)`), - and `*` means any number of additional dimensions. + The input and output shapes should be :math:`(N, *, H_in) -> (N, *, 1)`, + where :math:`N` stands for the number of Monte Carlo paths of + the underlier of the derivative, + :math:`H_in` stands for the number of input features + (namely, ``len(inputs)``), + and :math:`*` means any number of additional dimensions. inputs (list[Feature]): The input features to the module. Examples: diff --git a/pfhedge/instruments/__init__.py b/pfhedge/instruments/__init__.py index 4be750ef..0c436967 100644 --- a/pfhedge/instruments/__init__.py +++ b/pfhedge/instruments/__init__.py @@ -3,6 +3,7 @@ from .derivative.european import EuropeanOption from .derivative.european_binary import EuropeanBinaryOption from .derivative.lookback import LookbackOption +from .derivative.variance_swap import VarianceSwap from .primary.base import Primary from .primary.brownian import BrownianStock from .primary.heston import HestonStock diff --git a/pfhedge/instruments/base.py b/pfhedge/instruments/base.py index af072a99..8c38bcc4 100644 --- a/pfhedge/instruments/base.py +++ b/pfhedge/instruments/base.py @@ -33,8 +33,8 @@ def to(self: T, *args, **kwargs) -> T: """Performs dtype and/or device conversion of the buffers associated to the instument. - A `torch.dtype` and `torch.device` are inferred from the arguments of - `self.to(*args, **kwargs)`. + A ``torch.dtype`` and ``torch.device`` are inferred from the arguments of + ``self.to(*args, **kwargs)``. Args: dtype (torch.dtype): Desired floating point type of the floating point @@ -69,43 +69,43 @@ def cuda(self: T, device: Optional[int] = None) -> T: ) def double(self: T) -> T: - """`self.double()` is equivalent to `self.to(torch.float64)`. + """It is equivalent to ``self.to(torch.float64)``. See :func:`to()`. """ return self.to(torch.float64) def float(self: T) -> T: - """`self.float()` is equivalent to `self.to(torch.float32)`. + """It is equivalent to ``self.to(torch.float32)``. See :func:`to()`. """ return self.to(torch.float32) def half(self: T) -> T: - """`self.half()` is equivalent to `self.to(torch.float16)`. + """It is equivalent to ``self.to(torch.float16)``. See :func:`to()`. """ return self.to(torch.float16) def bfloat16(self: T) -> T: - """`self.bfloat16()` is equivalent to `self.to(torch.bfloat16)`. + """It is equivalent to ``self.to(torch.bfloat16)``. See :func:`to()`. """ return self.to(torch.bfloat16) @property def dinfo(self) -> List[str]: - """Returns list of strings that tell `dtype` and `device` of `self`. + """Returns list of strings that tell ``dtype`` and ``device`` of self. - Intended to be used in `__repr__`. + Intended to be used in :func:`__repr__`. - If `dtype` (`device`) is the one specified in default type, - `dinfo` will not have the information of it. + If ``dtype`` (``device``) is the one specified in default type, + ``dinfo`` will not have the information of it. Returns: list[str] """ - # Implementation here refers to the function `_str_intern` in - # `pytorch/_tensor_str.py`. + # Implementation here refers to the function _str_intern in + # pytorch/_tensor_str.py. dinfo = [] diff --git a/pfhedge/instruments/derivative/american_binary.py b/pfhedge/instruments/derivative/american_binary.py index 80b622fa..a3de9f77 100644 --- a/pfhedge/instruments/derivative/american_binary.py +++ b/pfhedge/instruments/derivative/american_binary.py @@ -55,11 +55,11 @@ class AmericanBinaryOption(Derivative, OptionMixin): maturity (float, default=20/250): The maturity of the option. dtype (torch.device, optional): Desired device of returned tensor. Default: If None, uses a global default - (see `torch.set_default_tensor_type()`). + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): Desired device of returned tensor. Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Attributes: @@ -78,8 +78,8 @@ class AmericanBinaryOption(Derivative, OptionMixin): maturity=5/250, strike=1.01) >>> derivative.simulate(n_paths=2) >>> derivative.underlier.spot - tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930], - [1.0000, 1.0282, 1.0199, 1.0258, 1.0292]]) + tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930, 0.9906], + [1.0000, 0.9919, 0.9976, 1.0009, 1.0076, 1.0179]]) >>> derivative.payoff() tensor([0., 1.]) """ diff --git a/pfhedge/instruments/derivative/base.py b/pfhedge/instruments/derivative/base.py index cca85ba2..c7bf7683 100644 --- a/pfhedge/instruments/derivative/base.py +++ b/pfhedge/instruments/derivative/base.py @@ -57,7 +57,7 @@ def simulate( n_paths (int): The number of paths to simulate. init_state (tuple[torch.Tensor | float], optional): The initial state of the underlier. - **kwargs: Other parameters passed to `self.underlier.simulate()`. + **kwargs: Other parameters passed to ``self.underlier.simulate()``. """ self.underlier.simulate( n_paths=n_paths, time_horizon=self.maturity, init_state=init_state @@ -96,34 +96,32 @@ def moneyness(self, time_step: Optional[int] = None) -> Tensor: Args: time_step (int, optional): The time step to calculate - the moneyness. If `None` (default), the moneyness is calculated + the moneyness. If ``None`` (default), the moneyness is calculated at all time steps. Shape: - Output: :math:`(N, T)` where :math:`N` is the number of paths and :math:`T` is the number of time steps. - If `time_step` is given, the shape is :math:`(N, 1)`. + If ``time_step`` is given, the shape is :math:`(N, 1)`. Returns: torch.Tensor """ - spot = self.underlier.spot - if time_step is not None: - spot = spot[..., time_step] - return spot / self.strike + index = ... if time_step is None else [time_step] + return self.underlier.spot[..., index] / self.strike def log_moneyness(self, time_step: Optional[int] = None) -> Tensor: """Returns the log moneyness of self. Args: time_step (int, optional): The time step to calculate the log - moneyness. If `None` (default), the moneyness is calculated + moneyness. If ``None`` (default), the moneyness is calculated at all time steps. Shape: - Output: :math:`(N, T)` where :math:`N` is the number of paths and :math:`T` is the number of time steps. - If `time_step` is given, the shape is :math:`(N, 1)`. + If ``time_step`` is given, the shape is :math:`(N, 1)`. Returns: torch.Tensor @@ -135,27 +133,26 @@ def time_to_maturity(self, time_step: Optional[int] = None) -> Tensor: Args: time_step (int, optional): The time step to calculate - the time to maturity. If `None` (default), the time to + the time to maturity. If ``None`` (default), the time to maturity is calculated at all time steps. Shape: - Output: :math:`(N, T)` where :math:`N` is the number of paths and :math:`T` is the number of time steps. - If `time_step` is given, the shape is :math:`(N, 1)`. + If ``time_step`` is given, the shape is :math:`(N, 1)`. Returns: torch.Tensor """ n_paths, n_steps = self.underlier.spot.size() if time_step is None: - t = self.underlier.dt * torch.arange(n_steps).repeat(n_paths, 1) - t.to(self.underlier.device) - return self.maturity - t + # Time passed from the beginning + t = torch.arange(n_steps).to(self.underlier.spot) * self.underlier.dt + return (t[-1] - t).unsqueeze(0).expand(n_paths, -1) else: - t = torch.full_like( - self.underlier.spot[:, 0], time_step * self.underlier.dt - ) - return self.maturity - t + time = n_steps - (time_step % n_steps) - 1 + t = torch.tensor([[time]]).to(self.underlier.spot) * self.underlier.dt + return t.expand(n_paths, -1) # Assign docstrings so they appear in Sphinx documentation diff --git a/pfhedge/instruments/derivative/european.py b/pfhedge/instruments/derivative/european.py index 3b583f31..e0837282 100644 --- a/pfhedge/instruments/derivative/european.py +++ b/pfhedge/instruments/derivative/european.py @@ -41,11 +41,11 @@ class EuropeanOption(Derivative, OptionMixin): maturity (float, default=20/250): The maturity of the option. dtype (torch.dtype, optional): Desired device of returned tensor. Default: If None, uses a global default - (see `torch.set_default_tensor_type()`). + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): Desired device of returned tensor. Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Attributes: @@ -63,12 +63,12 @@ class EuropeanOption(Derivative, OptionMixin): >>> derivative = EuropeanOption(BrownianStock(), maturity=5/250) >>> derivative.simulate(n_paths=2) >>> derivative.underlier.spot - tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930], - [1.0000, 1.0282, 1.0199, 1.0258, 1.0292]]) + tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930, 0.9906], + [1.0000, 0.9919, 0.9976, 1.0009, 1.0076, 1.0179]]) >>> derivative.payoff() - tensor([0.0000, 0.0292]) + tensor([0.0000, 0.0179]) - Using custom `dtype` and `device`. + Using custom ``dtype`` and ``device``. >>> derivative = EuropeanOption(BrownianStock()) >>> derivative.to(dtype=torch.float64, device="cuda:0") diff --git a/pfhedge/instruments/derivative/european_binary.py b/pfhedge/instruments/derivative/european_binary.py index 0b8b9e5b..7dfdfbb9 100644 --- a/pfhedge/instruments/derivative/european_binary.py +++ b/pfhedge/instruments/derivative/european_binary.py @@ -51,11 +51,11 @@ class EuropeanBinaryOption(Derivative, OptionMixin): maturity (float, default=20/250) The maturity of the option. dtype (torch.dtype, optional): Desired device of returned tensor. Default: If None, uses a global default - (see `torch.set_default_tensor_type()`). + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): Desired device of returned tensor. Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Attributes: @@ -73,8 +73,8 @@ class EuropeanBinaryOption(Derivative, OptionMixin): >>> derivative = EuropeanBinaryOption(BrownianStock(), maturity=5/250) >>> derivative.simulate(n_paths=2) >>> derivative.underlier.spot - tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930], - [1.0000, 1.0282, 1.0199, 1.0258, 1.0292]]) + tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930, 0.9906], + [1.0000, 0.9919, 0.9976, 1.0009, 1.0076, 1.0179]]) >>> derivative.payoff() tensor([0., 1.]) """ diff --git a/pfhedge/instruments/derivative/lookback.py b/pfhedge/instruments/derivative/lookback.py index 915eef98..00cb14cb 100644 --- a/pfhedge/instruments/derivative/lookback.py +++ b/pfhedge/instruments/derivative/lookback.py @@ -45,11 +45,11 @@ class LookbackOption(Derivative, OptionMixin): maturity (float, default=20/250): The maturity of the option. dtype (torch.dtype, optional): Desired device of returned tensor. Default: If None, uses a global default - (see `torch.set_default_tensor_type()`). + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): Desired device of returned tensor. Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Attributes: @@ -67,10 +67,10 @@ class LookbackOption(Derivative, OptionMixin): >>> derivative = LookbackOption(BrownianStock(), maturity=5/250) >>> derivative.simulate(n_paths=2) >>> derivative.underlier.spot - tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930], - [1.0000, 1.0282, 1.0199, 1.0258, 1.0292]]) + tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930, 0.9906], + [1.0000, 0.9919, 0.9976, 1.0009, 1.0076, 1.0179]]) >>> derivative.payoff() - tensor([0.0073, 0.0292]) + tensor([0.0073, 0.0179]) """ def __init__( diff --git a/pfhedge/instruments/derivative/variance_swap.py b/pfhedge/instruments/derivative/variance_swap.py new file mode 100644 index 00000000..ff0ca22a --- /dev/null +++ b/pfhedge/instruments/derivative/variance_swap.py @@ -0,0 +1,97 @@ +from typing import Optional + +import torch +from torch import Tensor + +from pfhedge._utils.doc import set_attr_and_docstring +from pfhedge._utils.doc import set_docstring +from pfhedge.nn.functional import realized_variance + +from ..primary.base import Primary +from .base import Derivative + + +class VarianceSwap(Derivative): + """A variance swap. + + A variance swap pays cash in the amount of the realized variance + until the maturity and levies the cash of the strike variance. + + The payoff of a variance swap is given by + + .. math :: + + \\mathrm{payoff} = \\sigma^2 - K + + where :math:`\\sigma^2` is the realized variance of the underlying asset + until maturity and :math:`K` is the strike variance (``strike``). + See :func:`pfhedge.nn.functional.realized_variance` for the definition of + the realized variance. + + Args: + underlier (:class:`Primary`): The underlying instrument. + strike (float, default=0.04): The strike variance of the swap. + maturity (float, default=20/250): The maturity of the derivative. + dtype (torch.device, optional): Desired device of returned tensor. + Default: If None, uses a global default + (see ``torch.set_default_tensor_type()``). + device (torch.device, optional): Desired device of returned tensor. + Default: if None, uses the current device for the default tensor type + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and + the current CUDA device for CUDA tensor types. + + Attributes: + dtype (torch.dtype): The dtype with which the simulated time-series are + represented. + device (torch.device): The device where the simulated time-series are. + + Examples: + + >>> import torch + >>> from pfhedge.nn.functional import realized_variance + >>> from pfhedge.instruments import BrownianStock + >>> from pfhedge.instruments import VarianceSwap + >>> + >>> _ = torch.manual_seed(42) + >>> derivative = VarianceSwap(BrownianStock(), strike=0.04, maturity=5/250) + >>> derivative.simulate(n_paths=2) + >>> derivative.underlier.spot + tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930, 0.9906], + [1.0000, 0.9919, 0.9976, 1.0009, 1.0076, 1.0179]]) + >>> realized_variance(derivative.ul().spot, dt=derivative.ul().dt) + tensor([0.0114, 0.0129]) + >>> derivative.payoff() + tensor([-0.0286, -0.0271]) + """ + + def __init__( + self, + underlier: Primary, + strike: float = 0.04, + maturity: float = 20 / 250, + dtype: Optional[torch.dtype] = None, + device: Optional[torch.device] = None, + ) -> None: + super().__init__() + self.underlier = underlier + self.strike = strike + self.maturity = maturity + self.to(dtype=dtype, device=device) + + def __repr__(self): + params = [f"{self.underlier.__class__.__name__}(...)"] + params.append(f"strike={self.strike:.2e}") + params.append(f"maturity={self.maturity:.2e}") + params += self.dinfo + return self.__class__.__name__ + "(" + ", ".join(params) + ")" + + def payoff(self) -> Tensor: + return realized_variance(self.ul().spot, dt=self.ul().dt) - self.strike + + +# Assign docstrings so they appear in Sphinx documentation +set_attr_and_docstring(VarianceSwap, "simulate", Derivative.simulate) +set_attr_and_docstring(VarianceSwap, "to", Derivative.to) +set_attr_and_docstring(VarianceSwap, "ul", Derivative.ul) +set_docstring(VarianceSwap, "payoff", Derivative.payoff) diff --git a/pfhedge/instruments/primary/base.py b/pfhedge/instruments/primary/base.py index 0af7b139..c75e027c 100644 --- a/pfhedge/instruments/primary/base.py +++ b/pfhedge/instruments/primary/base.py @@ -62,12 +62,16 @@ def simulate( ) -> None: """Simulate time series associated with the instrument and add them as buffers. + The shapes of the registered buffers should be ``(n_paths, n_steps)`` + where ``n_steps`` is the minimum integer that satisfies + ``n_steps * self.dt >= time_horizon``. + Args: n_paths (int): The number of paths to simulate. time_horizon (float): The period of time to simulate the price. init_state (tuple[torch.Tensor | float], optional): The initial state of the instrument. - If `None` (default), it uses the default value + If ``None`` (default), it uses the default value (See :func:`default_init_state`). """ @@ -84,7 +88,7 @@ def register_buffer(self, name: str, tensor: Tensor) -> None: If ``None``, the buffer is **not** included in the module's :attr:`state_dict`. """ - # Implementation here refers to `torch.nn.Module.register_buffer`. + # Implementation here refers to torch.nn.Module.register_buffer. if "_buffers" not in self.__dict__: raise AttributeError("cannot assign buffer before Primary.__init__() call") elif not isinstance(name, torch._six.string_classes): diff --git a/pfhedge/instruments/primary/brownian.py b/pfhedge/instruments/primary/brownian.py index 8ea50591..86f2a162 100644 --- a/pfhedge/instruments/primary/brownian.py +++ b/pfhedge/instruments/primary/brownian.py @@ -1,3 +1,4 @@ +from math import ceil from typing import Optional from typing import Tuple from typing import Union @@ -29,11 +30,11 @@ class BrownianStock(Primary): dt (float, default=1/250): The intervals of the time steps. dtype (torch.device, optional): Desired device of returned tensor. Default: If None, uses a global default - (see `torch.set_default_tensor_type()`). + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): Desired device of returned tensor. Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Buffers: @@ -51,10 +52,10 @@ class BrownianStock(Primary): >>> stock = BrownianStock() >>> stock.simulate(n_paths=2, time_horizon=5 / 250) >>> stock.spot - tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930], - [1.0000, 1.0282, 1.0199, 1.0258, 1.0292]]) + tensor([[1.0000, 1.0016, 1.0044, 1.0073, 0.9930, 0.9906], + [1.0000, 0.9919, 0.9976, 1.0009, 1.0076, 1.0179]]) - Using custom `dtype` and `device`. + Using custom ``dtype`` and ``device``. >>> stock = BrownianStock() >>> stock.to(dtype=torch.float64, device="cuda:0") @@ -87,11 +88,11 @@ def simulate( time_horizon: float = 20 / 250, init_state: Optional[Tuple[TensorOrFloat]] = None, ) -> None: - """Simulate the spot price and add it as a buffer named `spot`. + """Simulate the spot price and add it as a buffer named ``spot``. The shape of the spot is :math:`(N, T)`, where :math:`N` is the number of simulated paths and :math:`T` is the number of time steps. - The number of time steps is determinded from `dt` and `time_horizon`. + The number of time steps is determinded from ``dt`` and ``time_horizon``. Args: n_paths (int, default=1): The number of paths to simulate. @@ -99,11 +100,11 @@ def simulate( the price. init_state (tuple[torch.Tensor | float], optional): The initial state of the instrument. - This is specified by `(spot,)`, where `spot` is the initial value + This is specified by ``(spot,)``, where ``spot`` is the initial value of the stock price. - If `None` (default), it uses the default value + If ``None`` (default), it uses the default value (See :func:`default_init_state`). - It also accepts a float or a `torch.Tensor`. + It also accepts a ``float`` or a ``torch.Tensor``. Examples: @@ -111,15 +112,15 @@ def simulate( >>> stock = BrownianStock() >>> stock.simulate(n_paths=2, time_horizon=5 / 250, init_state=(2.0,)) >>> stock.spot - tensor([[2.0000, 2.0031, 2.0089, 2.0146, 1.9860], - [2.0000, 2.0565, 2.0398, 2.0516, 2.0584]]) + tensor([[2.0000, 2.0031, 2.0089, 2.0146, 1.9860, 1.9812], + [2.0000, 1.9838, 1.9952, 2.0018, 2.0153, 2.0358]]) """ if init_state is None: init_state = cast(Tuple[float], self.default_init_state) spot = generate_geometric_brownian( n_paths=n_paths, - n_steps=int(time_horizon / self.dt), + n_steps=ceil(time_horizon / self.dt + 1), init_state=init_state, volatility=self.volatility, dt=self.dt, diff --git a/pfhedge/instruments/primary/heston.py b/pfhedge/instruments/primary/heston.py index f790d29e..9eba38b3 100644 --- a/pfhedge/instruments/primary/heston.py +++ b/pfhedge/instruments/primary/heston.py @@ -28,11 +28,11 @@ class HestonStock(Primary): dt (float, default=1/250): The intervals of the time steps. dtype (torch.device, optional): Desired device of returned tensor. Default: If None, uses a global default - (see `torch.set_default_tensor_type()`). + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): Desired device of returned tensor. Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Buffers: @@ -45,7 +45,6 @@ class HestonStock(Primary): Note that this is different from the realized variance of the spot price. This attribute is set by a method :func:`simulate()`. The shape is :math:`(N, T)`. - - ``volatility`` (``torch.Tensor``): An alias for ``self.variance.sqrt()``. Examples: @@ -55,14 +54,14 @@ class HestonStock(Primary): >>> stock = HestonStock() >>> stock.simulate(n_paths=2, time_horizon=5/250) >>> stock.spot - tensor([[1.0000, 0.9941, 0.9905, 0.9846, 0.9706], - [1.0000, 1.0031, 0.9800, 0.9785, 0.9735]]) + tensor([[1.0000, 0.9902, 0.9823, 0.9926, 0.9968, 1.0040], + [1.0000, 0.9826, 0.9891, 0.9898, 0.9851, 0.9796]]) >>> stock.variance - tensor([[0.0400, 0.0408, 0.0411, 0.0417, 0.0422], - [0.0400, 0.0395, 0.0452, 0.0434, 0.0446]]) + tensor([[0.0400, 0.0408, 0.0411, 0.0417, 0.0422, 0.0393], + [0.0400, 0.0457, 0.0440, 0.0451, 0.0458, 0.0472]]) >>> stock.volatility - tensor([[0.2000, 0.2020, 0.2027, 0.2041, 0.2054], - [0.2000, 0.1987, 0.2126, 0.2084, 0.2112]]) + tensor([[0.2000, 0.2020, 0.2027, 0.2041, 0.2054, 0.1982], + [0.2000, 0.2138, 0.2097, 0.2124, 0.2140, 0.2172]]) """ spot: Tensor @@ -96,6 +95,7 @@ def default_init_state(self) -> Tuple[float, ...]: @property def volatility(self) -> Tensor: + """An alias for ``self.variance.sqrt()``.""" return self.variance.clamp(min=0.0).sqrt() def simulate( @@ -104,11 +104,11 @@ def simulate( time_horizon: float = 20 / 250, init_state: Optional[Tuple[TensorOrFloat, ...]] = None, ) -> None: - """Simulate the spot price and add it as a buffer named `spot`. + """Simulate the spot price and add it as a buffer named ``spot``. The shape of the spot is :math:`(N, T)`, where :math:`N` is the number of simulated paths and :math:`T` is the number of time steps. - The number of time steps is determinded from `dt` and `time_horizon`. + The number of time steps is determinded from ``dt`` and ``time_horizon``. Args: n_paths (int, default=1): The number of paths to simulate. @@ -116,9 +116,9 @@ def simulate( the price. init_state (tuple[torch.Tensor | float], default=(1.0,)): The initial state of the instrument. - This is specified by `(S0, V0)`, where `S0` and `V0` are the initial - values of of spot and variance, respectively. - If `None` (default), it uses the default value + This is specified by ``(S0, V0)``, where ``S0`` and ``V0`` are + the initial values of of spot and variance, respectively. + If ``None`` (default), it uses the default value (See :func:`default_init_state`). """ if init_state is None: @@ -126,7 +126,7 @@ def simulate( spot, variance = generate_heston( n_paths=n_paths, - n_steps=int(time_horizon / self.dt), + n_steps=int(time_horizon / self.dt + 1), init_state=init_state, kappa=self.kappa, theta=self.theta, diff --git a/pfhedge/nn/functional.py b/pfhedge/nn/functional.py index 66e7112e..e65c4c67 100644 --- a/pfhedge/nn/functional.py +++ b/pfhedge/nn/functional.py @@ -1,5 +1,6 @@ from math import ceil from typing import Optional +from typing import Union import torch import torch.nn.functional as fn @@ -15,7 +16,7 @@ def european_payoff(input: Tensor, call: bool = True, strike: float = 1.0) -> Te strike (float, default=1.0): The strike price of the option. Shape: - - input: :math:`(*, T)`, where, :math:`T` stands for the number of time steps + - input: :math:`(*, T)` where :math:`T` stands for the number of time steps and :math:`*` means any number of additional dimensions. - output: :math:`(*)` @@ -37,7 +38,7 @@ def lookback_payoff(input: Tensor, call: bool = True, strike: float = 1.0) -> Te strike (float, default=1.0): The strike price of the option. Shape: - - input: :math:`(*, T)`, where, :math:`T` stands for the number of time steps + - input: :math:`(*, T)` where :math:`T` stands for the number of time steps and :math:`*` means any number of additional dimensions. - output: :math:`(*)` @@ -61,7 +62,7 @@ def american_binary_payoff( strike (float, default=1.0): The strike price of the option. Shape: - - input: :math:`(*, T)`, where, :math:`T` stands for the number of time steps + - input: :math:`(*, T)` where :math:`T` stands for the number of time steps and :math:`*` means any number of additional dimensions. - output: :math:`(*)` @@ -85,7 +86,7 @@ def european_binary_payoff( strike (float, default=1.0): The strike price of the option. Shape: - - input: :math:`(*, T)`, where, :math:`T` stands for the number of time steps + - input: :math:`(*, T)` where :math:`T` stands for the number of time steps and :math:`*` means any number of additional dimensions. - output: :math:`(*)` @@ -145,15 +146,15 @@ def isoelastic_utility(input: Tensor, a: float) -> Tensor: def topp(input: Tensor, p: float, dim: Optional[int] = None, largest: bool = True): - """Returns the largest `p * N` elements of the given input tensor, - where `N` stands for the total number of elements in the input tensor. + """Returns the largest :math:`p * N` elements of the given input tensor, + where :math:`N` stands for the total number of elements in the input tensor. - If `dim` is not given, the last dimension of the `input` is chosen. + If ``dim`` is not given, the last dimension of the ``input`` is chosen. - If `largest` is `False` then the smallest elements are returned. + If ``largest`` is ``False`` then the smallest elements are returned. - A namedtuple of `(values, indices)` is returned, where the `indices` are the indices - of the elements in the original `input` tensor. + A namedtuple of ``(values, indices)`` is returned, where the ``indices`` + are the indices of the elements in the original ``input`` tensor. Args: input (torch.Tensor): The input tensor. @@ -214,7 +215,7 @@ def leaky_clamp( max: Optional[Tensor] = None, clamped_slope: float = 0.01, ) -> Tensor: - """Leakily clamp all elements in `input` into the range :math:`[\\min, \\max]`. + """Leakily clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. The bounds :math:`\\min` and :math:`\\max` can be tensors. @@ -239,10 +240,60 @@ def leaky_clamp( def clamp( input: Tensor, min: Optional[Tensor] = None, max: Optional[Tensor] = None ) -> Tensor: - """Clamp all elements in `input` into the range :math:`[\\min, \\max]`. + """Clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. The bounds :math:`\\min` and :math:`\\max` can be tensors. See :class:`pfhedge.nn.Clamp` for details. """ return leaky_clamp(input, min=min, max=max, clamped_slope=0.0) + + +def realized_variance(input: Tensor, dt: Union[Tensor, float]) -> Tensor: + """Returns the realized variance of the price. + + Realized variance :math:`\\sigma^2` of the stock price :math:`S` is defined as: + + .. math :: + + \\sigma^2 = \\frac{1}{T - 1} \\sum_{i = 1}^{T - 1} + \\frac{1}{dt} \\log(S_{i + 1} / S_i)^2 + + where :math:`T` is the number of time steps. + + .. note :: + + The mean of log return is assumed to be zero. + + Args: + input (torch.Tensor): The input tensor. + dt (torch.Tensor or float): The intervals of the time steps. + + Shape: + - input: :math:`(*, T)` where :math:`T` stands for the number of time steps + and :math:`*` means any number of additional dimensions. + - output: :math:`(*)` + + Returns: + torch.Tensor + """ + return input.log().diff(dim=-1).square().mean(dim=-1) / dt + + +def realized_volatility(input: Tensor, dt: Union[Tensor, float]) -> Tensor: + """Returns the realized volatility of the price. + It is square root of :func:`realized_variance`. + + Args: + input (torch.Tensor): The input tensor. + dt (torch.Tensor or float): The intervals of the time steps. + + Shape: + - input: :math:`(*, T)` where :math:`T` stands for the number of time steps + and :math:`*` means any number of additional dimensions. + - output: :math:`(*)` + + Returns: + torch.Tensor + """ + return realized_variance(input, dt=dt).sqrt() diff --git a/pfhedge/nn/modules/bs/_base.py b/pfhedge/nn/modules/bs/_base.py index f1edef85..db4d9b55 100644 --- a/pfhedge/nn/modules/bs/_base.py +++ b/pfhedge/nn/modules/bs/_base.py @@ -14,7 +14,7 @@ class BSModuleMixin(Module): Shape: - Input: :math:`(N, *, H_\\text{in})`, where :math:`*` means any number of - additional dimensions. See `inputs()` for the names of input features. + additional dimensions. See :func:`inputs` for the names of input features. - Output: :math:`(N, *, 1)`: All but the last dimension are the same shape as the input. """ @@ -24,7 +24,8 @@ def forward(self, input: Tensor) -> Tensor: Args: input (torch.Tensor): The input tensor. Features are concatenated along - the last dimension. See `inputs()` for the names of the input features. + the last dimension. + See :func:`inputs()` for the names of the input features. Returns: torch.Tensor diff --git a/pfhedge/nn/modules/bs/american_binary.py b/pfhedge/nn/modules/bs/american_binary.py index 97d110d0..bb8d490f 100644 --- a/pfhedge/nn/modules/bs/american_binary.py +++ b/pfhedge/nn/modules/bs/american_binary.py @@ -18,7 +18,7 @@ class BSAmericanBinaryOption(BSModuleMixin): Shape: - Input: :math:`(N, *, 4)`, where :math:`*` means any number of additional - dimensions. See `inputs()` for the names of input features. + dimensions. See :func:`inputs` for the names of input features. - Output: :math:`(N, *, 1)`. Delta of the derivative. All but the last dimension are the same shape as the input. @@ -29,7 +29,7 @@ class BSAmericanBinaryOption(BSModuleMixin): Examples: - The `forward` method returns delta of the derivative. + The ``forward`` method returns delta of the derivative. >>> from pfhedge.nn import BSAmericanBinaryOption >>> diff --git a/pfhedge/nn/modules/bs/black_scholes.py b/pfhedge/nn/modules/bs/black_scholes.py index e39e93df..52ca5ac9 100644 --- a/pfhedge/nn/modules/bs/black_scholes.py +++ b/pfhedge/nn/modules/bs/black_scholes.py @@ -13,7 +13,7 @@ class BlackScholes(Module): """Initialize Black-Scholes formula module from a derivative. - The `forward` method returns the Black-Scholes delta. + The ``forward`` method returns the Black-Scholes delta. Args: derivative (:class:`pfhedge.instruments.Derivative`): @@ -22,7 +22,7 @@ class BlackScholes(Module): Shape: - Input : :math:`(N, *, H_{\\mathrm{in}})`, where :math:`*` means any number of additional dimensions and :math:`H_{\\mathrm{in}}` is the number of input - features. See `inputs()` for the names of input features. + features. See :func:`inputs` for the names of input features. - Output : :math:`(N, *, 1)`. All but the last dimension are the same shape as the input. @@ -31,7 +31,7 @@ class BlackScholes(Module): One can instantiate Black-Scholes module by using a derivative. For example, one can instantiate :class:`BSEuropeanOption` using a :class:`pfhedge.instruments.EuropeanOption`. - The `forward` method returns delta of the derivative. + The ``forward`` method returns delta of the derivative. >>> import torch >>> from pfhedge.instruments import BrownianStock diff --git a/pfhedge/nn/modules/bs/european.py b/pfhedge/nn/modules/bs/european.py index 42ab569f..1af9a8c5 100644 --- a/pfhedge/nn/modules/bs/european.py +++ b/pfhedge/nn/modules/bs/european.py @@ -16,7 +16,7 @@ class BSEuropeanOption(BSModuleMixin): Shape: - Input : :math:`(N, *, 3)`, where :math:`*` means any number - of additional dimensions. See `inputs()` for the names of input features. + of additional dimensions. See ``inputs`` for the names of input features. - Output: :math:`(N, *, 1)`. Delta of the derivative. All but the last dimension are the same shape as the input. @@ -27,7 +27,7 @@ class BSEuropeanOption(BSModuleMixin): Examples: - The `forward` method returns delta of the derivative. + The ``forward`` method returns delta of the derivative. >>> from pfhedge.nn import BSEuropeanOption >>> diff --git a/pfhedge/nn/modules/bs/european_binary.py b/pfhedge/nn/modules/bs/european_binary.py index 7b2e38b1..ba814e71 100644 --- a/pfhedge/nn/modules/bs/european_binary.py +++ b/pfhedge/nn/modules/bs/european_binary.py @@ -20,7 +20,7 @@ class BSEuropeanBinaryOption(BSModuleMixin): Shape: - Input: :math:`(N, *, 3)`, where :math:`*` means any number of additional - dimensions. See `inputs()` for the names of input features. + dimensions. See :func:`inputs` for the names of input features. - Output: :math:`(N, *, 1)` Delta of the derivative. All but the last dimension are the same shape as the input. @@ -31,7 +31,7 @@ class BSEuropeanBinaryOption(BSModuleMixin): Examples: - The `forward` method returns delta of the derivative. + The ``forward`` method returns delta of the derivative. >>> from pfhedge.nn import BSEuropeanBinaryOption >>> diff --git a/pfhedge/nn/modules/bs/lookback.py b/pfhedge/nn/modules/bs/lookback.py index 0082b02b..60f10051 100644 --- a/pfhedge/nn/modules/bs/lookback.py +++ b/pfhedge/nn/modules/bs/lookback.py @@ -21,7 +21,7 @@ class BSLookbackOption(BSModuleMixin): Shape: - Input: :math:`(N, *, 4)`, where :math:`*` means any number of additional - dimensions. See `inputs()` for the names of input features. + dimensions. See :func:`inputs` for the names of input features. - Output: :math:`(N, *, 1)`. Delta of the derivative. All but the last dimension are the same shape as the input. @@ -32,7 +32,7 @@ class BSLookbackOption(BSModuleMixin): Examples: - The `forward` method returns delta of the derivative. + The ``forward`` method returns delta of the derivative. >>> from pfhedge.nn import BSLookbackOption >>> diff --git a/pfhedge/nn/modules/clamp.py b/pfhedge/nn/modules/clamp.py index 15c9bf77..176e0e50 100644 --- a/pfhedge/nn/modules/clamp.py +++ b/pfhedge/nn/modules/clamp.py @@ -8,7 +8,7 @@ class LeakyClamp(Module): - """Leakily Clamp all elements in `input` into the range :math:`[\\min, \\max]`. + """Leakily Clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. The bounds :math:`\\min` and :math:`\\max` can be tensors. @@ -68,7 +68,7 @@ def extra_repr(self) -> str: def forward( self, input: Tensor, min: Optional[Tensor] = None, max: Optional[Tensor] = None ) -> Tensor: - """Clamp all elements in `input` into the range :math:`[\\min, \\max]`. + """Clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. Args: input (torch.Tensor): The input tensor. @@ -89,7 +89,7 @@ def forward( class Clamp(Module): - """Clamp all elements in `input` into the range :math:`[\\min, \\max]`. + """Clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. The bounds :math:`\\min` and :math:`\\max` can be tensors. @@ -144,7 +144,7 @@ class Clamp(Module): def forward( self, input: Tensor, min: Optional[Tensor] = None, max: Optional[Tensor] = None ) -> Tensor: - """Clamp all elements in `input` into the range :math:`[\\min, \\max]`. + """Clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. Args: input (torch.Tensor): The input tensor. diff --git a/pfhedge/nn/modules/hedger.py b/pfhedge/nn/modules/hedger.py index e42e4542..de32941e 100644 --- a/pfhedge/nn/modules/hedger.py +++ b/pfhedge/nn/modules/hedger.py @@ -24,17 +24,17 @@ class Hedger(Module): - """A `torch.nn.Module` to hedge and price derivatives. + """A ``torch.nn.Module`` to hedge and price derivatives. Args: model (torch.nn.Module): Hedging model to compute the hedge ratio at the next time step from the input features at the current time step. The input and output shapes should be :math:`(N, H_\\text{in})` and - :math:`(N, 1)` respectively, where `N` stands for the number simulated + :math:`(N, 1)` respectively, where :math:`N` stands for the number simulated paths of the asset prices and :math:`H_\\text{in}` stands for the number of - input features (namely, `len(inputs)`). + input features (namely, ``len(inputs)``). inputs (list[str|Feature]): List of (names of) input features to feed to model. - See `[str(f) for f in pfhedge.features.FEATURES]` for valid options. + See ``[str(f) for f in pfhedge.features.FEATURES]`` for valid options. criterion (HedgeLoss, default=EntropicRiskMeasure()): Loss function to minimize by hedging. Default: :class:`pfhedge.nn.EntropicRiskMeasure()` . @@ -129,7 +129,7 @@ def __init__( self.register_forward_hook(save_prev_output) def forward(self, input: Tensor) -> Tensor: - """Returns the outout of `model`. + """Returns the outout of ``self.model``. The output represents the hedge ratio at the next time step. """ @@ -148,7 +148,7 @@ def compute_pnl( A hedger sells the derivative to its customer and obliges to settle the payoff at maturity. The dealer hedges the risk of this liability by trading - the underlying instrument of the derivative based on `model`. + the underlying instrument of the derivative based on ``self.model``. The resulting profit and loss is obtained by adding up the payoff to the customer, capital gains from the underlying asset, and the transaction cost. @@ -158,7 +158,7 @@ def compute_pnl( underlying instrument. init_state (tuple[torch.Tensor | float], optional): The initial state of the underlying instrument of the derivative. - If `None` (default), it uses the default value. + If ``None`` (default), it uses the default value. Shape: - Output: :math:`(N)`, where :math:`N` is the number of paths. @@ -234,11 +234,11 @@ def compute_loss( derivative (pfhedge.instruments.Derivative): The derivative to hedge. n_paths (int, default=1000): The number of simulated price paths of the underlying instrument. - n_times (int, default=1): If `n_times > 1`, returns the ensemble mean + n_times (int, default=1): If ``n_times > 1``, returns the ensemble mean of the losses computed through multiple simulations. init_state (tuple, optional): The initial price of the underlying instrument of the derivative. - If `None` (default), sensible default value is used. + If ``None`` (default), sensible default value is used. enable_grad (bool, default=True): Context-manager that sets gradient calculation to on or off. @@ -287,14 +287,14 @@ def fit( n_epochs (int, default=100): Number of Monte-Carlo simulations. n_paths (int, default=1000): The number of simulated price paths of the underlying instrument. - n_times (int, default=1): If `n_times > 1`, returns the ensemble mean of + n_times (int, default=1): If ``n_times > 1``, returns the ensemble mean of the losses computed through multiple simulations. optimizer (torch.optim.Optimizer, default=Adam): The optimizer algorithm - to use. It can be an instance or a class of `torch.optim.Optimizer`. + to use. It can be an instance or a class of ``torch.optim.Optimizer``. init_state (tuple, optional): The initial price of the underlying instrument of the derivative. - If `None` (default), sensible default value is used. - verbose (bool, default=True): If `True`, print progress of the training to + If ``None`` (default), sensible default value is used. + verbose (bool, default=True): If ``True``, print progress of the training to standard output. Returns: @@ -354,7 +354,7 @@ def compute_loss(**kwargs) -> Tensor: ) history = [] - progress = tqdm(range(n_epochs)) if verbose else range(n_epochs) + progress = tqdm(range(n_epochs), disable=not verbose) for _ in progress: # Compute training loss and backpropagate self.train() @@ -387,11 +387,11 @@ def price( derivative (pfhedge.instuments.Derivative): The derivative to price. n_paths (int, default=1000): The number of simulated price paths of the underlying instrument. - n_times (int, default=1): If `n_times > 1`, returns the ensemble mean of + n_times (int, default=1): If ``n_times > 1``, returns the ensemble mean of the losses computed through multiple simulations. init_state (tuple, optional): The initial price of the underlying instrument of the derivative. - If `None` (default), sensible default value is used. + If ``None`` (default), sensible default value is used. enable_grad (bool, default=False): Context-manager that sets gradient calculation to on or off. diff --git a/pfhedge/nn/modules/loss.py b/pfhedge/nn/modules/loss.py index 0fff7d5b..3961d5b2 100644 --- a/pfhedge/nn/modules/loss.py +++ b/pfhedge/nn/modules/loss.py @@ -37,7 +37,7 @@ def cash(self, input: Tensor) -> Tensor: """Returns the cash amount which is as preferable as the given profit-loss distribution in terms of the loss. - The output `cash` is expected to satisfy the following relation: + The output ``cash`` is expected to satisfy the following relation: .. code:: @@ -64,7 +64,8 @@ def cash(self, input: Tensor) -> Tensor: class EntropicRiskMeasure(HedgeLoss): """Creates a loss given by the entropic risk measure. - The entropic risk measure of the profit-loss distribution `pnl` is given by: + The entropic risk measure of the profit-loss distribution + :math:`\\text{pnl}` is given by: .. math :: @@ -127,7 +128,7 @@ def cash(self, input: Tensor) -> Tensor: class EntropicLoss(HedgeLoss): """Creates a loss given by the negative of expected exponential utility. - The loss of the profit-loss `pnl` is given by + The loss of the profit-loss :math:`\\text{pnl}` is given by: .. math :: @@ -190,7 +191,7 @@ def cash(self, input: Tensor) -> Tensor: class IsoelasticLoss(HedgeLoss): """Creates a loss function that measures the isoelastic utility. - The loss of the profit-loss distribution `pnl` is given by + The loss of the profit-loss :math:`\\text{pnl}` is given by: .. math :: diff --git a/pfhedge/nn/modules/mlp.py b/pfhedge/nn/modules/mlp.py index 654d7ee7..75dd3fff 100644 --- a/pfhedge/nn/modules/mlp.py +++ b/pfhedge/nn/modules/mlp.py @@ -19,14 +19,14 @@ class MultiLayerPerceptron(Sequential): Args: in_features (int, default=None): Size of each input sample. - If `None` (default), the number of input features will be - will be inferred from the `input.shape[-1]` after the first call to - `forward` is done. Also, before the first `forward` parameters in the - module are of `torch.nn.UninitializedParameter` class. + If ``None`` (default), the number of input features will be + will be inferred from the ``input.shape[-1]`` after the first call to + ``forward`` is done. Also, before the first ``forward`` parameters in the + module are of ``torch.nn.UninitializedParameter`` class. out_features (int, default=1): Size of each output sample. n_layers (int, default=4): Number of hidden layers. n_units (int or tuple[int], default=32): Number of units in each hidden layer. - If `tuple[int]`, it specifies different number of units for each layer. + If ``tuple[int]``, it specifies different number of units for each layer. activation (torch.nn.Module, default=torch.nn.ReLU()): Activation module of the hidden layers. out_activation (torch.nn.Module, default=torch.nn.Identity()): @@ -38,11 +38,11 @@ class MultiLayerPerceptron(Sequential): features. - Output: :math:`(N, *, H_{\\text{out}})`, where all but the last dimension are the same shape as the input and :math:`H_{\\text{in}})` is - `out_features`. + ``out_features``. Examples: - By default, `in_features` is lazily determined: + By default, ``in_features`` is lazily determined: >>> import torch >>> from pfhedge.nn import MultiLayerPerceptron diff --git a/pfhedge/nn/modules/naked.py b/pfhedge/nn/modules/naked.py index 0f3be132..7d11aba8 100644 --- a/pfhedge/nn/modules/naked.py +++ b/pfhedge/nn/modules/naked.py @@ -15,7 +15,7 @@ class Naked(Module): features. - Output: :math:`(N, *, H_{\\text{out}})`, where all but the last dimension are the same shape as the input and :math:`H_{\\text{in}})` is - `out_features`. + ``out_features``. Examples: diff --git a/pfhedge/nn/modules/ww.py b/pfhedge/nn/modules/ww.py index 67b36a4b..d5fcaa73 100644 --- a/pfhedge/nn/modules/ww.py +++ b/pfhedge/nn/modules/ww.py @@ -10,7 +10,7 @@ class WhalleyWilmott(Module): """Initialize Whalley-Wilmott's hedging strategy of a derivative. - The `forward` method returns the next hedge ratio. + The ``forward`` method returns the next hedge ratio. This is the optimal hedging strategy for asymptotically small transaction cost. @@ -20,8 +20,9 @@ class WhalleyWilmott(Module): Shape: - Input: :math:`(N, *, H_{\\text{in}})`. Here, :math:`*` means any number of - additional dimensions and `H_in` is the number of input features. - See `inputs()` for the names of input features. + additional dimensions and :math:`H_{\\text{in}}` is + the number of input features. + See :func:`inputs()` for the names of input features. - Output: :math:`(N, *, 1)`. The hedge ratio at the next time step. Examples: diff --git a/pfhedge/stochastic/brownian.py b/pfhedge/stochastic/brownian.py index 566aa011..db353499 100644 --- a/pfhedge/stochastic/brownian.py +++ b/pfhedge/stochastic/brownian.py @@ -33,17 +33,18 @@ def generate_brownian( n_steps (int): The number of time steps. init_state (tuple[torch.Tensor | float], default=(0.0,)): The initial state of the time series. - This is specified by `(S0,)`, where `S0` is the initial value of :math:`S`. - It also accepts a float or a `torch.Tensor`. + This is specified by ``(S0,)``, where ``S0`` is + the initial value of :math:`S`. + It also accepts a ``torch.Tensor` or a ``float``. volatility (float, default=0.2): The volatility of the Brownian motion. dt (float, default=1/250): The intervals of the time steps. dtype (torch.dtype, optional): The desired data type of returned tensor. - Default: If `None`, uses a global default - (see `torch.set_default_tensor_type()`). + Default: If ``None``, uses a global default + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): The desired device of returned tensor. - Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and the current CUDA device + Default: If ``None``, uses the current device for the default tensor type + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Shape: @@ -101,17 +102,17 @@ def generate_geometric_brownian( n_steps (int): The number of time steps. init_state (tuple[torch.Tensor | float], default=(1.0,)): The initial state of the time series. - This is specified by `(S0,)`, where `S0` is the initial value of :math:`S`. - It also accepts a float or a `torch.Tensor`. + This is specified by ``(S0,)``, where ``S0`` is the initial value of :math:`S`. + It also accepts a ``torch.Tensor`` or a ``float``. volatility (float, default=0.2): The volatility of the Brownian motion. dt (float, default=1/250): The intervals of the time steps. dtype (torch.dtype, optional): The desired data type of returned tensor. - Default: If `None`, uses a global default - (see `torch.set_default_tensor_type()`). + Default: If ``None``, uses a global default + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): The desired device of returned tensor. - Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and the current CUDA device + Default: If ``None``, uses the current device for the default tensor type + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Shape: diff --git a/pfhedge/stochastic/cir.py b/pfhedge/stochastic/cir.py index 0a70d18b..1eb3ffd5 100644 --- a/pfhedge/stochastic/cir.py +++ b/pfhedge/stochastic/cir.py @@ -35,19 +35,20 @@ def generate_cir( n_steps (int): The number of time steps. init_state (tuple[torch.Tensor | float], default=(0.04,)): The initial state of the time series. - This is specified by `(X0,)`, where `X0` is the initial value of :math:`X`. - It also accepts a float or a `torch.Tensor`. + This is specified by ``(X0,)``, where ``X0`` is + the initial value of :math:`X`. + It also accepts a ``torch.Tensor`` or a float. kappa (torch.Tensor or float, default=1.0): The parameter :math:`\\kappa`. theta (torch.Tensor or float, default=0.04): The parameter :math:`\\theta`. sigma (torch.Tensor or float, default=2.0): The parameter :math:`\\sigma`. dt (torch.Tensor or float, default=1/250): The intervals of the time steps. dtype (torch.dtype, optional): The desired data type of returned tensor. - Default: If `None`, uses a global default - (see `torch.set_default_tensor_type()`). + Default: If ``None``, uses a global default + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): The desired device of returned tensor. Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and the current CUDA device + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Shape: diff --git a/pfhedge/stochastic/heston.py b/pfhedge/stochastic/heston.py index 5c0bbaa6..47d54f3d 100644 --- a/pfhedge/stochastic/heston.py +++ b/pfhedge/stochastic/heston.py @@ -41,7 +41,7 @@ def generate_heston( n_steps (int): The number of time steps. init_state (tuple[torch.Tensor | float], default=(1.0,)): The initial state of the time series. - This is specified by `(S0, V0)`, where `S0` and `V0` are the initial values + This is specified by ``(S0, V0)``, where ``S0`` and ``V0`` are the initial values of :math:`S` and :math:`V`, respectively. kappa (float, default=1.0): The parameter :math:`\\kappa`. theta (float, default=0.04): The parameter :math:`\\theta`. @@ -49,12 +49,12 @@ def generate_heston( rho (float, default=-0.7): The parameter :math:`\\rho`. dt (float, default=1/250): The intervals of the time steps. dtype (torch.dtype, optional): The desired data type of returned tensor. - Default: If `None`, uses a global default - (see `torch.set_default_tensor_type()`). + Default: If ``None``, uses a global default + (see ``torch.set_default_tensor_type()``). device (torch.device, optional): The desired device of returned tensor. - Default: if None, uses the current device for the default tensor type - (see `torch.set_default_tensor_type()`). - `device` will be the CPU for CPU tensor types and the current CUDA device + Default: If ``None``, uses the current device for the default tensor type + (see ``torch.set_default_tensor_type()``). + ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. Shape: diff --git a/pyproject.toml b/pyproject.toml index 342aea40..acb0b678 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pfhedge" -version = "0.8.1" +version = "0.9.0" description = "Deep Hedging in PyTorch" authors = ["Shota Imaki "] license = "MIT" diff --git a/tests/features/test_features.py b/tests/features/test_features.py index 44e3997c..c0d1da49 100644 --- a/tests/features/test_features.py +++ b/tests/features/test_features.py @@ -104,22 +104,38 @@ class TestExpiryTime(_TestFeature): """ def test(self): - maturity = 3 / 365 - dt = 1 / 365 - derivative = EuropeanOption(BrownianStock(dt=dt), maturity=maturity) + derivative = EuropeanOption(BrownianStock(dt=0.1), maturity=0.2) derivative.underlier.spot = torch.empty(2, 3) + f = ExpiryTime().of(derivative) + + result = f[0] + expect = torch.full((2, 1), 0.2) + assert_close(result, expect, check_stride=False) + + result = f[1] + expect = torch.full((2, 1), 0.1) + assert_close(result, expect, check_stride=False) + + result = f[2] + expect = torch.full((2, 1), 0.0) + assert_close(result, expect, check_stride=False) + def test_2(self): + derivative = EuropeanOption(BrownianStock(dt=0.1), maturity=0.15) + derivative.underlier.spot = torch.empty(2, 3) f = ExpiryTime().of(derivative) result = f[0] - expect = torch.full((2, 1), 3 / 365) - assert_close(result, expect) + expect = torch.full((2, 1), 0.2) + assert_close(result, expect, check_stride=False) + result = f[1] - expect = torch.full((2, 1), 2 / 365) - assert_close(result, expect) + expect = torch.full((2, 1), 0.1) + assert_close(result, expect, check_stride=False) + result = f[2] - expect = torch.full((2, 1), 1 / 365) - assert_close(result, expect) + expect = torch.full((2, 1), 0.0) + assert_close(result, expect, check_stride=False) def test_str(self): assert str(ExpiryTime()) == "expiry_time" diff --git a/tests/instruments/derivative/test_european.py b/tests/instruments/derivative/test_european.py index 44f44d72..6ff2e05b 100644 --- a/tests/instruments/derivative/test_european.py +++ b/tests/instruments/derivative/test_european.py @@ -49,7 +49,7 @@ def test_moneyness(self, strike): assert_close(result, expect) result = derivative.moneyness(0) - expect = stock.spot[:, 0] / strike + expect = stock.spot[:, [0]] / strike assert_close(result, expect) result = derivative.log_moneyness() @@ -57,21 +57,50 @@ def test_moneyness(self, strike): assert_close(result, expect) result = derivative.log_moneyness(0) - expect = (stock.spot[:, 0] / strike).log() + expect = (stock.spot[:, [0]] / strike).log() assert_close(result, expect) def test_time_to_maturity(self): - stock = BrownianStock(dt=1.0) - derivative = EuropeanOption(stock, maturity=4.0) + stock = BrownianStock(dt=0.1) + derivative = EuropeanOption(stock, maturity=0.2) derivative.simulate(n_paths=2) result = derivative.time_to_maturity() - expect = torch.tensor([[4.0, 3.0, 2.0, 1.0], [4.0, 3.0, 2.0, 1.0]]) - assert_close(result, expect) + expect = torch.tensor([[0.2, 0.1, 0.0], [0.2, 0.1, 0.0]]) + assert_close(result, expect, check_stride=False) result = derivative.time_to_maturity(0) - expect = torch.tensor([4.0, 4.0]) - assert_close(result, expect) + expect = torch.full((2, 1), 0.2) + assert_close(result, expect, check_stride=False) + + result = derivative.time_to_maturity(1) + expect = torch.full((2, 1), 0.1) + assert_close(result, expect, check_stride=False) + + result = derivative.time_to_maturity(-1) + expect = torch.full((2, 1), 0.0) + assert_close(result, expect, check_stride=False) + + def test_time_to_maturity_2(self): + stock = BrownianStock(dt=0.1) + derivative = EuropeanOption(stock, maturity=0.25) + derivative.simulate(n_paths=2) + + result = derivative.time_to_maturity() + expect = torch.tensor([[0.3, 0.2, 0.1, 0.0], [0.3, 0.2, 0.1, 0.0]]) + assert_close(result, expect, check_stride=False) + + result = derivative.time_to_maturity(0) + expect = torch.full((2, 1), 0.3) + assert_close(result, expect, check_stride=False) + + result = derivative.time_to_maturity(1) + expect = torch.full((2, 1), 0.2) + assert_close(result, expect, check_stride=False) + + result = derivative.time_to_maturity(-1) + expect = torch.full((2, 1), 0.0) + assert_close(result, expect, check_stride=False) @pytest.mark.parametrize("dtype", [torch.float32, torch.float64]) def test_dtype(self, dtype): diff --git a/tests/instruments/derivative/test_variance_swap.py b/tests/instruments/derivative/test_variance_swap.py new file mode 100644 index 00000000..138d08c0 --- /dev/null +++ b/tests/instruments/derivative/test_variance_swap.py @@ -0,0 +1,41 @@ +from math import sqrt + +import torch +from torch.testing import assert_close + +from pfhedge.instruments import BrownianStock +from pfhedge.instruments import HestonStock +from pfhedge.instruments import VarianceSwap + + +class TestVarianceSwap: + def test_repr(self): + derivative = VarianceSwap(BrownianStock()) + + result = repr(derivative) + expect = "VarianceSwap(BrownianStock(...), strike=4.00e-02, maturity=8.00e-02)" + assert result == expect + + def test_payoff(self): + derivative = VarianceSwap(BrownianStock(), strike=0.04) + derivative.ul().register_buffer("spot", torch.ones(2, 3)) + result = derivative.payoff() + expect = torch.full_like(result, -0.04) + assert_close(result, expect) + + derivative = VarianceSwap(BrownianStock(dt=0.01), strike=0) + var = 0.04 + log_return = torch.full((2, 10), sqrt(var * derivative.ul().dt)) + log_return[:, 0] = 0.0 + spot = log_return.cumsum(-1).exp() + derivative.ul().register_buffer("spot", spot) + result = derivative.payoff() + expect = torch.full_like(result, var) + assert_close(result, expect) + + derivative = VarianceSwap(HestonStock()) + derivative.ul().register_buffer("spot", torch.ones(2, 3)) + derivative.ul().register_buffer("variance", torch.ones(2, 3)) + result = derivative.payoff() + expect = torch.full_like(result, -0.04) + assert_close(result, expect) diff --git a/tests/instruments/primary/test_brownian.py b/tests/instruments/primary/test_brownian.py index 8f119d4d..9b7a85d8 100644 --- a/tests/instruments/primary/test_brownian.py +++ b/tests/instruments/primary/test_brownian.py @@ -115,6 +115,15 @@ def test_device(self): s = BrownianStock(device=torch.device("cuda:0")) assert s.cpu().device == torch.device("cpu") + def test_simulate_shape(self): + s = BrownianStock(dt=0.1) + s.simulate(time_horizon=0.2, n_paths=10) + assert s.spot.size() == torch.Size((10, 3)) + + s = BrownianStock(dt=0.1) + s.simulate(time_horizon=0.25, n_paths=10) + assert s.spot.size() == torch.Size((10, 4)) + def test_cuda(self): s = BrownianStock() assert s.cuda(1).device == torch.device("cuda:1") diff --git a/tests/nn/bs/test_european_binary.py b/tests/nn/bs/test_european_binary.py index 83a77f4d..16f5ee9f 100644 --- a/tests/nn/bs/test_european_binary.py +++ b/tests/nn/bs/test_european_binary.py @@ -99,7 +99,7 @@ def test_example(self): model = BSEuropeanBinaryOption(deriv) hedger = Hedger(model, model.inputs()) result = hedger.price(deriv) - expect = torch.tensor(0.51) + expect = torch.tensor(0.4922) assert_close(result, expect, atol=1e-2, rtol=1e-2) def test_shape(self): diff --git a/tests/nn/modules/test_ww.py b/tests/nn/modules/test_ww.py index 22f37cf6..92c34c3d 100644 --- a/tests/nn/modules/test_ww.py +++ b/tests/nn/modules/test_ww.py @@ -1,8 +1,10 @@ import torch +from pfhedge import autogreek from pfhedge.instruments import BrownianStock from pfhedge.instruments import EuropeanOption from pfhedge.instruments import LookbackOption +from pfhedge.nn import Hedger from pfhedge.nn import WhalleyWilmott @@ -55,3 +57,22 @@ def test_shape(self): input = torch.empty((N, M_1, M_2, H_in)) assert m(input).size() == torch.Size((N, M_1, M_2, 1)) + + def test(self): + derivative = EuropeanOption(BrownianStock(cost=1e-4)) + model = WhalleyWilmott(derivative) + hedger = Hedger(model, model.inputs()) + pnl = hedger.compute_pnl(derivative) + assert not pnl.isnan().any() + + def test_autogreek(self): + with torch.autograd.detect_anomaly(): + derivative = EuropeanOption(BrownianStock(cost=1e-4)).to(torch.float64) + model = WhalleyWilmott(derivative).to(torch.float64) + hedger = Hedger(model, model.inputs()).to(torch.float64) + + def pricer(spot): + return hedger.price(derivative, init_state=(spot,), enable_grad=True) + + delta = autogreek.delta(pricer, spot=torch.tensor(1.0)) + assert not delta.isnan().any() diff --git a/tests/nn/test_functional.py b/tests/nn/test_functional.py index d36ef307..a76b2f6d 100644 --- a/tests/nn/test_functional.py +++ b/tests/nn/test_functional.py @@ -6,6 +6,8 @@ from pfhedge.nn.functional import exp_utility from pfhedge.nn.functional import expected_shortfall from pfhedge.nn.functional import leaky_clamp +from pfhedge.nn.functional import realized_variance +from pfhedge.nn.functional import realized_volatility from pfhedge.nn.functional import topp @@ -79,3 +81,31 @@ def test_leaky_clamp(): result = leaky_clamp(input, 0, 1, clamped_slope=0.0) expect = clamp(input, 0, 1) assert_close(result, expect) + + +def test_realized_variance(): + torch.manual_seed(42) + + log_return = 0.01 * torch.randn(2, 10) + log_return[:, 0] = 0.0 + log_return -= log_return.mean(dim=-1, keepdim=True) + input = log_return.cumsum(-1).exp() + + result = realized_variance(input, dt=1.0) + expect = log_return[:, 1:].var(-1, unbiased=False) + + assert_close(result, expect) + + +def test_realized_volatility(): + torch.manual_seed(42) + + log_return = 0.01 * torch.randn(2, 10) + log_return[:, 0] = 0.0 + log_return -= log_return.mean(dim=-1, keepdim=True) + input = log_return.cumsum(-1).exp() + + result = realized_volatility(input, dt=1.0) + expect = log_return[:, 1:].std(-1, unbiased=False) + + assert_close(result, expect)