Coverage for src/dummypy/payoffs.py: 100%
16 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-02 20:30 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-02 20:30 +0000
1"""Payoff functions for vanilla European option contracts."""
3import math
5import numpy as np
6import numpy.typing as npt
9def _check_strike(strike: float) -> None:
10 """Reject a nonsensical strike with a clear error.
12 Mirrors the validation style of :func:`dummypy.grid._check_n`: fail fast
13 with an actionable message rather than silently producing a meaningless
14 payoff. Infinities are rejected alongside NaN: an unchecked infinite
15 strike does not fail, it silently yields an infinite put payoff, which
16 is exactly the meaningless result this validator exists to prevent.
18 Args:
19 strike: The proposed strike price.
21 Raises:
22 ValueError: If ``strike`` is not finite (NaN or infinite), or is
23 negative.
24 """
25 if not math.isfinite(strike):
26 msg = f"strike must be a finite real number, got {strike}"
27 raise ValueError(msg)
28 if strike < 0:
29 msg = f"strike must be non-negative, got {strike}"
30 raise ValueError(msg)
33def call_payoff(spot: npt.ArrayLike, strike: float) -> np.float64 | npt.NDArray[np.float64]:
34 """Return the expiry payoff of a European call option.
36 The return type follows :func:`numpy.maximum`, which this delegates to: a
37 scalar ``spot`` yields a :class:`numpy.float64`, an array-like yields an
38 array. The two are deliberately *not* normalised to an array — doing so
39 would make ``call_payoff(120.0, 100.0)`` return ``array([20.])`` and
40 surprise every caller who passed a single number.
42 Args:
43 spot: Underlying spot price(s) at expiry. Scalars and array-likes
44 are both accepted.
45 strike: Strike price of the option. Must be a finite, non-negative
46 real number.
48 Returns:
49 Element-wise payoff ``max(spot - strike, 0)``: a :class:`numpy.float64`
50 for scalar ``spot``, or a ``float64`` array for array-like ``spot``.
52 Raises:
53 ValueError: If ``strike`` is not finite (NaN or infinite), or is
54 negative.
56 Examples:
57 A scalar spot gives a scalar payoff:
59 >>> call_payoff(120.0, strike=100.0)
60 np.float64(20.0)
62 Out of the money the payoff floors at zero rather than going negative:
64 >>> call_payoff(80.0, strike=100.0)
65 np.float64(0.0)
67 An array-like spot is evaluated element-wise:
69 >>> call_payoff([80.0, 100.0, 130.0], strike=100.0)
70 array([ 0., 0., 30.])
72 An unusable strike fails fast:
74 >>> call_payoff(120.0, strike=-1.0)
75 Traceback (most recent call last):
76 ...
77 ValueError: strike must be non-negative, got -1.0
78 """
79 _check_strike(strike)
80 return np.maximum(np.asarray(spot, dtype=np.float64) - strike, 0.0)
83def put_payoff(spot: npt.ArrayLike, strike: float) -> np.float64 | npt.NDArray[np.float64]:
84 """Return the expiry payoff of a European put option.
86 Mirrors :func:`call_payoff`, including its return-type convention: scalar in,
87 scalar out; array-like in, array out.
89 Args:
90 spot: Underlying spot price(s) at expiry. Scalars and array-likes
91 are both accepted.
92 strike: Strike price of the option. Must be a finite, non-negative
93 real number.
95 Returns:
96 Element-wise payoff ``max(strike - spot, 0)``: a :class:`numpy.float64`
97 for scalar ``spot``, or a ``float64`` array for array-like ``spot``.
99 Raises:
100 ValueError: If ``strike`` is not finite (NaN or infinite), or is
101 negative.
103 Examples:
104 A scalar spot gives a scalar payoff:
106 >>> put_payoff(80.0, strike=100.0)
107 np.float64(20.0)
109 Out of the money the payoff floors at zero:
111 >>> put_payoff(120.0, strike=100.0)
112 np.float64(0.0)
114 An array-like spot is evaluated element-wise:
116 >>> put_payoff([70.0, 100.0, 130.0], strike=100.0)
117 array([30., 0., 0.])
119 A non-finite strike is rejected alongside a negative one:
121 >>> put_payoff(80.0, strike=float("nan"))
122 Traceback (most recent call last):
123 ...
124 ValueError: strike must be a finite real number, got nan
125 """
126 _check_strike(strike)
127 return np.maximum(strike - np.asarray(spot, dtype=np.float64), 0.0)