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

1"""Payoff functions for vanilla European option contracts.""" 

2 

3import math 

4 

5import numpy as np 

6import numpy.typing as npt 

7 

8 

9def _check_strike(strike: float) -> None: 

10 """Reject a nonsensical strike with a clear error. 

11 

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. 

17 

18 Args: 

19 strike: The proposed strike price. 

20 

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) 

31 

32 

33def call_payoff(spot: npt.ArrayLike, strike: float) -> np.float64 | npt.NDArray[np.float64]: 

34 """Return the expiry payoff of a European call option. 

35 

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. 

41 

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. 

47 

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``. 

51 

52 Raises: 

53 ValueError: If ``strike`` is not finite (NaN or infinite), or is 

54 negative. 

55 

56 Examples: 

57 A scalar spot gives a scalar payoff: 

58 

59 >>> call_payoff(120.0, strike=100.0) 

60 np.float64(20.0) 

61 

62 Out of the money the payoff floors at zero rather than going negative: 

63 

64 >>> call_payoff(80.0, strike=100.0) 

65 np.float64(0.0) 

66 

67 An array-like spot is evaluated element-wise: 

68 

69 >>> call_payoff([80.0, 100.0, 130.0], strike=100.0) 

70 array([ 0., 0., 30.]) 

71 

72 An unusable strike fails fast: 

73 

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) 

81 

82 

83def put_payoff(spot: npt.ArrayLike, strike: float) -> np.float64 | npt.NDArray[np.float64]: 

84 """Return the expiry payoff of a European put option. 

85 

86 Mirrors :func:`call_payoff`, including its return-type convention: scalar in, 

87 scalar out; array-like in, array out. 

88 

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. 

94 

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``. 

98 

99 Raises: 

100 ValueError: If ``strike`` is not finite (NaN or infinite), or is 

101 negative. 

102 

103 Examples: 

104 A scalar spot gives a scalar payoff: 

105 

106 >>> put_payoff(80.0, strike=100.0) 

107 np.float64(20.0) 

108 

109 Out of the money the payoff floors at zero: 

110 

111 >>> put_payoff(120.0, strike=100.0) 

112 np.float64(0.0) 

113 

114 An array-like spot is evaluated element-wise: 

115 

116 >>> put_payoff([70.0, 100.0, 130.0], strike=100.0) 

117 array([30., 0., 0.]) 

118 

119 A non-finite strike is rejected alongside a negative one: 

120 

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)