diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 407154e1..79559cad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,17 +39,14 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install - run: | - python3 -m pip install --upgrade pip - python3 -m pip install . - python3 -m pip install pytest pytest-cov codecov + run: pip install poetry && poetry install - name: Run doctest - run: python3 -m pytest --doctest-modules ${PROJECT_NAME} + run: make doctest if: always() - name: Run pytest - run: python3 -m pytest --cov=${PROJECT_NAME} tests + run: make test-cov if: always() - name: Upload codecov report @@ -70,7 +67,7 @@ jobs: - run: pip install poetry && poetry install - - run: poetry run python -m mypy ${PROJECT_NAME} + - run: make mypy lint: diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index e583cd1f..c62aa5ba 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -27,9 +27,7 @@ jobs: - uses: actions/setup-python@v2 - - run: pip install . - - - run: pip install -r docs/requirements.txt + - run: pip install poetry && poetry install - run: cd docs && make html diff --git a/docs/Makefile b/docs/Makefile index c5f3308f..a3ad37f2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,24 +4,25 @@ # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build +SPHINXBUILD ?= poetry run sphinx-build +SPHINXAUTOBUILD ?= poetry run sphinx-autobuild SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) clean: - @echo "Removing everything under 'build' and 'source/generated'.." - @rm -rf $(BUILDDIR)/html/ $(BUILDDIR)/doctrees $(SOURCEDIR)/generated + echo "Removing everything under 'build' and 'source/generated'.." + rm -rf $(BUILDDIR)/html/ $(BUILDDIR)/doctrees $(SOURCEDIR)/generated livehtml: - sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --open-browser $(O) + $(SPHINXAUTOBUILD) "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --open-browser $(O) diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index f6936905..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -sphinx -sphinx-autobuild -sphinx-copybutton -furo diff --git a/pfhedge/features/_base.py b/pfhedge/features/_base.py index 7a9cfcb9..a6fe3a53 100644 --- a/pfhedge/features/_base.py +++ b/pfhedge/features/_base.py @@ -1,5 +1,4 @@ import copy -import warnings from abc import ABC from abc import abstractmethod from typing import Optional diff --git a/pfhedge/features/container.py b/pfhedge/features/container.py index 97fe260e..293a5e03 100644 --- a/pfhedge/features/container.py +++ b/pfhedge/features/container.py @@ -1,7 +1,6 @@ import copy from typing import List from typing import Optional -from typing import Type from typing import TypeVar from typing import Union diff --git a/pfhedge/instruments/primary/base.py b/pfhedge/instruments/primary/base.py index c94b31da..e8c00816 100644 --- a/pfhedge/instruments/primary/base.py +++ b/pfhedge/instruments/primary/base.py @@ -10,7 +10,6 @@ import torch from torch import Tensor -from torch.nn import Module from pfhedge._utils.doc import _set_attr_and_docstring from pfhedge._utils.doc import _set_docstring @@ -150,7 +149,7 @@ def spot(self) -> Tensor: if "_buffers" in self.__dict__: _buffers = self.__dict__["_buffers"] if name in _buffers: - return _buffers["spot"] + return _buffers[name] raise AttributeError( f"'{self._get_name()}' object has no attribute '{name}'. " "Asset may not be simulated." diff --git a/pfhedge/instruments/primary/brownian.py b/pfhedge/instruments/primary/brownian.py index 3e4d8aa6..a88c4cf4 100644 --- a/pfhedge/instruments/primary/brownian.py +++ b/pfhedge/instruments/primary/brownian.py @@ -1,7 +1,6 @@ from math import ceil from typing import Optional from typing import Tuple -from typing import Union from typing import cast import torch diff --git a/pfhedge/instruments/primary/cir.py b/pfhedge/instruments/primary/cir.py index 4dc7d922..72f83af7 100644 --- a/pfhedge/instruments/primary/cir.py +++ b/pfhedge/instruments/primary/cir.py @@ -1,10 +1,8 @@ from math import ceil from typing import Optional from typing import Tuple -from typing import Union import torch -from torch import Tensor from pfhedge._utils.doc import _set_attr_and_docstring from pfhedge._utils.doc import _set_docstring diff --git a/pfhedge/nn/functional.py b/pfhedge/nn/functional.py index f7eb12eb..c11e4e82 100644 --- a/pfhedge/nn/functional.py +++ b/pfhedge/nn/functional.py @@ -108,13 +108,13 @@ def european_binary_payoff( def exp_utility(input: Tensor, a: float = 1.0) -> Tensor: - """Applies an exponential utility function. + r"""Applies an exponential utility function. An exponential utility function is defined as: .. math:: - u(x) = -\\exp(-a x) \\,. + u(x) = -\exp(-a x) \,. Args: input (torch.Tensor): The input tensor. @@ -128,16 +128,16 @@ def exp_utility(input: Tensor, a: float = 1.0) -> Tensor: def isoelastic_utility(input: Tensor, a: float) -> Tensor: - """Applies an isoelastic utility function. + r"""Applies an isoelastic utility function. An isoelastic utility function is defined as: .. math:: - u(x) = \\begin{cases} - x^{1 - a} & a \\neq 1 \\\\ - \\log{x} & a = 1 - \\end{cases} \\,. + u(x) = \begin{cases} + x^{1 - a} & a \neq 1 \\ + \log{x} & a = 1 + \end{cases} \,. Args: input (torch.Tensor): The input tensor. @@ -201,12 +201,13 @@ def expected_shortfall(input: Tensor, p: float, dim: Optional[int] = None) -> Te dim (int, optional): The dimension to sort along. Examples: - >>> from pfhedge.nn.functional import expected_shortfall >>> - >>> input = -torch.arange(1., 6.) - >>> expected_shortfall(input, 3 / 5) - tensor(4.) + >>> input = -torch.arange(1.0, 10.0) + >>> input + tensor([-1., -2., -3., -4., -5., -6., -7., -8., -9.]) + >>> expected_shortfall(input, 0.3) + tensor(8.) Returns: torch.Tensor @@ -224,7 +225,7 @@ def leaky_clamp( clamped_slope: float = 0.01, inverted_output: str = "mean", ) -> Tensor: - """Leakily clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. + r"""Leakily clamp all elements in ``input`` into the range :math:`[\min, \max]`. See :class:`pfhedge.nn.LeakyClamp` for details. """ @@ -256,7 +257,7 @@ def clamp( max: Optional[Tensor] = None, inverted_output: str = "mean", ) -> Tensor: - """Clamp all elements in ``input`` into the range :math:`[\\min, \\max]`. + r"""Clamp all elements in ``input`` into the range :math:`[\min, \max]`. See :class:`pfhedge.nn.Clamp` for details. """ @@ -270,14 +271,14 @@ def clamp( def realized_variance(input: Tensor, dt: TensorOrScalar) -> Tensor: - """Returns the realized variance of the price. + r"""Returns the realized variance of the price. - Realized variance :math:`\\sigma^2` of the stock price :math:`S` is defined as: + 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 + \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. @@ -327,19 +328,19 @@ def terminal_value( payoff: Optional[Tensor] = None, deduct_first_cost: bool = True, ): - """Returns the terminal portfolio value. + r"""Returns the terminal portfolio value. The terminal value of a hedger's portfolio is given by .. math:: - \\text{PL}(Z, \\delta, S) = + \text{PL}(Z, \delta, S) = - Z - + \\sum_{i = 0}^{T - 2} \\delta_{i - 1} (S_{i} - S_{i - 1}) - - c \\sum_{i = 0}^{T - 1} |\\delta_{i} - \\delta_{i - 1}| S_{i} + + \sum_{i = 0}^{T - 2} \delta_{i - 1} (S_{i} - S_{i - 1}) + - c \sum_{i = 0}^{T - 1} |\delta_{i} - \delta_{i - 1}| S_{i} where :math:`Z` is the payoff of the derivative, :math:`T` is the number of - time steps, :math:`S` is the spot price, :math:`\\delta` is the signed number + time steps, :math:`S` is the spot price, :math:`\delta` is the signed number of shares held at each time step. We define :math:`\delta_0 = 0` for notational convenience. @@ -358,7 +359,7 @@ def terminal_value( Args: spot (torch.Tensor): The spot price of the underlying asset :math:`S`. unit (torch.Tensor): The signed number of shares of the underlying asset - :math:`\\delta`. + :math:`\delta`. cost (float, default=0.0): The proportional transaction cost rate of the underlying asset :math:`c`. payoff (torch.Tensor, optional): The payoff of the derivative :math:`Z`. @@ -434,7 +435,19 @@ def npdf(input: Tensor) -> Tensor: def d1(log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor) -> Tensor: - """Returns :math:`d_1` in the Black-Scholes formula. + r"""Returns :math:`d_1` in the Black-Scholes formula. + + .. math:: + + d_1 = \frac{s + \frac12 \sigma^2 t}{\sigma \sqrt{t}} + + where + :math:`s` is the log moneyness, + :math:`t` is the time to maturity, and + :math:`\sigma` is the volatility. + + Note: + Risk-free rate is set to zero. Args: log_moneyness (torch.Tensor or float): Log moneyness of the underlying asset. @@ -449,7 +462,19 @@ def d1(log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor) -> T def d2(log_moneyness: Tensor, time_to_maturity: Tensor, volatility: Tensor) -> Tensor: - """Returns :math:`d_2` in the Black-Scholes formula. + r"""Returns :math:`d_2` in the Black-Scholes formula. + + .. math:: + + d_2 = \frac{s - \frac12 \sigma^2 t}{\sigma \sqrt{t}} + + where + :math:`s` is the log moneyness, + :math:`t` is the time to maturity, and + :math:`\sigma` is the volatility. + + Note: + Risk-free rate is set to zero. Args: log_moneyness (torch.Tensor or float): Log moneyness of the underlying asset. diff --git a/pfhedge/nn/modules/bs/_base.py b/pfhedge/nn/modules/bs/_base.py index f821c07e..b8e8924f 100644 --- a/pfhedge/nn/modules/bs/_base.py +++ b/pfhedge/nn/modules/bs/_base.py @@ -1,4 +1,3 @@ -import abc from inspect import signature from typing import List from typing import no_type_check diff --git a/pfhedge/nn/modules/bs/american_binary.py b/pfhedge/nn/modules/bs/american_binary.py index b056995d..84f4f304 100644 --- a/pfhedge/nn/modules/bs/american_binary.py +++ b/pfhedge/nn/modules/bs/american_binary.py @@ -4,7 +4,6 @@ from torch import Tensor from torch.distributions.utils import broadcast_all -import pfhedge.autogreek as autogreek from pfhedge._utils.bisect import find_implied_volatility from pfhedge._utils.doc import _set_attr_and_docstring from pfhedge._utils.str import _format_float @@ -19,6 +18,9 @@ class BSAmericanBinaryOption(BSModuleMixin): """Black-Scholes formula for an American Binary Option. + Note: + Risk-free rate is set to zero. + Args: call (bool, default=True): Specifies whether the option is call or put. strike (float, default=1.0): The strike price of the option. @@ -33,6 +35,8 @@ class BSAmericanBinaryOption(BSModuleMixin): .. seealso:: - :class:`pfhedge.nn.BlackScholes`: Initialize Black-Scholes formula module from a derivative. + - :class:`pfhedge.instruments.AmericanBinaryOption`: + Corresponding derivative. References: - Dai, M., 2000. A closed-form solution for perpetual American floating strike diff --git a/pfhedge/nn/modules/bs/european.py b/pfhedge/nn/modules/bs/european.py index f2533227..9fb59407 100644 --- a/pfhedge/nn/modules/bs/european.py +++ b/pfhedge/nn/modules/bs/european.py @@ -16,6 +16,9 @@ class BSEuropeanOption(BSModuleMixin): """Black-Scholes formula for a European option. + Note: + Risk-free rate is set to zero. + Args: call (bool, default=True): Specifies whether the option is call or put. strike (float, default=1.0): The strike price of the option. @@ -28,9 +31,10 @@ class BSEuropeanOption(BSModuleMixin): All but the last dimension are the same shape as the input. .. seealso:: - - :class:`pfhedge.nn.BlackScholes`: Initialize Black-Scholes formula module from a derivative. + - :class:`pfhedge.instruments.EuropeanOption`: + Corresponding derivative. References: - John C. Hull, 2003. Options futures and other derivatives. Pearson. diff --git a/pfhedge/nn/modules/bs/european_binary.py b/pfhedge/nn/modules/bs/european_binary.py index 1d821fc9..89a807f0 100644 --- a/pfhedge/nn/modules/bs/european_binary.py +++ b/pfhedge/nn/modules/bs/european_binary.py @@ -19,6 +19,9 @@ class BSEuropeanBinaryOption(BSModuleMixin): """Black-Scholes formula for a European binary option. + Note: + Risk-free rate is set to zero. + Args: call (bool, default=True): Specifies whether the option is call or put. strike (float, default=1.0): The strike price of the option. @@ -33,6 +36,8 @@ class BSEuropeanBinaryOption(BSModuleMixin): .. seealso:: - :class:`pfhedge.nn.BlackScholes`: Initialize Black-Scholes formula module from a derivative. + - :class:`pfhedge.instruments.EuropeanBinaryOption`: + Corresponding derivative. References: - John C. Hull, 2003. Options futures and other derivatives. Pearson. diff --git a/pfhedge/nn/modules/bs/lookback.py b/pfhedge/nn/modules/bs/lookback.py index 726fefed..c519297d 100644 --- a/pfhedge/nn/modules/bs/lookback.py +++ b/pfhedge/nn/modules/bs/lookback.py @@ -2,7 +2,6 @@ import torch from torch import Tensor -from torch.distributions.utils import broadcast_all from pfhedge._utils.bisect import find_implied_volatility from pfhedge._utils.doc import _set_attr_and_docstring @@ -19,6 +18,9 @@ class BSLookbackOption(BSModuleMixin): """Black-Scholes formula for a lookback option with a fixed strike. + Note: + Risk-free rate is set to zero. + Args: call (bool, default=True): Specifies whether the option is call or put. strike (float, default=1.0): The strike price of the option. @@ -31,9 +33,10 @@ class BSLookbackOption(BSModuleMixin): All but the last dimension are the same shape as the input. .. seealso:: - - :class:`pfhedge.nn.BlackScholes`: Initialize Black-Scholes formula module from a derivative. + - :class:`pfhedge.instruments.LookBackOption`: + Corresponding derivative. References: - Conze, A., 1991. Path dependent options: The case of lookback options. diff --git a/pfhedge/nn/modules/hedger.py b/pfhedge/nn/modules/hedger.py index 1059c2d4..ebe08ec4 100644 --- a/pfhedge/nn/modules/hedger.py +++ b/pfhedge/nn/modules/hedger.py @@ -312,7 +312,7 @@ def compute_hedge( # the last time step is not included. output[..., -1, :] = output[..., -2, :] - output = output.transpose(-1, -2) # (N, H, T) + output = output.transpose(-1, -2) # (N, H, T) return output diff --git a/pfhedge/nn/modules/ww.py b/pfhedge/nn/modules/ww.py index cd4332df..591886d5 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. + r"""Initialize Whalley-Wilmott's hedging strategy of a derivative. The ``forward`` method returns the next hedge ratio. @@ -21,18 +21,18 @@ class WhalleyWilmott(Module): .. math:: - w = \\left( \\frac{3 c \\Gamma^2 S}{2 a} \\right)^{1 / 3} \,, + w = \left( \frac{3 c \Gamma^2 S}{2 a} \right)^{1 / 3} \,, where :math:`c` is the transaction cost rate, - :math:`\\Gamma` is the gamma of the derivative, + :math:`\Gamma` is the gamma of the derivative, :math:`S` is the spot price of the underlying instrument, and :math:`a` is the risk-aversion coefficient of the exponential utility. Note: A backward computation for this module generates ``nan`` - if the :math:`\\Gamma` of the derivative is too small. - This is because the output is proportional to :math:`\\Gamma^{2 / 3}` - of which gradient diverges for :math:`\\Gamma \\to 0`. + if the :math:`\Gamma` of the derivative is too small. + This is because the output is proportional to :math:`\Gamma^{2 / 3}` + of which gradient diverges for :math:`\Gamma \to 0`. A ``dtype`` with higher precision may alleviate this problem. References: @@ -48,9 +48,9 @@ class WhalleyWilmott(Module): a (float, default=1.0): Risk aversion parameter in exponential utility. Shape: - - Input: :math:`(N, *, H_{\\text{in}})` where + - Input: :math:`(N, *, H_{\text{in}})` where :math:`*` means any number of additional dimensions and - :math:`H_{\\text{in}}` is the number of input features. + :math:`H_{\text{in}}` is the number of input features. See :meth:`inputs()` for the names of input features. - Output: :math:`(N, *, 1)`. @@ -130,15 +130,15 @@ def forward(self, input: Tensor) -> Tensor: return self.clamp(prev_hedge, min=min, max=max) def width(self, input: Tensor) -> Tensor: - """Returns half-width of the no-transaction band. + r"""Returns half-width of the no-transaction band. Args: input (Tensor): The input tensor. Shape: - - Input: :math:`(N, *, H_{\\text{in}} - 1)` where + - Input: :math:`(N, *, H_{\text{in}} - 1)` where :math:`*` means any number of additional dimensions and - :math:`H_{\\text{in}}` is the number of input features. + :math:`H_{\text{in}}` is the number of input features. See :meth:`inputs()` for the names of input features. - Output: :math:`(N, *, 1)` diff --git a/pfhedge/stochastic/cir.py b/pfhedge/stochastic/cir.py index 57ed26e6..206c5ebc 100644 --- a/pfhedge/stochastic/cir.py +++ b/pfhedge/stochastic/cir.py @@ -1,6 +1,5 @@ from typing import Optional from typing import Tuple -from typing import Union from typing import cast import torch @@ -20,13 +19,13 @@ def generate_cir( dtype: Optional[torch.dtype] = None, device: Optional[torch.device] = None, ) -> Tensor: - """Returns time series following Cox-Ingersoll-Ross process. + r"""Returns time series following Cox-Ingersoll-Ross process. The time evolution of the process is given by: .. math:: - dX(t) = \\kappa (\\theta - X(t)) dt + \\sigma \\sqrt{X(t)} dW(t) \\,. + dX(t) = \kappa (\theta - X(t)) dt + \sigma \sqrt{X(t)} dW(t) \,. Time series is generated by Andersen's QE-M method (See Reference for details). @@ -42,9 +41,9 @@ def generate_cir( the time series. This is specified by a tuple :math:`(X(0),)`. It also accepts a :class:`torch.Tensor` or a :class:`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`. + 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 @@ -105,7 +104,7 @@ def generate_cir( s2 = v * (sigma ** 2) * exp * (1 - exp) / kappa + theta * (sigma ** 2) * ( (1 - exp).square() ) / (2 * kappa) - psi = s2 / (m.square() + EPSILON) + psi = s2 / m.square().clamp(min=EPSILON) # Compute V(t + dt) where psi <= PSI_CRIT: Eq(23, 27, 28) b = ((2 / psi) - 1 + (2 / psi).sqrt() * (2 / psi - 1).sqrt()).sqrt() @@ -115,8 +114,8 @@ def generate_cir( # Compute V(t + dt) where psi > PSI_CRIT: Eq(25) u = rand[:, i_step] p = (psi - 1) / (psi + 1) - beta = (1 - p) / (m + EPSILON) - pinv = ((1 - p) / (1 - u + EPSILON)).log() / beta + beta = (1 - p) / m.clamp(min=EPSILON) + pinv = ((1 - p) / (1 - u).clamp(min=EPSILON)).log() / beta next_1 = torch.where(u > p, pinv, torch.zeros_like(u)) output[:, i_step + 1] = torch.where(psi <= PSI_CRIT, next_0, next_1) diff --git a/pyproject.toml b/pyproject.toml index 02bfded5..f39c0e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pfhedge" -version = "0.13.1" +version = "0.13.2" description = "Deep Hedging in PyTorch" authors = ["Shota Imaki "] license = "MIT" @@ -17,6 +17,11 @@ black = "^21.4b0" isort = "^5.7.0" mypy = "^0.910" pytest-cov = "^2.12.1" +Sphinx = "^4.2.0" +sphinx-autobuild = "^2021.3.14" +sphinx-copybutton = "^0.4.0" +furo = "^2021.9.22" +codecov = "^2.1.12" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/features/test_features.py b/tests/features/test_features.py index 3b2fc088..f0559859 100644 --- a/tests/features/test_features.py +++ b/tests/features/test_features.py @@ -832,8 +832,9 @@ def test_value(self, strike, log): assert_close(result, expect) result = f.get() - expect = expect.log() if log else expect - expect = spot.unsqueeze(-1) + expect = spot + expect = expect.log_() if log else expect + expect = expect.unsqueeze(-1) assert_close(result, expect) def test_str(self): @@ -876,8 +877,9 @@ def test_value(self, strike, log): assert_close(result, expect) result = f.get() - expect = expect.log() if log else expect - expect = spot.unsqueeze(-1) + expect = spot + expect = expect.log_() if log else expect + expect = expect.unsqueeze(-1) assert_close(result, expect) def test_str(self): @@ -886,5 +888,5 @@ def test_str(self): def test_is_state_dependent(self): derivative = EuropeanOption(BrownianStock()) hedger = Hedger(Naked(), inputs=["empty"]) - f = UnderlierSpot().of(derivative, hedger) + f = Spot().of(derivative, hedger) assert not f.is_state_dependent()