Skip to content

Commit 00f5628

Browse files
authored
Correct laplace (#33)
* Update version * Correct distributions in support.py * Update test_core.py * Black source * Update CHANGELOG.md
1 parent c3f34bf commit 00f5628

File tree

6 files changed

+69
-75
lines changed

6 files changed

+69
-75
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 1.3.2 (2024-11-30)
4+
5+
- Changed Laplace distribution implementation in `create_price_samples` and `get_pop` functions in support.py.
6+
37
## 1.3.1 (2024-09-27)
48

59
- discriminator="type" removed from strategy: list[StrategyLeg] = Field(..., min_length=1) in models.py, since

optionlab/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing
22

33

4-
VERSION = "1.3.1"
4+
VERSION = "1.3.2"
55

66

77
if typing.TYPE_CHECKING:

optionlab/engine.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def _run(data: EngineData) -> EngineData:
183183
data._profit_ranges = get_profit_range(data.stock_price_array, data.strategy_profit)
184184

185185
pop_inputs: ProbabilityOfProfitInputs | ProbabilityOfProfitArrayInputs
186+
186187
if inputs.distribution in ("normal", "laplace", "black-scholes"):
187188
pop_inputs = ProbabilityOfProfitInputs(
188189
source=inputs.distribution, # type: ignore

optionlab/support.py

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from functools import lru_cache
44

5-
import numpy as np
65
from numpy import ndarray, exp, abs, round, diff, flatnonzero, arange, inf
76
from numpy.lib.scimath import log, sqrt
87
from numpy.random import normal, laplace
@@ -25,9 +24,9 @@ def get_pl_profile(
2524
x: float,
2625
val: float,
2726
n: int,
28-
s: np.ndarray,
27+
s: ndarray,
2928
commission: float = 0.0,
30-
) -> tuple[np.ndarray, float]:
29+
) -> tuple[ndarray, float]:
3130
"""
3231
get_pl_profile(option_type, action, x, val, n, s, commission) -> returns the profit/loss
3332
profile and cost of an option trade at expiration.
@@ -60,8 +59,8 @@ def get_pl_profile(
6059

6160

6261
def get_pl_profile_stock(
63-
s0: float, action: Action, n: int, s: np.ndarray, commission: float = 0.0
64-
) -> tuple[np.ndarray, float]:
62+
s0: float, action: Action, n: int, s: ndarray, commission: float = 0.0
63+
) -> tuple[ndarray, float]:
6564
"""
6665
get_pl_profile_stock(s0, action, n, s, commission) -> returns the profit/loss
6766
profile and cost of a stock position.
@@ -94,7 +93,7 @@ def get_pl_profile_bs(
9493
target_to_maturity_years: float,
9594
volatility: float,
9695
n: int,
97-
s: np.ndarray,
96+
s: ndarray,
9897
y: float = 0.0,
9998
commission: float = 0.0,
10099
):
@@ -137,7 +136,7 @@ def get_pl_profile_bs(
137136

138137

139138
@lru_cache
140-
def create_price_seq(min_price: float, max_price: float) -> np.ndarray:
139+
def create_price_seq(min_price: float, max_price: float) -> ndarray:
141140
"""
142141
create_price_seq(min_price, max_price) -> generates a sequence of stock prices
143142
from 'min_price' to 'max_price' with increment $0.01.
@@ -164,7 +163,7 @@ def create_price_samples(
164163
distribution: Distribution = "black-scholes",
165164
y: float = 0.0,
166165
n: int = 100_000,
167-
) -> np.ndarray:
166+
) -> ndarray:
168167
"""
169168
create_price_samples(s0, volatility, years_to_maturity, r, distribution, y, n) -> generates
170169
random stock prices at maturity according to a statistical distribution.
@@ -177,28 +176,26 @@ def create_price_samples(
177176
r: annualized risk-free interest rate (default is 0.01). Used only if
178177
distribution is 'black-scholes'.
179178
distribution: statistical distribution used to generate random stock prices
180-
at maturity. It can be 'black-scholes' (default), 'normal' or
181-
'laplace'.
179+
at maturity. It can be 'black-scholes' (default; 'normal' is
180+
equivalent) or 'laplace'.
182181
y: annualized dividend yield (default is zero).
183182
n: number of randomly generated terminal prices.
184183
"""
185-
if distribution == "normal":
186-
return exp(normal(log(s0), volatility * sqrt(years_to_maturity), n))
187-
elif distribution == "black-scholes":
188-
drift = (r - y - 0.5 * volatility * volatility) * years_to_maturity
184+
drift = (r - y - 0.5 * volatility * volatility) * years_to_maturity
189185

186+
if distribution in ("black-scholes", "normal"):
190187
return exp(normal((log(s0) + drift), volatility * sqrt(years_to_maturity), n))
191188
elif distribution == "laplace":
192189
return exp(
193-
laplace(log(s0), (volatility * sqrt(years_to_maturity)) / sqrt(2.0), n)
190+
laplace(
191+
(log(s0) + drift), (volatility * sqrt(years_to_maturity)) / sqrt(2.0), n
192+
)
194193
)
195194
else:
196195
raise ValueError("Distribution not implemented yet!")
197196

198197

199-
def get_profit_range(
200-
s: np.ndarray, profit: np.ndarray, target: float = 0.01
201-
) -> list[Range]:
198+
def get_profit_range(s: ndarray, profit: ndarray, target: float = 0.01) -> list[Range]:
202199
"""
203200
get_profit_range(s, profit, target) -> returns pairs of stock prices, as a list,
204201
for which an option trade is expected to get the desired profit in between.
@@ -252,13 +249,12 @@ def get_pop(
252249
get_pop(profit_ranges, source, kwargs) -> estimates the probability of profit
253250
(PoP) of an option trade.
254251
255-
* For 'source="normal"' or 'source="laplace"': the probability of
256-
profit is calculated assuming either a (log)normal or a (log)Laplace
257-
distribution of terminal stock prices at maturity.
252+
* For 'source="black-scholes"' (default; 'normal' is equivalent): the probability
253+
of profit is calculated assuming a (log)normal distribution as implemented in
254+
the Black-Scholes model.
258255
259-
* For 'source="black-scholes"' (default): the probability of profit
260-
is calculated assuming a (log)normal distribution with risk neutrality
261-
as implemented in the Black-Scholes model.
256+
* For 'source="laplace"': the probability of profit is calculated assuming
257+
a (log)Laplace distribution of terminal stock prices at maturity.
262258
263259
* For 'source="array"': the probability of profit is calculated
264260
from a 1D numpy array of stock prices typically at maturity generated
@@ -282,24 +278,17 @@ def get_pop(
282278
stock_price = inputs.stock_price
283279
volatility = inputs.volatility
284280
years_to_maturity = inputs.years_to_maturity
285-
drift = 0.0
286-
287-
if inputs.source == "black-scholes":
288-
r = (
289-
inputs.interest_rate or 0.0
290-
) # 'or' just for typing purposes, as `interest_rate` must be non-zero
291-
y = inputs.dividend_yield
292-
293-
drift = (r - y - 0.5 * volatility * volatility) * years_to_maturity
294-
281+
r = (
282+
inputs.interest_rate or 0.0
283+
) # 'or' just for typing purposes, as `interest_rate` must be non-zero
284+
y = inputs.dividend_yield
285+
drift = (r - y - 0.5 * volatility * volatility) * years_to_maturity
295286
sigma = volatility * sqrt(years_to_maturity)
296287

297288
if sigma == 0.0:
298289
sigma = 1e-10
299290

300-
beta = 0.0
301-
if inputs.source == "laplace":
302-
beta = sigma / sqrt(2.0)
291+
beta = sigma / sqrt(2.0)
303292

304293
for p_range in profit_ranges:
305294
lval = p_range[0]
@@ -314,8 +303,8 @@ def get_pop(
314303
) - stats.norm.cdf((log(lval / stock_price) - drift) / sigma)
315304
else:
316305
pop += stats.laplace.cdf(
317-
log(hval / stock_price) / beta
318-
) - stats.laplace.cdf(log(lval / stock_price) / beta)
306+
(log(hval / stock_price) - drift) / beta
307+
) - stats.laplace.cdf((log(lval / stock_price) - drift) / beta)
319308

320309
elif isinstance(inputs, ProbabilityOfProfitArrayInputs):
321310
stocks = inputs.array
@@ -338,8 +327,8 @@ def get_pop(
338327

339328

340329
def _get_pl_option(
341-
option_type: OptionType, opvalue: float, action: Action, s: np.ndarray, x: float
342-
) -> np.ndarray:
330+
option_type: OptionType, opvalue: float, action: Action, s: ndarray, x: float
331+
) -> ndarray:
343332
"""
344333
getPLoption(option_type,opvalue,action,s,x) -> returns the profit (P) or loss
345334
(L) per option of an option trade at expiration.
@@ -361,7 +350,7 @@ def _get_pl_option(
361350
raise ValueError("Action must be either 'sell' or 'buy'!")
362351

363352

364-
def _get_payoff(option_type: OptionType, s: np.ndarray, x: float) -> np.ndarray:
353+
def _get_payoff(option_type: OptionType, s: ndarray, x: float) -> ndarray:
365354
"""
366355
get_payoff(option_type, s, x) -> returns the payoff of an option trade at expiration.
367356
@@ -380,7 +369,7 @@ def _get_payoff(option_type: OptionType, s: np.ndarray, x: float) -> np.ndarray:
380369
raise ValueError("Option type must be either 'call' or 'put'!")
381370

382371

383-
def _get_pl_stock(s0: float, action: Action, s: np.ndarray) -> np.ndarray:
372+
def _get_pl_stock(s0: float, action: Action, s: ndarray) -> ndarray:
384373
"""
385374
get_pl_stock(s0,action,s) -> returns the profit (P) or loss (L) of a stock
386375
position.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "optionlab"
3-
version = "1.3.1"
3+
version = "1.3.2"
44
description = "Evaluate option strategies"
55
authors = ["Roberto Gomes, PhD <[email protected]>"]
66
readme = "README.md"

tests/test_core.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -326,35 +326,35 @@ def test_100_itm_with_compute_expectation(nvidia):
326326
)
327327

328328

329-
def test_covered_call_w_normal_distribution(nvidia):
330-
inputs = Inputs.model_validate(
331-
nvidia
332-
| {
333-
"distribution": "normal",
334-
# The covered call strategy is defined
335-
"strategy": [
336-
{"type": "stock", "n": 100, "action": "buy"},
337-
{
338-
"type": "call",
339-
"strike": 185.0,
340-
"premium": 4.1,
341-
"n": 100,
342-
"action": "sell",
343-
"expiration": nvidia["target_date"],
344-
},
345-
],
346-
}
347-
)
348-
349-
outputs = run_strategy(inputs)
350-
351-
# Print useful information on screen
352-
assert isinstance(outputs, Outputs)
353-
assert outputs.model_dump(
354-
exclude={"data", "inputs"}, exclude_none=True
355-
) == pytest.approx(
356-
COVERED_CALL_RESULT | {"probability_of_profit": 0.565279550918542}
357-
)
329+
# def test_covered_call_w_normal_distribution(nvidia):
330+
# inputs = Inputs.model_validate(
331+
# nvidia
332+
# | {
333+
# "distribution": "normal",
334+
# # The covered call strategy is defined
335+
# "strategy": [
336+
# {"type": "stock", "n": 100, "action": "buy"},
337+
# {
338+
# "type": "call",
339+
# "strike": 185.0,
340+
# "premium": 4.1,
341+
# "n": 100,
342+
# "action": "sell",
343+
# "expiration": nvidia["target_date"],
344+
# },
345+
# ],
346+
# }
347+
# )
348+
349+
# outputs = run_strategy(inputs)
350+
351+
# # Print useful information on screen
352+
# assert isinstance(outputs, Outputs)
353+
# assert outputs.model_dump(
354+
# exclude={"data", "inputs"}, exclude_none=True
355+
# ) == pytest.approx(
356+
# COVERED_CALL_RESULT | {"probability_of_profit": 0.565279550918542}
357+
# )
358358

359359

360360
def test_covered_call_w_laplace_distribution(nvidia):
@@ -384,5 +384,5 @@ def test_covered_call_w_laplace_distribution(nvidia):
384384
assert outputs.model_dump(
385385
exclude={"data", "inputs"}, exclude_none=True
386386
) == pytest.approx(
387-
COVERED_CALL_RESULT | {"probability_of_profit": 0.6037062828141202}
387+
COVERED_CALL_RESULT | {"probability_of_profit": 0.5772025728573296}
388388
)

0 commit comments

Comments
 (0)