Skip to content

Commit

Permalink
Release/0.20.0 (#572)
Browse files Browse the repository at this point in the history
* ENH: Support PyTorch builtin loss functions for hedging loss (#568) (#569)

  * You can now use PyTorch built-in loss function modules as `criterion` of `Hedger`.
  
  * Migration guide: If you have defined your own `HedgeLoss`, please modify the signatures of its methods as `forward(self, input) -> forward(input, target=0.0)` and `cash(self, input) -> cash(input, target=0.0)`.

* ENH: Suppprt multiple hedges in `nn.functional.pl` (#571)

* DOC: Add examples to Black-Scholes functionals (#566)

* MAINT: Use `cast_state` (#567)

* Bumping version from 0.19.2 to 0.20.0 (#573)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
Shota Imaki and github-actions[bot] authored Mar 31, 2022
1 parent 39143c5 commit a5ba9d0
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 202 deletions.
1 change: 1 addition & 0 deletions docs/source/nn.functional.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Other Functions
d2
ncdf
npdf
pl
realized_variance
realized_volatility
svi_variance
Expand Down
168 changes: 134 additions & 34 deletions pfhedge/nn/functional.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from math import ceil
from math import pi as kPI
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
Expand Down Expand Up @@ -374,7 +375,6 @@ def realized_variance(input: Tensor, dt: TensorOrScalar) -> Tensor:
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
Expand Down Expand Up @@ -419,28 +419,36 @@ def realized_volatility(input: Tensor, dt: Union[Tensor, float]) -> Tensor:
return realized_variance(input, dt=dt).sqrt()


def terminal_value(
def pl(
spot: Tensor,
unit: Tensor,
cost: float = 0.0,
cost: Optional[List[float]] = None,
payoff: Optional[Tensor] = None,
deduct_first_cost: bool = True,
deduct_final_cost: bool = False,
) -> Tensor:
r"""Returns the terminal portfolio value.
r"""Returns the final profit and loss of hedging.
The terminal value of a hedger's portfolio is given by
For
hedging instruments indexed by :math:`h = 1, \dots, H` and
time steps :math:`i = 1, \dots, T`,
the final profit and loss is given by
.. math::
\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}
- Z
+ \sum_{h = 1}^{H} \sum_{t = 1}^{T} \left[
\delta^{(h)}_{t - 1} (S^{(h)}_{t} - S^{(h)}_{t - 1})
- c^{(h)} |\delta^{(h)}_{t} - \delta^{(h)}_{t - 1}| S^{(h)}_{t}
\right] ,
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
of shares held at each time step.
We define :math:`\delta_0 = 0` for notational convenience.
where
:math:`Z` is the payoff of the derivative.
For each hedging instrument,
:math:`\{S^{(h)}_t ; t = 1, \dots, T\}` is the spot price,
:math:`\{\delta^{(h)}_t ; t = 1, \dots, T\}` is the number of shares
held at each time step.
We define :math:`\delta^{(h)}_0 = 0` for notational convenience.
A hedger sells the derivative to its customer and
obliges to settle the payoff at maturity.
Expand All @@ -458,40 +466,66 @@ def terminal_value(
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`.
cost (float, default=0.0): The proportional transaction cost rate of
the underlying asset :math:`c`.
cost (list[float], default=None): The proportional transaction cost rate of
the underlying assets.
payoff (torch.Tensor, optional): The payoff of the derivative :math:`Z`.
deduct_first_cost (bool, default=True): Whether to deduct the transaction
cost of the stock at the first time step.
If ``False``, :math:`- c |\delta_0| S_1` is omitted the above
equation of the terminal value.
Shape:
- spot: :math:`(N, *, T)` where
:math:`T` is the number of time steps and
:math:`*` means any number of additional dimensions.
- unit: :math:`(N, *, T)`
- payoff: :math:`(N, *)`
- output: :math:`(N, *)`.
- spot: :math:`(N, H, T)` where
:math:`N` is the number of paths,
:math:`H` is the number of hedging instruments, and
:math:`T` is the number of time steps.
- unit: :math:`(N, H, T)`
- payoff: :math:`(N)`
- output: :math:`(N)`.
Returns:
torch.Tensor
"""
# TODO(simaki): Support deduct_final_cost=True
assert not deduct_final_cost, "not supported"

if spot.size() != unit.size():
raise RuntimeError(f"unmatched sizes: spot {spot.size()}, unit {unit.size()}")
if payoff is not None and spot.size()[:-1] != payoff.size():
raise RuntimeError(
f"unmatched sizes: spot {spot.size()}, payoff {payoff.size()}"
)
if payoff is not None:
if payoff.dim() != 1 or spot.size(0) != payoff.size(0):
raise RuntimeError(
f"unmatched sizes: spot {spot.size()}, payoff {payoff.size()}"
)

output = unit[..., :-1].mul(spot.diff(dim=-1)).sum(dim=(-2, -1))

value = unit[..., :-1].mul(spot.diff(dim=-1)).sum(-1)
value += -cost * unit.diff(dim=-1).abs().mul(spot[..., 1:]).sum(-1)
if payoff is not None:
value -= payoff
if deduct_first_cost:
value -= cost * unit[..., 0].abs() * spot[..., 0]
output -= payoff

return value
if cost is not None:
c = torch.tensor(cost).unsqueeze(0).unsqueeze(-1)
output -= (spot[..., 1:] * unit.diff(dim=-1).abs() * c).sum(dim=(-2, -1))
if deduct_first_cost:
output -= (spot[..., [0]] * unit[..., [0]].abs() * c).sum(dim=(-2, -1))

return output


def terminal_value(
spot: Tensor,
unit: Tensor,
cost: Optional[List[float]] = None,
payoff: Optional[Tensor] = None,
deduct_first_cost: bool = True,
) -> Tensor:
"""Alias for :func:`pfhedge.nn.functional.pl`."""
return pl(
spot=spot,
unit=unit,
cost=cost,
payoff=payoff,
deduct_first_cost=deduct_first_cost,
)


def ncdf(input: Tensor) -> Tensor:
Expand Down Expand Up @@ -727,7 +761,29 @@ def bs_european_price(
) -> Tensor:
"""Returns Black-Scholes price of a European option.
See :func:`pfhedge.nn.BSEuropeanOption.price` for details.
.. seealso::
- :func:`pfhedge.nn.BSEuropeanOption.price`
Args:
log_moneyness (torch.Tensor, optional): Log moneyness of the underlying asset.
time_to_maturity (torch.Tensor, optional): Time to expiry of the option.
volatility (torch.Tensor, optional): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)` where
:math:`*` means any number of additional dimensions.
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Returns:
torch.Tensor
Examples:
>>> from pfhedge.nn.functional import bs_european_price
...
>>> bs_european_price(torch.tensor([-0.1, 0.0, 0.1]), 1.0, 0.2)
tensor([0.0375, 0.0797, 0.1467])
"""
s, t, v = broadcast_all(log_moneyness, time_to_maturity, volatility)

Expand All @@ -746,7 +802,29 @@ def bs_european_delta(
) -> Tensor:
"""Returns Black-Scholes delta of a European option.
See :func:`pfhedge.nn.BSEuropeanOption.delta` for details.
.. seealso::
- :func:`pfhedge.nn.BSEuropeanOption.delta`
Args:
log_moneyness (torch.Tensor, optional): Log moneyness of the underlying asset.
time_to_maturity (torch.Tensor, optional): Time to expiry of the option.
volatility (torch.Tensor, optional): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)` where
:math:`*` means any number of additional dimensions.
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Returns:
torch.Tensor
Examples:
>>> from pfhedge.nn.functional import bs_european_delta
...
>>> bs_european_delta(torch.tensor([-0.1, 0.0, 0.1]), 1.0, 0.2)
tensor([0.3446, 0.5398, 0.7257])
"""
s, t, v = broadcast_all(log_moneyness, time_to_maturity, volatility)

Expand All @@ -764,7 +842,29 @@ def bs_european_gamma(
) -> Tensor:
"""Returns Black-Scholes gamma of a European option.
See :func:`pfhedge.nn.BSEuropeanOption.gamma` for details.
.. seealso::
- :func:`pfhedge.nn.BSEuropeanOption.gamma`
Args:
log_moneyness (torch.Tensor, optional): Log moneyness of the underlying asset.
time_to_maturity (torch.Tensor, optional): Time to expiry of the option.
volatility (torch.Tensor, optional): Volatility of the underlying asset.
Shape:
- log_moneyness: :math:`(N, *)` where
:math:`*` means any number of additional dimensions.
- time_to_maturity: :math:`(N, *)`
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Returns:
torch.Tensor
Examples:
>>> from pfhedge.nn.functional import bs_european_gamma
...
>>> bs_european_gamma(torch.tensor([-0.1, 0.0, 0.1]), 1.0, 0.2)
tensor([2.0350, 1.9848, 1.5076])
"""
s, t, v = broadcast_all(log_moneyness, time_to_maturity, volatility)
spot = strike * s.exp()
Expand Down
14 changes: 6 additions & 8 deletions pfhedge/nn/modules/bs/lookback.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ def gamma(
torch.Tensor
Note:
args are not optional if it doesn't accept derivative in this initialization.
Arguments are not optional if it doesn't accept derivative in this initialization.
"""
(
log_moneyness,
Expand Down Expand Up @@ -319,7 +319,7 @@ def vega(
torch.Tensor
Note:
args are not optional if it doesn't accept derivative in this initialization.
- Arguments are not optional if it doesn't accept derivative in this initialization.
"""
(
log_moneyness,
Expand Down Expand Up @@ -366,14 +366,12 @@ def theta(
- volatility: :math:`(N, *)`
- output: :math:`(N, *)`
Note:
Risk-free rate is set to zero.
Returns:
torch.Tensor
Note:
args are not optional if it doesn't accept derivative in this initialization.
- Risk-free rate is set to zero.
- Arguments are not optional if it doesn't accept derivative in this initialization.
"""
(
log_moneyness,
Expand Down Expand Up @@ -424,9 +422,9 @@ def implied_volatility(
torch.Tensor
Note:
args are not optional if it doesn't accept derivative in this initialization.
price seems optional in typing, but it isn't. It is set for the compatibility to the previous versions.
- Arguments are not optional if it doesn't accept derivative in this initialization.
"""
# price seems optional in typing, but it isn't. It is set for the compatibility to the previous versions.
(log_moneyness, time_to_maturity) = acquire_params_from_derivative_0(
self.derivative, log_moneyness, time_to_maturity
)
Expand Down
Loading

0 comments on commit a5ba9d0

Please sign in to comment.