Skip to content

Commit

Permalink
Correct expected profit (#43)
Browse files Browse the repository at this point in the history
* Update .md files

* Update examples
  • Loading branch information
rgaveiga authored Jan 26, 2025
1 parent f05b0cc commit 217c7cd
Show file tree
Hide file tree
Showing 13 changed files with 62 additions and 116 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 1.4.2 (2025-01-25)

- Removed `expected_profit` and `expected_loss` calculation from `_get_pop_bs` in support.py; implementation was not correct, giving wrong results when compared with Monte Carlo simulations

## 1.4.1 (2025-01-04)

- Removed a small bug in `create_price_seq` in support.py
Expand Down
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ a user-defined target date, the range of stock prices for which the strategy is
profitable (i.e., generating a return greater than \$0.01), the Greeks associated with
each leg of the strategy using the Black-Sholes model, the resulting debit or credit on the
trading account, the maximum and minimum returns within a specified lower and higher price
range of the underlying asset, and an estimate of the strategy's probability of profit,
expected profit and expected loss.
range of the underlying asset, and an estimate of the strategy's probability of profit.

The probability of profit (PoP), expected profit and expected loss at the user-defined target
date for the strategy are calculated by default using the Black-Scholes model. Alternatively,
the user can provide an array of underlying asset prices following a distribution other than
the normal (e.g. Laplace) or model other than the Black-Scholes model (e.g. Heston model) that
will be used in the calculations.
The probability of profit (PoP) on the user-defined target date for the strategy is calculated
by default using the Black-Scholes model. Alternatively, the user can provide an array of
underlying asset prices following a distribution other than the normal (e.g. Laplace) or
model other than the Black-Scholes model (e.g. Heston model) that will be used in the calculations.

Despite the code having been developed with option strategies in mind, it can also be
used for strategies that combine options with stocks and/or take into account the
Expand Down Expand Up @@ -91,7 +89,8 @@ Jupyter notebooks in the **examples** directory.
Contributions are definitely welcome. However, it should be mentioned that this
repository uses [poetry](https://python-poetry.org/) as a package manager and
[git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) with
[pre-commit](https://pre-commit.com/) to customize actions on the repository.
[pre-commit](https://pre-commit.com/) to customize actions on the repository. Source
code must be formatted using [black](https://github.com/psf/black).

## Disclaimer

Expand Down
4 changes: 2 additions & 2 deletions examples/black_scholes_calculator.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"output_type": "stream",
"text": [
"Python version: 3.11.9 | packaged by Anaconda, Inc. | (main, Apr 19 2024, 16:40:41) [MSC v.1916 64 bit (AMD64)]\n",
"OptionLab version: 1.4.1\n"
"OptionLab version: 1.4.2\n"
]
}
],
Expand Down Expand Up @@ -106,7 +106,7 @@
"output_type": "stream",
"text": [
"CPU times: total: 0 ns\n",
"Wall time: 5.82 ms\n"
"Wall time: 0 ns\n"
]
}
],
Expand Down
10 changes: 4 additions & 6 deletions examples/calendar_spread.ipynb

Large diffs are not rendered by default.

32 changes: 12 additions & 20 deletions examples/call_spread.ipynb

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions examples/covered_call.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"output_type": "stream",
"text": [
"Python version: 3.11.9 | packaged by Anaconda, Inc. | (main, Apr 19 2024, 16:40:41) [MSC v.1916 64 bit (AMD64)]\n",
"OptionLab version: 1.4.1\n"
"OptionLab version: 1.4.2\n"
]
}
],
Expand Down Expand Up @@ -119,8 +119,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: total: 46.9 ms\n",
"Wall time: 52.4 ms\n"
"CPU times: total: 203 ms\n",
"Wall time: 205 ms\n"
]
}
],
Expand All @@ -142,7 +142,7 @@
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x225b149b510>"
"<matplotlib.legend.Legend at 0x2e55fd93910>"
]
},
"execution_count": 5,
Expand Down Expand Up @@ -195,8 +195,6 @@
"text": [
"Probability of profit: 0.522421406650333\n",
"Profit ranges: [(162.9, inf)]\n",
"Expected profit: 1028.0\n",
"Expected loss: -919.0\n",
"Per leg cost: [-16404.0, 114.99999999999999]\n",
"Strategy cost: -16289.0\n",
"Minimum return in the domain: -8087.0\n",
Expand Down
18 changes: 7 additions & 11 deletions examples/naked_call.ipynb

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion log

This file was deleted.

2 changes: 1 addition & 1 deletion optionlab/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import typing


VERSION = "1.4.1"
VERSION = "1.4.2"


if typing.TYPE_CHECKING:
Expand Down
2 changes: 1 addition & 1 deletion optionlab/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ class Outputs(BaseModel):
def __str__(self):
s = ""

for key, value in self.dict(
for key, value in self.model_dump(
exclude={"data", "inputs"},
exclude_none=True,
exclude_defaults=True,
Expand Down
67 changes: 19 additions & 48 deletions optionlab/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Optional

import numpy as np
from numpy import abs, round, arange, sum, exp
from numpy import abs, round, arange
from numpy.lib.scimath import log, sqrt
from scipy import stats

Expand Down Expand Up @@ -379,9 +379,6 @@ def _get_pop_bs(
target.
"""

probability_of_reaching_target = 0.0
probability_of_missing_target = 0.0

expected_return_above_target = None
expected_return_below_target = None

Expand All @@ -392,52 +389,26 @@ def _get_pop_bs(
)

for i, t in enumerate(profit_range):
prob = []
exp_profit = []

if t == [(0.0, 0.0)]:
continue

for p_range in t:
lval = log(p_range[0]) if p_range[0] > 0.0 else -float("inf")
hval = log(p_range[1])
drift = (
inputs.interest_rate
- inputs.dividend_yield
- 0.5 * inputs.volatility * inputs.volatility
) * inputs.years_to_target_date
m = log(inputs.stock_price) + drift
w = stats.norm.cdf((hval - m) / sigma) - stats.norm.cdf((lval - m) / sigma)

if w > 0.0:
v = stats.norm.pdf((hval - m) / sigma) - stats.norm.pdf(
prob = 0.0

if t != [(0.0, 0.0)]:
for p_range in t:
lval = log(p_range[0]) if p_range[0] > 0.0 else -float("inf")
hval = log(p_range[1])
drift = (
inputs.interest_rate
- inputs.dividend_yield
- 0.5 * inputs.volatility * inputs.volatility
) * inputs.years_to_target_date
m = log(inputs.stock_price) + drift
prob += stats.norm.cdf((hval - m) / sigma) - stats.norm.cdf(
(lval - m) / sigma
)
exp_stock = round(
exp(m - sigma * v / w), 2
) # Using inverse Mills ratio

if exp_stock > 0.0 and exp_stock <= s.max():
exp_stock_index = np.where(s == exp_stock)[0][0]
exp_profit.append(profit[exp_stock_index])
prob.append(w)

if len(t) > 0:
prob_array = np.array(prob)
exp_profit_array = np.array(exp_profit)

if i == 0:
probability_of_reaching_target = sum(prob_array)
expected_return_above_target = round(
sum(exp_profit_array * prob_array) / probability_of_reaching_target,
2,
)
else:
probability_of_missing_target = sum(prob_array)
expected_return_below_target = round(
sum(exp_profit_array * prob_array) / probability_of_missing_target,
2,
)

if i == 0:
probability_of_reaching_target = prob
else:
probability_of_missing_target = prob

return (
probability_of_reaching_target,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "optionlab"
version = "1.4.1"
version = "1.4.2"
description = "Python library for evaluating options trading strategies"
authors = ["Roberto Gomes, PhD <[email protected]>"]
readme = "README.md"
Expand Down
11 changes: 0 additions & 11 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
COVERED_CALL_RESULT = {
"probability_of_profit": 0.5472008423945267,
"profit_ranges": [(164.9, float("inf"))],
"expected_profit": 2011.0,
"expected_loss": -1758.0,
"per_leg_cost": [-16899.0, 409.99999999999994],
"strategy_cost": -16489.0,
"minimum_return_in_the_domain": -9590.000000000002,
Expand All @@ -27,7 +25,6 @@
PROB_100_ITM_RESULT = {
"probability_of_profit": 1.0,
"profit_ranges": [(0.0, float("inf"))],
"expected_profit": 524.0,
"per_leg_cost": [-750.0, 990.0],
"strategy_cost": 240.0,
"minimum_return_in_the_domain": 240.0,
Expand All @@ -44,8 +41,6 @@
PROB_NAKED_CALL = {
"probability_of_profit": 0.8389215512144531,
"profit_ranges": [(0.0, 176.14)],
"expected_profit": 115.0,
"expected_loss": -707.0,
"per_leg_cost": [114.99999999999999],
"strategy_cost": 114.99999999999999,
"minimum_return_in_the_domain": -6991.999999999999,
Expand Down Expand Up @@ -187,8 +182,6 @@ def test_covered_call_w_prev_position(nvidia):
) == {
"probability_of_profit": 0.7048129541301169,
"profit_ranges": [(154.9, float("inf"))],
"expected_profit": 2566.0,
"expected_loss": -1390.0,
"per_leg_cost": [-15899.0, 409.99999999999994],
"strategy_cost": -15489.0,
"minimum_return_in_the_domain": -8590.000000000002,
Expand Down Expand Up @@ -311,8 +304,6 @@ def test_3_legs(nvidia):
) == {
"probability_of_profit": 0.6790581742719213,
"profit_ranges": [(156.6, float("inf"))],
"expected_profit": 2997.0,
"expected_loss": -1447.0,
"per_leg_cost": [-15899.0, -750.0, 990.0],
"strategy_cost": -15659.0,
"minimum_return_in_the_domain": -8760.000000000002,
Expand Down Expand Up @@ -488,8 +479,6 @@ def test_calendar_spread():
) == {
"probability_of_profit": 0.599111819020198,
"profit_ranges": [(118.87, 136.15)],
"expected_profit": 2960.0,
"expected_loss": -835.99,
"per_leg_cost": [4600.0, -5900.0],
"strategy_cost": -1300.0,
"minimum_return_in_the_domain": -1300.0000000000146,
Expand Down

0 comments on commit 217c7cd

Please sign in to comment.