From af4d95ac22f81e7eb6315204e045ae2a7ad6eb4b Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Feb 2024 19:19:54 -0500 Subject: [PATCH 1/3] Add option strategy generator --- .github/workflows/cicd.yml | 1 + README.md | 6 +- .../strategies-checkpoint.ipynb | 110 +++ examples/call_spread.ipynb | 4 +- examples/covered_call.ipynb | 6 +- examples/strategies.ipynb | 159 ++++ optionsmonkey/api.py | 9 +- optionsmonkey/engine.py | 805 ++++++------------ optionsmonkey/holidays.py | 1 + optionsmonkey/models.py | 237 ++++-- optionsmonkey/plot.py | 170 ++++ optionsmonkey/strategies.py | 146 ++++ optionsmonkey/utils.py | 4 + poetry.lock | 14 +- pyproject.toml | 1 + tests/conftest.py | 1 + tests/test_api.py | 7 +- tests/test_core.py | 17 +- 18 files changed, 1068 insertions(+), 630 deletions(-) create mode 100644 examples/.ipynb_checkpoints/strategies-checkpoint.ipynb create mode 100644 examples/strategies.ipynb create mode 100644 optionsmonkey/plot.py create mode 100644 optionsmonkey/strategies.py diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 5dc9cc7..d7c6318 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -22,4 +22,5 @@ jobs: run: | source venv/bin/activate mypy optionsmonkey/ --ignore-missing-imports + black . --check --diff --color pytest diff --git a/README.md b/README.md index f752ff8..fb02f49 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ For options, the dictionary should contain up to 7 keys: Either "buy" or "sell". It is mandatory. -- "prevpos" : float +- "prev_pos" : float Premium effectively paid or received in a previously opened position. If positive, it means that the position remains open and the payoff calculation takes this price into account, not the current price of the option. If negative, it means that the position is closed and the difference between this price and the current price is considered in the payoff calculation. @@ -174,7 +174,7 @@ For stocks, the dictionary should contain up to 4 keys: Either "buy" or "sell". It is mandatory. -- "prevpos" : float +- "prev_pos" : float Stock price effectively paid or received in a previously opened position. If positive, it means that the position remains open and the payoff calculation takes this price into account, not thecurrent price of the stock. If negative, it means that the position is closed and the difference between this price and the current price is considered in the payoff calculation. @@ -188,7 +188,7 @@ For a non-determined previously opened position to be closed, which might consis It must be "closed". It is mandatory. -- "prevpos" : float +- "prev_pos" : float The total value of the position to be closed, which can be positive if it made a profit or negative if it is a loss. It is mandatory. diff --git a/examples/.ipynb_checkpoints/strategies-checkpoint.ipynb b/examples/.ipynb_checkpoints/strategies-checkpoint.ipynb new file mode 100644 index 0000000..069345c --- /dev/null +++ b/examples/.ipynb_checkpoints/strategies-checkpoint.ipynb @@ -0,0 +1,110 @@ +{ + "cells": [ + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "{'language': 'en-US',\n 'region': 'US',\n 'quoteType': 'EQUITY',\n 'typeDisp': 'Equity',\n 'quoteSourceName': 'Delayed Quote',\n 'triggerable': True,\n 'customPriceAlertConfidence': 'HIGH',\n 'currency': 'USD',\n 'marketState': 'CLOSED',\n 'regularMarketChangePercent': -0.6149154,\n 'regularMarketPrice': 404.06,\n 'exchange': 'NMS',\n 'shortName': 'Microsoft Corporation',\n 'longName': 'Microsoft Corporation',\n 'messageBoardId': 'finmb_21835',\n 'exchangeTimezoneName': 'America/New_York',\n 'exchangeTimezoneShortName': 'EST',\n 'gmtOffSetMilliseconds': -18000000,\n 'market': 'us_market',\n 'esgPopulated': False,\n 'firstTradeDateMilliseconds': 511108200000,\n 'priceHint': 2,\n 'postMarketChangePercent': -0.17571436,\n 'postMarketTime': 1708131600,\n 'postMarketPrice': 403.35,\n 'postMarketChange': -0.70999146,\n 'regularMarketChange': -2.5,\n 'regularMarketTime': 1708117201,\n 'regularMarketDayHigh': 408.27,\n 'regularMarketDayRange': '403.53 - 408.27',\n 'regularMarketDayLow': 403.53,\n 'regularMarketVolume': 22296495,\n 'regularMarketPreviousClose': 406.56,\n 'bid': 403.2,\n 'ask': 403.5,\n 'bidSize': 10,\n 'askSize': 9,\n 'fullExchangeName': 'NasdaqGS',\n 'financialCurrency': 'USD',\n 'regularMarketOpen': 407.96,\n 'averageDailyVolume3Month': 25389093,\n 'averageDailyVolume10Day': 22286910,\n 'fiftyTwoWeekLowChange': 158.45,\n 'fiftyTwoWeekLowChangePercent': 0.6451284,\n 'fiftyTwoWeekRange': '245.61 - 420.82',\n 'fiftyTwoWeekHighChange': -16.76001,\n 'fiftyTwoWeekHighChangePercent': -0.039827026,\n 'fiftyTwoWeekLow': 245.61,\n 'fiftyTwoWeekHigh': 420.82,\n 'fiftyTwoWeekChangePercent': 59.916092,\n 'dividendDate': 1710374400,\n 'earningsTimestamp': 1706653800,\n 'earningsTimestampStart': 1713869940,\n 'earningsTimestampEnd': 1714392000,\n 'trailingAnnualDividendRate': 2.86,\n 'trailingPE': 36.500454,\n 'dividendRate': 3.0,\n 'trailingAnnualDividendYield': 0.007034632,\n 'dividendYield': 0.74,\n 'epsTrailingTwelveMonths': 11.07,\n 'epsForward': 12.38,\n 'epsCurrentYear': 10.81,\n 'priceEpsCurrentYear': 37.378353,\n 'sharesOutstanding': 7430439936,\n 'bookValue': 32.06,\n 'fiftyDayAverage': 388.6076,\n 'fiftyDayAverageChange': 15.452393,\n 'fiftyDayAverageChangePercent': 0.039763484,\n 'twoHundredDayAverage': 348.3632,\n 'twoHundredDayAverageChange': 55.696808,\n 'twoHundredDayAverageChangePercent': 0.15988144,\n 'marketCap': 3002343620608,\n 'forwardPE': 32.638126,\n 'priceToBook': 12.603243,\n 'sourceInterval': 15,\n 'exchangeDataDelayedBy': 0,\n 'averageAnalystRating': '1.7 - Buy',\n 'tradeable': False,\n 'cryptoTradeable': False,\n 'displayName': 'Microsoft',\n 'symbol': 'MSFT'}" + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from optionsmonkey.api import get_options_chain\n", + "from optionsmonkey.utils import get_fridays_date\n", + "from optionsmonkey.strategies import generate_strategies\n", + "\n", + "\n", + "friday_in_3_weeks = get_fridays_date(weeks_until=3)\n", + "options = get_options_chain('MSFT', friday_in_3_weeks)\n", + "options.underlying" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-02-18T21:51:38.193175Z", + "start_time": "2024-02-18T21:51:37.970843Z" + } + }, + "id": "83463b241c45c82b", + "execution_count": 7 + }, + { + "cell_type": "code", + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'dict' object has no attribute 'regular_market_price'", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mAttributeError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[6], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m strike \u001B[38;5;241m=\u001B[39m \u001B[43moptions\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43munderlying\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mregular_market_price\u001B[49m \u001B[38;5;241m+\u001B[39m \u001B[38;5;241m10\u001B[39m\n\u001B[1;32m 2\u001B[0m strike\n", + "\u001B[0;31mAttributeError\u001B[0m: 'dict' object has no attribute 'regular_market_price'" + ] + } + ], + "source": [ + "strike = options.underlying.regular_market_price + 10\n", + "strike" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-02-18T21:51:16.365671Z", + "start_time": "2024-02-18T21:51:16.358319Z" + } + }, + "id": "9ff9d78568685850", + "execution_count": 6 + }, + { + "cell_type": "markdown", + "source": [ + "# covered call" + ], + "metadata": { + "collapsed": false + }, + "id": "7af69ff8acb39e1e" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "\n", + "\n", + "strategy = generate_strategies('covered-call', )" + ], + "metadata": { + "collapsed": true + }, + "id": "initial_id", + "execution_count": 0 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/call_spread.ipynb b/examples/call_spread.ipynb index 70d55e9..71ceada 100644 --- a/examples/call_spread.ipynb +++ b/examples/call_spread.ipynb @@ -221,7 +221,7 @@ } ], "source": [ - "s,pl_total=st.getPL()\n", + "s,pl_total=st.get_pl()\n", "zeroline=zeros(s.shape[0])\n", "plt.xlabel(\"Stock price\")\n", "plt.ylabel(\"Profit/Loss\")\n", @@ -381,7 +381,7 @@ } ], "source": [ - "s,pl_total=st.getPL()\n", + "s,pl_total=st.get_pl()\n", "zeroline=zeros(s.shape[0])\n", "plt.xlabel(\"Stock price\")\n", "plt.ylabel(\"Profit/Loss\")\n", diff --git a/examples/covered_call.ipynb b/examples/covered_call.ipynb index 23496c6..433f026 100644 --- a/examples/covered_call.ipynb +++ b/examples/covered_call.ipynb @@ -113,7 +113,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Profit/loss profiles for each leg and the overall strategy are obtained by calling the *getPL()* method and plotted at the option's maturity." + "Profit/loss profiles for each leg and the overall strategy are obtained by calling the *get_pl()* method and plotted at the option's maturity." ] }, { @@ -145,11 +145,11 @@ } ], "source": [ - "s,pl_total=st.getPL()\n", + "s,pl_total=st.get_pl()\n", "leg=[]\n", "\n", "for i in range(len(strategy)):\n", - " leg.append(st.getPL(i)[1])\n", + " leg.append(st.get_pl(i)[1])\n", " \n", "zeroline=zeros(s.shape[0])\n", "plt.xlabel(\"Stock price\")\n", diff --git a/examples/strategies.ipynb b/examples/strategies.ipynb new file mode 100644 index 0000000..f8e52ea --- /dev/null +++ b/examples/strategies.ipynb @@ -0,0 +1,159 @@ +{ + "cells": [ + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": " contractSymbol lastTradeDate strike lastPrice bid \\\n0 MSFT240315C00140000 2024-02-15 16:07:16+00:00 140.0 265.30 263.55 \n1 MSFT240315C00145000 2023-09-22 15:53:18+00:00 145.0 177.30 182.75 \n2 MSFT240315C00150000 2023-11-28 19:40:41+00:00 150.0 233.30 224.35 \n3 MSFT240315C00160000 2023-11-22 19:26:23+00:00 160.0 220.78 214.05 \n4 MSFT240315C00170000 2024-02-01 15:01:04+00:00 170.0 237.05 233.65 \n.. ... ... ... ... ... \n73 MSFT240315C00520000 2024-02-16 20:34:41+00:00 520.0 0.03 0.02 \n74 MSFT240315C00525000 2024-02-13 20:25:57+00:00 525.0 0.03 0.02 \n75 MSFT240315C00530000 2024-02-15 18:29:57+00:00 530.0 0.02 0.01 \n76 MSFT240315C00535000 2024-02-15 20:41:18+00:00 535.0 0.01 0.02 \n77 MSFT240315C00540000 2024-02-16 20:08:02+00:00 540.0 0.01 0.01 \n\n ask change percentChange volume openInterest impliedVolatility \\\n0 265.55 0.00 0.0 5.0 15 1.744142 \n1 186.00 0.00 0.0 10.0 7 0.000010 \n2 228.80 0.00 0.0 3.0 6 0.000010 \n3 217.20 0.00 0.0 1.0 11 0.000010 \n4 235.70 0.00 0.0 1.0 2 1.491213 \n.. ... ... ... ... ... ... \n73 0.04 -0.01 -25.0 9.0 546 0.339850 \n74 0.04 0.00 0.0 2.0 762 0.350592 \n75 0.04 0.00 0.0 100.0 999 0.361335 \n76 0.03 0.00 0.0 2.0 582 0.363288 \n77 0.03 -0.01 -50.0 10.0 3908 0.373053 \n\n inTheMoney contractSize currency \n0 True REGULAR USD \n1 True REGULAR USD \n2 True REGULAR USD \n3 True REGULAR USD \n4 True REGULAR USD \n.. ... ... ... \n73 False REGULAR USD \n74 False REGULAR USD \n75 False REGULAR USD \n76 False REGULAR USD \n77 False REGULAR USD \n\n[78 rows x 14 columns]", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
contractSymbollastTradeDatestrikelastPricebidaskchangepercentChangevolumeopenInterestimpliedVolatilityinTheMoneycontractSizecurrency
0MSFT240315C001400002024-02-15 16:07:16+00:00140.0265.30263.55265.550.000.05.0151.744142TrueREGULARUSD
1MSFT240315C001450002023-09-22 15:53:18+00:00145.0177.30182.75186.000.000.010.070.000010TrueREGULARUSD
2MSFT240315C001500002023-11-28 19:40:41+00:00150.0233.30224.35228.800.000.03.060.000010TrueREGULARUSD
3MSFT240315C001600002023-11-22 19:26:23+00:00160.0220.78214.05217.200.000.01.0110.000010TrueREGULARUSD
4MSFT240315C001700002024-02-01 15:01:04+00:00170.0237.05233.65235.700.000.01.021.491213TrueREGULARUSD
.............................................
73MSFT240315C005200002024-02-16 20:34:41+00:00520.00.030.020.04-0.01-25.09.05460.339850FalseREGULARUSD
74MSFT240315C005250002024-02-13 20:25:57+00:00525.00.030.020.040.000.02.07620.350592FalseREGULARUSD
75MSFT240315C005300002024-02-15 18:29:57+00:00530.00.020.010.040.000.0100.09990.361335FalseREGULARUSD
76MSFT240315C005350002024-02-15 20:41:18+00:00535.00.010.020.030.000.02.05820.363288FalseREGULARUSD
77MSFT240315C005400002024-02-16 20:08:02+00:00540.00.010.010.03-0.01-50.010.039080.373053FalseREGULARUSD
\n

78 rows × 14 columns

\n
" + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from optionsmonkey.api import get_options_chain\n", + "from optionsmonkey.utils import get_fridays_date\n", + "\n", + "\n", + "friday_in_3_weeks = get_fridays_date(weeks_until=3)\n", + "options = get_options_chain('MSFT', friday_in_3_weeks)\n", + "options.calls" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-02-18T22:45:14.530344Z", + "start_time": "2024-02-18T22:45:13.636569Z" + } + }, + "id": "83463b241c45c82b", + "execution_count": 1 + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": " contractSymbol lastTradeDate strike lastPrice bid \\\n53 MSFT240315C00420000 2024-02-16 20:58:23+00:00 420.0 3.65 3.5 \n\n ask change percentChange volume openInterest impliedVolatility \\\n53 3.65 -0.63 -14.719627 4882.0 14688 0.214424 \n\n inTheMoney contractSize currency \n53 False REGULAR USD ", + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
contractSymbollastTradeDatestrikelastPricebidaskchangepercentChangevolumeopenInterestimpliedVolatilityinTheMoneycontractSizecurrency
53MSFT240315C004200002024-02-16 20:58:23+00:00420.03.653.53.65-0.63-14.7196274882.0146880.214424FalseREGULARUSD
\n
" + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from optionsmonkey.utils import coerce_to_multiple\n", + "\n", + "strike = coerce_to_multiple(options.underlying.regular_market_price + 15)\n", + "options_at_strike = options.calls[options.calls['strike'] == strike]\n", + "options_at_strike" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-02-18T22:45:16.093407Z", + "start_time": "2024-02-18T22:45:16.084652Z" + } + }, + "id": "9ff9d78568685850", + "execution_count": 2 + }, + { + "cell_type": "markdown", + "source": [ + "# covered call" + ], + "metadata": { + "collapsed": false + }, + "id": "7af69ff8acb39e1e" + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/mnhmbp/PycharmProjects/optionsmonkey/venv/lib/python3.10/site-packages/pydantic/main.py:171: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead\n", + " self.__pydantic_validator__.validate_python(data, self_instance=self)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "P/L profile diagram:\n", + "--------------------\n", + "The vertical green dashed line corresponds to the position of the stock's spot price. The right and left arrow markers indicate the strike prices of calls and puts, respectively, with blue representing long and red representing short positions.\n" + ] + }, + { + "data": { + "text/plain": "
", + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from optionsmonkey.plot import plot_pl\n", + "from optionsmonkey.strategies import generate_strategies\n", + "from optionsmonkey.models import Inputs\n", + "from optionsmonkey.engine import StrategyEngine\n", + "\n", + "strategy = generate_strategies('covered-call', strike, options_at_strike.bid, friday_in_3_weeks)\n", + "\n", + "inputs = Inputs(\n", + " stock_price=options.underlying.regular_market_price,\n", + " volatility=options_at_strike.impliedVolatility,\n", + " interest_rate=0.0415,\n", + " min_stock=options.underlying.fifty_two_week_low,\n", + " max_stock=options.underlying.fifty_two_week_high + 50,\n", + " target_date=friday_in_3_weeks,\n", + " strategy=strategy, \n", + ")\n", + "\n", + "st = StrategyEngine(inputs)\n", + "outputs = st.run()\n", + "\n", + "plot_pl(st)\n" + ], + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2024-02-18T22:45:51.166865Z", + "start_time": "2024-02-18T22:45:49.986341Z" + } + }, + "id": "initial_id", + "execution_count": 3 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/optionsmonkey/api.py b/optionsmonkey/api.py index c0c1366..c806b82 100644 --- a/optionsmonkey/api.py +++ b/optionsmonkey/api.py @@ -1,16 +1,9 @@ import datetime as dt -from dataclasses import dataclass -from typing import Any import pandas as pd from yfinance import Ticker - -@dataclass -class OptionsChain: - calls: pd.DataFrame - puts: pd.DataFrame - underlying: dict[str, Any] # TODO: pydantic model +from optionsmonkey.models import OptionsChain def get_options_chain(ticker: str, expiration_date: dt.date) -> OptionsChain: diff --git a/optionsmonkey/engine.py b/optionsmonkey/engine.py index 81008e0..38f454b 100644 --- a/optionsmonkey/engine.py +++ b/optionsmonkey/engine.py @@ -1,13 +1,9 @@ from __future__ import print_function, division -from datetime import date, datetime - -import matplotlib.pyplot as plt -from matplotlib import rcParams -from numpy import array, ndarray, zeros, full, stack, savetxt +from numpy import array, zeros from optionsmonkey.black_scholes import get_bs_info, get_implied_vol -from optionsmonkey.models import Inputs, Strategy, Outputs +from optionsmonkey.models import Inputs, Strategy, Outputs, OptionStrategy from optionsmonkey.support import ( getPLprofile, getPLprofilestock, @@ -21,60 +17,7 @@ class StrategyEngine: - def __init__(self): - """ - __init__ -> initializes class variables. - - Returns - ------- - None. - """ - self.__s = array([]) - self.__s_mc = array([]) - self.__strike = [] - self.__premium = [] - self.__n = [] - self.__action = [] - self.__type = [] - self.__expiration = [] - self.__prevpos = [] - self.__usebs = [] - self.__profitranges = [] - self.__profittargrange = [] - self.__losslimitranges = [] - self.__days2maturity = [] - self.__stockprice = None - self.__volatility = None - self.__startdate = date.today() - self.__targetdate = self.__startdate - self.__r = None - self.__y = 0.0 - self.__profittarg = None - self.__losslimit = None - self.__optcommission = 0.0 - self.__stockcommission = 0.0 - self.__minstock = None - self.__maxstock = None - self.__distribution = "black-scholes" - self.__country = "US" - self.__days2target = 30 - self.__nmcprices = 100000 - self.__compute_expectation = False - self.__use_dates = True - self.__discard_nonbusinessdays = True - self.__daysinyear = 252 - self.impvol = [] - self.itmprob = [] - self.delta = [] - self.gamma = [] - self.vega = [] - self.theta = [] - self.cost = [] - self.profitprob = 0.0 - self.profittargprob = 0.0 - self.losslimitprob = 0.0 - - def getdata(self, inputs: Inputs): + def __init__(self, inputs: Inputs): """ getdata -> provides input data to performs calculations for a strategy. @@ -89,141 +32,145 @@ def getdata(self, inputs: Inputs): if len(inputs.strategy) == 0: raise ValueError("No strategy provided!") - self.__type = [] - self.__strike = [] - self.__premium = [] - self.__n = [] - self.__action = [] - self.__prevpos = [] - self.__expiration = [] - self.__days2maturity = [] - self.__usebs = [] + self.s = array([]) + self.s_mc = array([]) + self.strike = [] + self.premium = [] + self.n = [] + self.action: list[str] = [] + self.type = [] + self.expiration = [] + self.prev_pos = [] + self.use_bs = [] + self.profit_ranges: list[float] = [] + self.profit_target_range: list[float] = [] + self.loss_limit_ranges: list[float] = [] + self.days_to_maturity: list[float] = [] + self.stock_price = None + self.volatility = None + self.start_date = inputs.start_date + self.r = None + self.y = 0.0 + self.profit_target = None + self.loss_limit = None + self.opt_commission = 0.0 + self.stock_commission = 0.0 + self.min_stock = None + self.max_stock = None + self.distribution = "black-scholes" + self.country = "US" + self.days_to_target = 30 + self.nmc_prices = 100e3 + self.compute_expectation = False + self.discard_nonbusinessdays = True + self.days_in_year = 252 + self.impvol: list[float] = [] + self.itmprob: list[float] = [] + self.delta: list[float] = [] + self.gamma: list[float] = [] + self.vega: list[float] = [] + self.theta: list[float] = [] + self.cost: list[float] = [] + self.profitprob = 0.0 + self.profittargprob = 0.0 + self.losslimitprob = 0.0 - self.__discard_nonbusinessdays = inputs.discard_nonbusiness_days + self.discard_nonbusinessdays = inputs.discard_nonbusiness_days - if self.__discard_nonbusinessdays: - self.__daysinyear = 252 + if self.discard_nonbusinessdays: + self.days_in_year = 252 else: - self.__daysinyear = 365 + self.days_in_year = 365 - self.__country = inputs.country + self.country = inputs.country - if inputs.use_dates and inputs.start_date and inputs.target_date: - if inputs.target_date > inputs.start_date: - self.__startdate = inputs.start_date - self.__targetdate = inputs.target_date + if inputs.target_date > inputs.start_date: + self.start_date = inputs.start_date + self.target_date = inputs.target_date - if self.__discard_nonbusinessdays: - ndiscardeddays = getnonbusinessdays( - self.__startdate, self.__targetdate, self.__country - ) - else: - ndiscardeddays = 0 - - self.__days2target = ( - self.__targetdate - self.__startdate - ).days - ndiscardeddays + if self.discard_nonbusinessdays: + ndiscardeddays = getnonbusinessdays( + self.start_date, self.target_date, self.country + ) else: - raise ValueError("Start date cannot be after the target date!") + ndiscardeddays = 0 + + self.days_to_target = ( + self.target_date - self.start_date + ).days - ndiscardeddays else: - self.__days2target = inputs.days_to_target_date + raise ValueError( + "Start date cannot be after the target date!" + ) # TODO: move validation to pydantic for i, strat in enumerate(inputs.strategy): strategy: Strategy = strat - self.__type.append(strategy.type) - - if strategy.type in ("call", "put"): - self.__strike.append(strategy.strike) # type: ignore - self.__premium.append(strategy.premium) # type: ignore - self.__n.append(strategy.n) # type: ignore - self.__action.append(strategy.action) # type: ignore - self.__prevpos.append(strategy.prevpos or 0.0) - - if strategy.expiration: # type: ignore - if inputs.use_dates: - expirationtmp = datetime.strptime( - strategy.expiration, "%Y-%m-%d" # type: ignore - ).date() - else: - days2maturitytmp = strategy.expiration # type: ignore - else: - if inputs.use_dates: - expirationtmp = self.__targetdate - else: - days2maturitytmp = self.__days2target + self.type.append(strategy.type) - if inputs.use_dates: - if expirationtmp >= self.__targetdate: # FIXME - self.__expiration.append(expirationtmp) + if type(strategy) is OptionStrategy: + self.strike.append(strategy.strike) # type: ignore + self.premium.append(strategy.premium) # type: ignore + self.n.append(strategy.n) # type: ignore + self.action.append(strategy.action) # type: ignore + self.prev_pos.append(strategy.prev_pos or 0.0) - if self.__discard_nonbusinessdays: - ndiscardeddays = getnonbusinessdays( - self.__startdate, expirationtmp, self.__country - ) - else: - ndiscardeddays = 0 + if strategy.expiration >= self.target_date: + self.expiration.append(strategy.expiration) - self.__days2maturity.append( - (expirationtmp - self.__startdate).days - ndiscardeddays + if self.discard_nonbusinessdays: + ndiscardeddays = getnonbusinessdays( + self.start_date, strategy.expiration, self.country ) - - if expirationtmp == self.__targetdate: - self.__usebs.append(False) - else: - self.__usebs.append(True) else: - raise ValueError( - "Expiration date must be after or equal to the target date!" - ) + ndiscardeddays = 0 + + self.days_to_maturity.append( + (strategy.expiration - self.start_date).days - ndiscardeddays + ) + + self.use_bs.append(strategy.expiration != self.target_date) else: - if days2maturitytmp >= self.__days2target: # FIXME - self.__days2maturity.append(days2maturitytmp) + raise ValueError( + "Expiration date must be after or equal to the target date!" + ) - if days2maturitytmp == self.__days2target: - self.__usebs.append(False) - else: - self.__usebs.append(True) - else: - raise ValueError( - "Days remaining to maturity must be greater than or equal to the number of days remaining to the target date!" - ) elif strategy.type == "stock": - self.__n.append(strategy.n) - self.__action.append(strategy.action) - self.__prevpos.append(strategy.prevpos or 0.0) + self.n.append(strategy.n) + self.action.append(strategy.action) + self.prev_pos.append(strategy.prev_pos or 0.0) - self.__strike.append(0.0) - self.__premium.append(0.0) - self.__usebs.append(False) - self.__days2maturity.append(-1) - self.__expiration.append(self.__targetdate if inputs.use_dates else -1) + self.strike.append(0.0) + self.premium.append(0.0) + self.use_bs.append(False) + self.days_to_maturity.append(-1) + self.expiration.append(self.target_date) elif strategy.type == "closed": - self.__prevpos.append(strategy.prevpos) - self.__strike.append(0.0) - self.__n.append(0) - self.__premium.append(0.0) - self.__action.append("n/a") - self.__usebs.append(False) - self.__days2maturity.append(-1) - self.__expiration.append(self.__targetdate if inputs.use_dates else -1) + self.prev_pos.append(strategy.prev_pos) + self.strike.append(0.0) + self.n.append(0) + self.premium.append(0.0) + self.action.append("n/a") + self.use_bs.append(False) + self.days_to_maturity.append(-1) + self.expiration.append(self.target_date) else: raise ValueError("Type must be 'call', 'put', 'stock' or 'closed'!") - self.__distribution = inputs.distribution - self.__stockprice = inputs.stock_price - self.__volatility = inputs.volatility - self.__r = inputs.interest_rate - self.__y = inputs.dividend_yield - self.__minstock = inputs.min_stock - self.__maxstock = inputs.max_stock - self.__profittarg = inputs.profit_target - self.__losslimit = inputs.loss_limit - self.__optcommission = inputs.opt_commission - self.__stockcommission = inputs.stock_commission - self.__nmcprices = inputs.nmc_prices - self.__compute_expectation = inputs.compute_expectation - self.__use_dates = inputs.use_dates + self.distribution = inputs.distribution + self.stock_price = inputs.stock_price + self.volatility = inputs.volatility + self.r = inputs.interest_rate + self.y = inputs.dividend_yield + self.min_stock = inputs.min_stock + self.max_stock = inputs.max_stock + self.profit_target = inputs.profit_target + self.loss_limit = inputs.loss_limit + self.opt_commission = inputs.opt_commission + self.stock_commission = inputs.stock_commission + self.nmc_prices = inputs.nmc_prices + self.compute_expectation = inputs.compute_expectation + self.use_dates = inputs.use_dates def run(self): """ @@ -233,17 +180,17 @@ def run(self): ------- outputs : Outputs """ - if len(self.__type) == 0: + if len(self.type) == 0: raise RuntimeError("No legs in the strategy! Nothing to do!") - elif self.__type.count("closed") > 1: + elif self.type.count("closed") > 1: raise RuntimeError("Only one position of type 'closed' is allowed!") - elif self.__distribution == "array" and self.__s_mc.shape[0] == 0: + elif self.distribution == "array" and self.s_mc.shape[0] == 0: raise RuntimeError( "No terminal stock prices from Monte Carlo simulations! Nothing to do!" ) - time2target = self.__days2target / self.__daysinyear - self.cost = [0.0 for _ in range(len(self.__type))] + time2target = self.days_to_target / self.days_in_year + self.cost = [0.0 for _ in range(len(self.type))] self.impvol = [] self.itmprob = [] self.delta = [] @@ -251,38 +198,38 @@ def run(self): self.vega = [] self.theta = [] - if self.__s.shape[0] == 0: - self.__s = createpriceseq(self.__minstock, self.__maxstock) + if self.s.shape[0] == 0: + self.s = createpriceseq(self.min_stock, self.max_stock) - self.profit = zeros((len(self.__type), self.__s.shape[0])) - self.strategyprofit = zeros(self.__s.shape[0]) + self.profit = zeros((len(self.type), self.s.shape[0])) + self.strategyprofit = zeros(self.s.shape[0]) - if self.__compute_expectation and self.__s_mc.shape[0] == 0: - self.__s_mc = createpricesamples( - self.__stockprice, - self.__volatility, + if self.compute_expectation and self.s_mc.shape[0] == 0: + self.s_mc = createpricesamples( + self.stock_price, + self.volatility, time2target, - self.__r, - self.__distribution, - self.__y, - self.__nmcprices, + self.r, + self.distribution, + self.y, + self.nmc_prices, ) - if self.__s_mc.shape[0] > 0: - self.profit_mc = zeros((len(self.__type), self.__s_mc.shape[0])) - self.strategyprofit_mc = zeros(self.__s_mc.shape[0]) + if self.s_mc.shape[0] > 0: + self.profit_mc = zeros((len(self.type), self.s_mc.shape[0])) + self.strategyprofit_mc = zeros(self.s_mc.shape[0]) - for i, type in enumerate(self.__type): + for i, type in enumerate(self.type): if type in ("call", "put"): - if self.__prevpos[i] >= 0.0: - time2maturity = self.__days2maturity[i] / self.__daysinyear + if self.prev_pos[i] >= 0.0: + time2maturity = self.days_to_maturity[i] / self.days_in_year bs = get_bs_info( - self.__stockprice, - self.__strike[i], - self.__r, - self.__volatility, + self.stock_price, + self.strike[i], + self.r, + self.volatility, time2maturity, - self.__y, + self.y, ) self.gamma.append(bs.gamma) @@ -292,42 +239,42 @@ def run(self): self.impvol.append( get_implied_vol( "call", - self.__premium[i], - self.__stockprice, - self.__strike[i], - self.__r, + self.premium[i], + self.stock_price, + self.strike[i], + self.r, time2maturity, - self.__y, + self.y, ) ) self.itmprob.append(bs.call_itm_prob) - if self.__action[i] == "buy": + if self.action[i] == "buy": self.delta.append(bs.call_delta) - self.theta.append(bs.call_theta / self.__daysinyear) + self.theta.append(bs.call_theta / self.days_in_year) else: self.delta.append(-bs.call_delta) - self.theta.append(-bs.call_theta / self.__daysinyear) + self.theta.append(-bs.call_theta / self.days_in_year) else: self.impvol.append( get_implied_vol( "put", - self.__premium[i], - self.__stockprice, - self.__strike[i], - self.__r, + self.premium[i], + self.stock_price, + self.strike[i], + self.r, time2maturity, - self.__y, + self.y, ) ) self.itmprob.append(bs.putitmprob) - if self.__action[i] == "buy": + if self.action[i] == "buy": self.delta.append(bs.put_delta) - self.theta.append(bs.put_theta / self.__daysinyear) + self.theta.append(bs.put_theta / self.days_in_year) else: self.delta.append(-bs.put_delta) - self.theta.append(-bs.put_theta / self.__daysinyear) + self.theta.append(-bs.put_theta / self.days_in_year) else: self.impvol.append(0.0) self.itmprob.append(0.0) @@ -336,74 +283,74 @@ def run(self): self.vega.append(0.0) self.theta.append(0.0) - if self.__prevpos[i] < 0.0: # Previous position is closed - costtmp = (self.__premium[i] + self.__prevpos[i]) * self.__n[i] + if self.prev_pos[i] < 0.0: # Previous position is closed + costtmp = (self.premium[i] + self.prev_pos[i]) * self.n[i] - if self.__action[i] == "buy": + if self.action[i] == "buy": costtmp *= -1.0 self.cost[i] = costtmp self.profit[i] += costtmp - if self.__compute_expectation or self.__distribution == "array": + if self.compute_expectation or self.distribution == "array": self.profit_mc[i] += costtmp else: - if self.__prevpos[i] > 0.0: # Premium of the open position - opval = self.__prevpos[i] + if self.prev_pos[i] > 0.0: # Premium of the open position + opval = self.prev_pos[i] else: # Current premium - opval = self.__premium[i] + opval = self.premium[i] - if self.__usebs[i]: + if self.use_bs[i]: self.profit[i], self.cost[i] = getPLprofileBS( type, - self.__action[i], - self.__strike[i], + self.action[i], + self.strike[i], opval, - self.__r, - (self.__days2maturity[i] - self.__days2target) - / self.__daysinyear, - self.__volatility, - self.__n[i], - self.__s, - self.__y, - self.__optcommission, + self.r, + (self.days_to_maturity[i] - self.days_to_target) + / self.days_in_year, + self.volatility, + self.n[i], + self.s, + self.y, + self.opt_commission, ) - if self.__compute_expectation or self.__distribution == "array": + if self.compute_expectation or self.distribution == "array": self.profit_mc[i] = getPLprofileBS( type, - self.__action[i], - self.__strike[i], + self.action[i], + self.strike[i], opval, - self.__r, - (self.__days2maturity[i] - self.__days2target) - / self.__daysinyear, - self.__volatility, - self.__n[i], - self.__s_mc, - self.__y, - self.__optcommission, + self.r, + (self.days_to_maturity[i] - self.days_to_target) + / self.days_in_year, + self.volatility, + self.n[i], + self.s_mc, + self.y, + self.opt_commission, )[0] else: self.profit[i], self.cost[i] = getPLprofile( type, - self.__action[i], - self.__strike[i], + self.action[i], + self.strike[i], opval, - self.__n[i], - self.__s, - self.__optcommission, + self.n[i], + self.s, + self.opt_commission, ) - if self.__compute_expectation or self.__distribution == "array": + if self.compute_expectation or self.distribution == "array": self.profit_mc[i] = getPLprofile( type, - self.__action[i], - self.__strike[i], + self.action[i], + self.strike[i], opval, - self.__n[i], - self.__s_mc, - self.__optcommission, + self.n[i], + self.s_mc, + self.opt_commission, )[0] elif type == "stock": self.impvol.append(0.0) @@ -413,38 +360,38 @@ def run(self): self.vega.append(0.0) self.theta.append(0.0) - if self.__prevpos[i] < 0.0: # Previous position is closed - costtmp = (self.__stockprice + self.__prevpos[i]) * self.__n[i] + if self.prev_pos[i] < 0.0: # Previous position is closed + costtmp = (self.stock_price + self.prev_pos[i]) * self.n[i] - if self.__action[i] == "buy": + if self.action[i] == "buy": costtmp *= -1.0 self.cost[i] = costtmp self.profit[i] += costtmp - if self.__compute_expectation or self.__distribution == "array": + if self.compute_expectation or self.distribution == "array": self.profit_mc[i] += costtmp else: - if self.__prevpos[i] > 0.0: # Stock price at previous position - stockpos = self.__prevpos[i] + if self.prev_pos[i] > 0.0: # Stock price at previous position + stockpos = self.prev_pos[i] else: # Spot price of the stock at start date - stockpos = self.__stockprice + stockpos = self.stock_price self.profit[i], self.cost[i] = getPLprofilestock( stockpos, - self.__action[i], - self.__n[i], - self.__s, - self.__stockcommission, + self.action[i], + self.n[i], + self.s, + self.stock_commission, ) - if self.__compute_expectation or self.__distribution == "array": + if self.compute_expectation or self.distribution == "array": self.profit_mc[i] = getPLprofilestock( stockpos, - self.__action[i], - self.__n[i], - self.__s_mc, - self.__stockcommission, + self.action[i], + self.n[i], + self.s_mc, + self.stock_commission, )[0] elif type == "closed": self.impvol.append(0.0) @@ -454,89 +401,89 @@ def run(self): self.vega.append(0.0) self.theta.append(0.0) - self.cost[i] = self.__prevpos[i] - self.profit[i] += self.__prevpos[i] + self.cost[i] = self.prev_pos[i] + self.profit[i] += self.prev_pos[i] - if self.__compute_expectation or self.__distribution == "array": - self.profit_mc[i] += self.__prevpos[i] + if self.compute_expectation or self.distribution == "array": + self.profit_mc[i] += self.prev_pos[i] self.strategyprofit += self.profit[i] - if self.__compute_expectation or self.__distribution == "array": + if self.compute_expectation or self.distribution == "array": self.strategyprofit_mc += self.profit_mc[i] - self.__profitranges = getprofitrange(self.__s, self.strategyprofit) + self.profit_ranges = getprofitrange(self.s, self.strategyprofit) - if self.__profitranges: - if self.__distribution in ("normal", "laplace", "black-scholes"): + if self.profit_ranges: + if self.distribution in ("normal", "laplace", "black-scholes"): self.profitprob = getPoP( - self.__profitranges, - self.__distribution, - stockprice=self.__stockprice, - volatility=self.__volatility, + self.profit_ranges, + self.distribution, + stockprice=self.stock_price, + volatility=self.volatility, time2maturity=time2target, - interestrate=self.__r, - dividendyield=self.__y, + interestrate=self.r, + dividendyield=self.y, ) - elif self.__distribution == "array": + elif self.distribution == "array": self.profitprob = getPoP( - self.__profitranges, self.__distribution, array=self.__s_mc + self.profit_ranges, self.distribution, array=self.s_mc ) - if self.__profittarg is not None: - self.__profittargrange = getprofitrange( - self.__s, self.strategyprofit, self.__profittarg + if self.profit_target is not None: + self.profit_target_range = getprofitrange( + self.s, self.strategyprofit, self.profit_target ) - if self.__profittargrange: - if self.__distribution in ("normal", "laplace", "black-scholes"): + if self.profit_target_range: + if self.distribution in ("normal", "laplace", "black-scholes"): self.profittargprob = getPoP( - self.__profittargrange, - self.__distribution, - stockprice=self.__stockprice, - volatility=self.__volatility, + self.profit_target_range, + self.distribution, + stockprice=self.stock_price, + volatility=self.volatility, time2maturity=time2target, - interestrate=self.__r, - dividendyield=self.__y, + interestrate=self.r, + dividendyield=self.y, ) - elif self.__distribution == "array": + elif self.distribution == "array": self.profittargprob = getPoP( - self.__profittargrange, self.__distribution, array=self.__s_mc + self.profit_target_range, self.distribution, array=self.s_mc ) - if self.__losslimit is not None: - self.__losslimitranges = getprofitrange( - self.__s, self.strategyprofit, self.__losslimit + 0.01 + if self.loss_limit is not None: + self.loss_limit_ranges = getprofitrange( + self.s, self.strategyprofit, self.loss_limit + 0.01 ) - if self.__losslimitranges: - if self.__distribution in ("normal", "laplace", "black-scholes"): + if self.loss_limit_ranges: + if self.distribution in ("normal", "laplace", "black-scholes"): self.losslimitprob = 1.0 - getPoP( - self.__losslimitranges, - self.__distribution, - stockprice=self.__stockprice, - volatility=self.__volatility, + self.loss_limit_ranges, + self.distribution, + stockprice=self.stock_price, + volatility=self.volatility, time2maturity=time2target, - interestrate=self.__r, - dividendyield=self.__y, + interestrate=self.r, + dividendyield=self.y, ) - elif self.__distribution == "array": + elif self.distribution == "array": self.losslimitprob = 1.0 - getPoP( - self.__losslimitranges, self.__distribution, array=self.__s_mc + self.loss_limit_ranges, self.distribution, array=self.s_mc ) opt_outputs = {} - if self.__profittarg is not None: + if self.profit_target is not None: opt_outputs["probability_of_profit_target"] = self.profittargprob - opt_outputs["project_target_ranges"] = self.__profittargrange + opt_outputs["project_target_ranges"] = self.profit_target_range - if self.__losslimit is not None: + if self.loss_limit is not None: opt_outputs["probability_of_loss_limit"] = self.losslimitprob if ( - self.__compute_expectation or self.__distribution == "array" - ) and self.__s_mc.shape[0] > 0: + self.compute_expectation or self.distribution == "array" + ) and self.s_mc.shape[0] > 0: tmpprof = self.strategyprofit_mc[self.strategyprofit_mc >= 0.01] tmploss = self.strategyprofit_mc[self.strategyprofit_mc < 0.0] opt_outputs["average_profit_from_mc"] = ( @@ -556,7 +503,7 @@ def run(self): "probability_of_profit": self.profitprob, "strategy_cost": sum(self.cost), "per_leg_cost": self.cost, - "profit_ranges": self.__profitranges, + "profit_ranges": self.profit_ranges, "minimum_return_in_the_domain": self.strategyprofit.min(), "maximum_return_in_the_domain": self.strategyprofit.max(), "implied_volatility": self.impvol, @@ -568,9 +515,9 @@ def run(self): } ) - def getPL(self, leg=-1): + def get_pl(self, leg=-1): """ - getPL -> returns the profit/loss profile of either a leg or the whole + get_pl -> returns the profit/loss profile of either a leg or the whole strategy. Parameters @@ -586,197 +533,9 @@ def getPL(self, leg=-1): Profit/loss profile of either a leg or the whole strategy. """ if self.profit.size > 0 and leg >= 0 and leg < self.profit.shape[0]: - return self.__s, self.profit[leg] - else: - return self.__s, self.strategyprofit - - def PL2csv(self, filename="pl.csv", leg=-1): - """ - PL2csv -> saves the profit/loss data to a .csv file. - - Parameters - ---------- - filename : string, optional - Name of the .csv file. Default is 'pl.csv'. - leg : int, optional - Index of the leg. Default is -1 (whole strategy). - - Returns - ------- - None. - """ - if self.profit.size > 0 and leg >= 0 and leg < self.profit.shape[0]: - arr = stack((self.__s, self.profit[leg])) - else: - arr = stack((self.__s, self.strategyprofit)) - - savetxt( - filename, arr.transpose(), delimiter=",", header="StockPrice,Profit/Loss" - ) - - def plotPL(self): - """ - plotPL -> displays the strategy's profit/loss profile diagram. - - Returns - ------- - None. - """ - if len(self.strategyprofit) == 0: - raise RuntimeError( - "Before plotting the profit/loss profile diagram, you must run a calculation!" - ) - - rcParams.update({"figure.autolayout": True}) - - zeroline = zeros(self.__s.shape[0]) - strikecallbuy = [] - strikeputbuy = [] - zerocallbuy = [] - zeroputbuy = [] - strikecallsell = [] - strikeputsell = [] - zerocallsell = [] - zeroputsell = [] - comment = "P/L profile diagram:\n--------------------\n" - comment += "The vertical green dashed line corresponds to the position " - comment += "of the stock's spot price. The right and left arrow " - comment += "markers indicate the strike prices of calls and puts, " - comment += "respectively, with blue representing long and red representing " - comment += "short positions." - - plt.axvline(self.__stockprice, ls="--", color="green") - plt.xlabel("Stock price") - plt.ylabel("Profit/Loss") - plt.xlim(self.__s.min(), self.__s.max()) - - for i in range(len(self.__strike)): - if self.__strike[i] > 0.0: - if self.__type[i] == "call": - if self.__action[i] == "buy": - strikecallbuy.append(self.__strike[i]) - zerocallbuy.append(0.0) - elif self.__action[i] == "sell": - strikecallsell.append(self.__strike[i]) - zerocallsell.append(0.0) - elif self.__type[i] == "put": - if self.__action[i] == "buy": - strikeputbuy.append(self.__strike[i]) - zeroputbuy.append(0.0) - elif self.__action[i] == "sell": - strikeputsell.append(self.__strike[i]) - zeroputsell.append(0.0) - - if self.__profittarg is not None: - comment += " The blue dashed line represents the profit target level." - targetline = full(self.__s.shape[0], self.__profittarg) - - if self.__losslimit is not None: - comment += " The red dashed line represents the loss limit level." - lossline = full(self.__s.shape[0], self.__losslimit) - - print(comment) - - if self.__losslimit is not None and self.__profittarg is not None: - plt.plot( - self.__s, - zeroline, - "m--", - self.__s, - lossline, - "r--", - self.__s, - targetline, - "b--", - self.__s, - self.strategyprofit, - "k-", - strikecallbuy, - zerocallbuy, - "b>", - strikeputbuy, - zeroputbuy, - "b<", - strikecallsell, - zerocallsell, - "r>", - strikeputsell, - zeroputsell, - "r<", - markersize=10, - ) - elif self.__losslimit is not None: - plt.plot( - self.__s, - zeroline, - "m--", - self.__s, - lossline, - "r--", - self.__s, - self.strategyprofit, - "k-", - strikecallbuy, - zerocallbuy, - "b>", - strikeputbuy, - zeroputbuy, - "b<", - strikecallsell, - zerocallsell, - "r>", - strikeputsell, - zeroputsell, - "r<", - markersize=10, - ) - elif self.__profittarg is not None: - plt.plot( - self.__s, - zeroline, - "m--", - self.__s, - targetline, - "b--", - self.__s, - self.strategyprofit, - "k-", - strikecallbuy, - zerocallbuy, - "b>", - strikeputbuy, - zeroputbuy, - "b<", - strikecallsell, - zerocallsell, - "r>", - strikeputsell, - zeroputsell, - "r<", - markersize=10, - ) + return self.s, self.profit[leg] else: - plt.plot( - self.__s, - zeroline, - "m--", - self.__s, - self.strategyprofit, - "k-", - strikecallbuy, - zerocallbuy, - "b>", - strikeputbuy, - zeroputbuy, - "b<", - strikecallsell, - zerocallsell, - "r>", - strikeputsell, - zeroputsell, - "r<", - markersize=10, - ) + return self.s, self.strategyprofit """ Properties @@ -794,32 +553,12 @@ def plotPL(self): @property def days2target(self): - return self.__days2target + return self.days_to_target @property def stockpricearray(self): - return self.__s - - @stockpricearray.setter - def stockpricearray(self, s): - if isinstance(s, ndarray): - if s.shape[0] > 0: - self.__s = s - else: - raise ValueError("Empty stock price array is not allowed!") - else: - raise TypeError("A numpy array is expected!") + return self.s @property def terminalstockprices(self): - return self.__s_mc - - @terminalstockprices.setter - def terminalstockprices(self, s): - if isinstance(s, ndarray): - if s.shape[0] > 0: - self.__s_mc = s - else: - raise ValueError("Empty terminal stock price array is not allowed!") - else: - raise TypeError("A numpy array is expected!") + return self.s_mc diff --git a/optionsmonkey/holidays.py b/optionsmonkey/holidays.py index 72892f0..9802582 100644 --- a/optionsmonkey/holidays.py +++ b/optionsmonkey/holidays.py @@ -9,6 +9,7 @@ # TODO: convert to JSON + def getholidays(country: Country): if country == "US": return __getholidaysUS__() diff --git a/optionsmonkey/models.py b/optionsmonkey/models.py index 83cb820..78ec3d3 100644 --- a/optionsmonkey/models.py +++ b/optionsmonkey/models.py @@ -1,7 +1,9 @@ import datetime as dt -from typing import Literal +from typing import Literal, Any -from pydantic import BaseModel, Field +import pandas as pd +from humps import decamelize +from pydantic import BaseModel, Field, field_validator, ConfigDict OptionType = Literal["call", "put"] Range = tuple[float, float] @@ -26,26 +28,75 @@ class BaseStrategy(BaseModel): action: Literal["buy", "sell"] - prevpos: float | None = None + prev_pos: float | None = None class StockStrategy(BaseStrategy): - type: Literal["stock"] + """ + "type" : string + It must be 'stock'. It is mandatory. + "n" : int + Number of shares. It is mandatory. + "action" : string + Either 'buy' or 'sell'. It is mandatory. + "prev_pos" : float + Stock price effectively paid or received in a previously + opened position. If positive, it means that the position + remains open and the payoff calculation takes this price + into account, not the current price of the stock. If + negative, it means that the position is closed and the + difference between this price and the current price is + considered in the payoff calculation. + """ + + type: Literal["stock"] = "stock" n: int = Field(gt=0) premium: float | None = None class OptionStrategy(BaseStrategy): + """ + "type" : string + Either 'call' or 'put'. It is mandatory. + "strike" : float + Option strike price. It is mandatory. + "premium" : float + Option premium. It is mandatory. + "n" : int + Number of options. It is mandatory + "action" : string + Either 'buy' or 'sell'. It is mandatory. + "prev_pos" : float + Premium effectively paid or received in a previously opened + position. If positive, it means that the position remains + open and the payoff calculation takes this price into + account, not the current price of the option. If negative, + it means that the position is closed and the difference + between this price and the current price is considered in + the payoff calculation. + "expiration" : string | int + Expiration date. + """ + type: OptionType strike: float = Field(gt=0) premium: float = Field(gt=0) n: int = Field(gt=0) - expiration: str | int | None = None + expiration: dt.date class ClosedPosition(BaseModel): - type: Literal["closed"] - prevpos: float + """ + "type" : string + It must be 'closed'. It is mandatory. + "prev_pos" : float + The total value of the position to be closed, which can be + positive if it made a profit or negative if it is a loss. + It is mandatory. + """ + + type: Literal["closed"] = "closed" + prev_pos: float Strategy = StockStrategy | OptionStrategy | ClosedPosition @@ -64,54 +115,7 @@ class Inputs(BaseModel): max_stock : float Maximum value of the stock in the stock price domain. strategy : list - A Python list containing the strategy legs as Python dictionaries. - For options, the dictionary should contain up to 7 keys: - "type" : string - Either 'call' or 'put'. It is mandatory. - "strike" : float - Option strike price. It is mandatory. - "premium" : float - Option premium. It is mandatory. - "n" : int - Number of options. It is mandatory - "action" : string - Either 'buy' or 'sell'. It is mandatory. - "prevpos" : float - Premium effectively paid or received in a previously opened - position. If positive, it means that the position remains - open and the payoff calculation takes this price into - account, not the current price of the option. If negative, - it means that the position is closed and the difference - between this price and the current price is considered in - the payoff calculation. - "expiration" : string | int - Expiration date in 'YYYY-MM-DD' format or number of days - left before maturity, depending on the value in 'use_dates' - (see below). - For stocks, the dictionary should contain up to 4 keys: - "type" : string - It must be 'stock'. It is mandatory. - "n" : int - Number of shares. It is mandatory. - "action" : string - Either 'buy' or 'sell'. It is mandatory. - "prevpos" : float - Stock price effectively paid or received in a previously - opened position. If positive, it means that the position - remains open and the payoff calculation takes this price - into account, not the current price of the stock. If - negative, it means that the position is closed and the - difference between this price and the current price is - considered in the payoff calculation. - For a non-determined previously opened position to be closed, which - might consist of any combination of calls, puts and stocks, the - dictionary must contain two keys: - "type" : string - It must be 'closed'. It is mandatory. - "prevpos" : float - The total value of the position to be closed, which can be - positive if it made a profit or negative if it is a loss. - It is mandatory. + A list of `Strategy` dividend_yield : float, optional Annualized dividend yield. Default is 0.0. profit_target : float, optional @@ -127,25 +131,16 @@ class Inputs(BaseModel): Whether or not the strategy's average profit and loss must be computed from a numpy array of random terminal prices generated from the chosen distribution. Default is False. - use_dates : logical, optional - Whether the target and maturity dates are provided or not. If False, - the number of days remaining to the target date and maturity are - provided. Default is True. discard_nonbusinessdays : logical, optional Whether to discard Saturdays and Sundays (and maybe holidays) when counting the number of days between two dates. Default is True. country : string, optional Country for which the holidays will be considered if 'discard_nonbusinessdyas' is True. Default is 'US'. - start_date : string, optional - Start date in the calculations, in 'YYYY-MM-DD' format. Default is "". - Mandatory if 'use_dates' is True. - target_date : string, optional - Target date in the calculations, in 'YYYY-MM-DD' format. Default is "". - Mandatory if 'use_dates' is True. - days_to_target_date : int, optional - Number of days remaining until the target date. Not considered if - 'use_dates' is True. Default is 30 days. + start_date : dt.date, optional + Start date in the calculations (today if not provided). + target_date : dt.date, optional + Start date in the calculations (today if not provided). distribution : string, optional Statistical distribution used to compute probabilities. It can be 'black-scholes', 'normal', 'laplace' or 'array'. Default is 'black-scholes'. @@ -171,7 +166,6 @@ class Inputs(BaseModel): country: Country = "US" start_date: dt.date = Field(default_factory=dt.date.today) target_date: dt.date = Field(default_factory=dt.date.today) - days_to_target_date: int = 30 distribution: Literal["black-scholes", "normal", "laplace", "array"] = ( "black-scholes" ) @@ -191,6 +185,111 @@ class BlackScholesInfo(BaseModel): put_itm_prob: float +class UnderlyingAsset(BaseModel): + symbol: str + region: str + quote_type: Literal["EQUITY"] + quote_source_name: Literal["Delayed Quote"] + triggerable: bool + currency: Literal["USD"] + market_state: Literal["CLOSED", "OPEN"] + regular_market_change_percent: float + regular_market_price: float + exchange: str + short_name: str + long_name: str + exchange_timezone_name: str + exchange_timezone_short_name: str + gmt_off_set_milliseconds: int + market: Literal["us_market"] + esg_populated: bool + first_trade_date_milliseconds: int + post_market_change_percent: float + post_market_time: int + post_market_price: float + post_market_change: float + regular_market_change: float + regular_market_time: int + regular_market_day_high: float + regular_market_day_range: tuple[float, float] + regular_market_day_low: float + regular_market_volume: float + regular_market_previous_close: float + bid: float + ask: float + bid_size: int + ask_size: int + full_exchange_name: str + financial_currency: Literal["USD"] + regular_market_open: float + average_daily_volume3_month: int + average_daily_volume10_day: int + fifty_two_week_low_change: float + fifty_two_week_low_change_percent: float + fifty_two_week_range: tuple[float, float] + fifty_two_week_high_change: float + fifty_two_week_high_change_percent: float + fifty_two_week_low: float + fifty_two_week_high: float + fifty_two_week_change_percent: float + dividend_date: int + earnings_timestamp: int + earnings_timestamp_start: int + earnings_timestamp_end: int + trailing_annual_dividend_rate: float + trailing_pe: float + dividend_rate: float + trailing_annual_dividend_yield: float + dividend_yield: float + eps_trailing_twelve_months: float + eps_forward: float + eps_current_year: float + price_eps_current_year: float + shares_outstanding: int + book_value: float + fifty_day_average: float + fifty_day_average_change: float + fifty_day_average_change_percent: float + two_hundred_day_average: float + two_hundred_day_average_change: float + two_hundred_day_average_change_percent: float + market_cap: int + forward_pe: float + price_to_book: float + source_interval: int + exchange_data_delayed_by: int + average_analyst_rating: str + tradeable: bool + crypto_tradeable: bool + display_name: str + + +class OptionsChain(BaseModel): + calls: pd.DataFrame + puts: pd.DataFrame + underlying: UnderlyingAsset + model_config = ConfigDict(arbitrary_types_allowed=True) + + @field_validator("underlying", mode="before") + @classmethod + def validate_underlying(cls, v: dict[str, Any]) -> UnderlyingAsset: + day_range_split = v["regularMarketDayRange"].split(" - ") + fifty_two_week_range_split = v["fiftyTwoWeekRange"].split(" - ") + return UnderlyingAsset.model_validate( + decamelize(v) + | { + "regular_market_day_range": ( + float(day_range_split[0]), + float(day_range_split[1]), + ), + "fifty_two_week_range": ( + float(fifty_two_week_range_split[0]), + float(fifty_two_week_range_split[1]), + ), + } + ) + + class Outputs(BaseModel): """ probability_of_profit : float diff --git a/optionsmonkey/plot.py b/optionsmonkey/plot.py new file mode 100644 index 0000000..3e6ffcb --- /dev/null +++ b/optionsmonkey/plot.py @@ -0,0 +1,170 @@ +import matplotlib.pyplot as plt +from matplotlib import rcParams +from numpy import zeros, full + +from optionsmonkey.engine import StrategyEngine + + +def plot_pl(st: StrategyEngine): + """ + plotPL -> displays the strategy's profit/loss profile diagram. + + Returns + ------- + None. + """ + if len(st.strategyprofit) == 0: + raise RuntimeError( + "Before plotting the profit/loss profile diagram, you must run a calculation!" + ) + + rcParams.update({"figure.autolayout": True}) + + zeroline = zeros(st.s.shape[0]) + strikecallbuy = [] + strikeputbuy = [] + zerocallbuy = [] + zeroputbuy = [] + strikecallsell = [] + strikeputsell = [] + zerocallsell = [] + zeroputsell = [] + comment = "P/L profile diagram:\n--------------------\n" + comment += "The vertical green dashed line corresponds to the position " + comment += "of the stock's spot price. The right and left arrow " + comment += "markers indicate the strike prices of calls and puts, " + comment += "respectively, with blue representing long and red representing " + comment += "short positions." + + plt.axvline(st.stock_price, ls="--", color="green") + plt.xlabel("Stock price") + plt.ylabel("Profit/Loss") + plt.xlim(st.s.min(), st.s.max()) + + for i in range(len(st.strike)): + if st.strike[i] > 0.0: + if st.type[i] == "call": + if st.action[i] == "buy": + strikecallbuy.append(st.strike[i]) + zerocallbuy.append(0.0) + elif st.action[i] == "sell": + strikecallsell.append(st.strike[i]) + zerocallsell.append(0.0) + elif st.type[i] == "put": + if st.action[i] == "buy": + strikeputbuy.append(st.strike[i]) + zeroputbuy.append(0.0) + elif st.action[i] == "sell": + strikeputsell.append(st.strike[i]) + zeroputsell.append(0.0) + + if st.profit_target is not None: + comment += " The blue dashed line represents the profit target level." + targetline = full(st.s.shape[0], st.profit_target) + + if st.loss_limit is not None: + comment += " The red dashed line represents the loss limit level." + lossline = full(st.s.shape[0], st.loss_limit) + + print(comment) + + if st.loss_limit is not None and st.profit_target is not None: + plt.plot( + st.s, + zeroline, + "m--", + st.s, + lossline, + "r--", + st.s, + targetline, + "b--", + st.s, + st.strategyprofit, + "k-", + strikecallbuy, + zerocallbuy, + "b>", + strikeputbuy, + zeroputbuy, + "b<", + strikecallsell, + zerocallsell, + "r>", + strikeputsell, + zeroputsell, + "r<", + markersize=10, + ) + elif st.loss_limit is not None: + plt.plot( + st.s, + zeroline, + "m--", + st.s, + lossline, + "r--", + st.s, + st.strategyprofit, + "k-", + strikecallbuy, + zerocallbuy, + "b>", + strikeputbuy, + zeroputbuy, + "b<", + strikecallsell, + zerocallsell, + "r>", + strikeputsell, + zeroputsell, + "r<", + markersize=10, + ) + elif st.profit_target is not None: + plt.plot( + st.s, + zeroline, + "m--", + st.s, + targetline, + "b--", + st.s, + st.strategyprofit, + "k-", + strikecallbuy, + zerocallbuy, + "b>", + strikeputbuy, + zeroputbuy, + "b<", + strikecallsell, + zerocallsell, + "r>", + strikeputsell, + zeroputsell, + "r<", + markersize=10, + ) + else: + plt.plot( + st.s, + zeroline, + "m--", + st.s, + st.strategyprofit, + "k-", + strikecallbuy, + zerocallbuy, + "b>", + strikeputbuy, + zeroputbuy, + "b<", + strikecallsell, + zerocallsell, + "r>", + strikeputsell, + zeroputsell, + "r<", + markersize=10, + ) diff --git a/optionsmonkey/strategies.py b/optionsmonkey/strategies.py new file mode 100644 index 0000000..033ed7b --- /dev/null +++ b/optionsmonkey/strategies.py @@ -0,0 +1,146 @@ +import datetime as dt +from typing import Literal + +from optionsmonkey.models import Strategy, StockStrategy, OptionStrategy + +NamedStrategy = Literal["covered-call", "married-put", "bull-call", "protective-collar"] + + +def generate_strategies( + named_strat: NamedStrategy, + strike: float, + premium: float, + expiration: dt.date, + n: int = 100, + prev_pos: float | None = None, + higher_strike: float | None = None, +) -> list[Strategy]: + """Generate common option strategies + Source: https://www.investopedia.com/trading/options-strategies/ + """ + + match named_strat: + case "covered-call": + return _generate_covered_call_strategy( + strike, premium, n, expiration, prev_pos + ) + case "married-put": + return _generate_married_put_strategy( + strike, premium, n, expiration, prev_pos + ) + case "bull-call": + return _generate_bull_call_strategy( + strike, premium, n, expiration, prev_pos, higher_strike + ) + case "protective-collar": + return _generate_protective_collar_strategy( + strike, premium, n, expiration, prev_pos, higher_strike + ) + case _: + raise ValueError("Strategy not defined") + + +def _generate_covered_call_strategy( + strike: float, premium: float, n: int, expiration: dt.date, prev_pos: float | None +) -> list[Strategy]: + + return [ + StockStrategy(n=n, prev_pos=prev_pos, action="buy"), + OptionStrategy( + n=n, + prev_pos=prev_pos, + strike=strike, + premium=premium, + type="call", + action="sell", + expiration=expiration, + ), + ] + + +def _generate_married_put_strategy( + strike: float, premium: float, n: int, expiration: dt.date, prev_pos: float | None +) -> list[Strategy]: + + return [ + StockStrategy(n=n, prev_pos=prev_pos, action="buy"), + OptionStrategy( + n=n, + prev_pos=prev_pos, + strike=strike, + premium=premium, + type="put", + action="buy", + expiration=expiration, + ), + ] + + +def _generate_bull_call_strategy( + strike: float, + premium: float, + n: int, + expiration: dt.date, + prev_pos: float | None, + higher_strike: float | None, +) -> list[Strategy]: + + if higher_strike is None or higher_strike < strike: + raise ValueError("Higher strike price must be provided for bull call strategy") + + return [ + OptionStrategy( + n=n, + prev_pos=prev_pos, + strike=strike, + premium=premium, + type="call", + action="buy", + expiration=expiration, + ), + OptionStrategy( + n=n, + prev_pos=prev_pos, + strike=higher_strike, + premium=premium, + type="call", + action="sell", + expiration=expiration, + ), + ] + + +def _generate_protective_collar_strategy( + strike: float, + premium: float, + n: int, + expiration: dt.date, + prev_pos: float | None, + higher_strike: float | None, +) -> list[Strategy]: + + if higher_strike is None or higher_strike < strike: + raise ValueError( + "Higher strike price must be provided for protective caller strategy" + ) + + return [ + OptionStrategy( + n=n, + prev_pos=prev_pos, + strike=strike, + premium=premium, + type="put", + action="buy", + expiration=expiration, + ), + OptionStrategy( + n=n, + prev_pos=prev_pos, + strike=higher_strike, + premium=premium, + type="call", + action="sell", + expiration=expiration, + ), + ] diff --git a/optionsmonkey/utils.py b/optionsmonkey/utils.py index c8fbeb6..374dabc 100644 --- a/optionsmonkey/utils.py +++ b/optionsmonkey/utils.py @@ -9,3 +9,7 @@ def get_fridays_date(weeks_until: int = 0) -> dt.date: datetime = current_date + dt.timedelta(days=days_until_friday + weeks_until * 7) return datetime.date() + + +def coerce_to_multiple(v: float, multiple: int = 5) -> float: + return float(round(v / multiple) * multiple) diff --git a/poetry.lock b/poetry.lock index aef47db..8324f65 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1198,6 +1198,18 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pyhumps" +version = "3.8.0" +description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6"}, + {file = "pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3"}, +] + [[package]] name = "pyparsing" version = "3.1.1" @@ -1477,4 +1489,4 @@ repair = ["scipy (>=1.6.3)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "fa0f68f3624ed84a8cf60f26ab7fa8b593ac86cd84b91c9d9e5478537f6ff9ff" +content-hash = "a8389e38a8fa4a6e50143916dd239bf2eddcfec6c4bfef756ae8459b239248da" diff --git a/pyproject.toml b/pyproject.toml index 1a43c82..d17fec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ scipy = "^1.12.0" matplotlib = "^3.8.2" pydantic = "^2.5.3" yfinance = "^0.2.36" +pyhumps = "^3.8.0" [tool.poetry.group.dev.dependencies] black = "^24.1.0" diff --git a/tests/conftest.py b/tests/conftest.py index d19fd17..8eb9fdd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import pytest import datetime as dt + @pytest.fixture def nvidia(): stockprice = 168.99 diff --git a/tests/test_api.py b/tests/test_api.py index 2cb79e5..3eace57 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,7 @@ import pandas as pd from optionsmonkey.api import get_options_chain, get_stock_history +from optionsmonkey.models import UnderlyingAsset from optionsmonkey.utils import get_fridays_date @@ -8,15 +9,15 @@ def test_get_options_chain(): next_friday_date = get_fridays_date() - options = get_options_chain('MSFT', next_friday_date) + options = get_options_chain("MSFT", next_friday_date) assert isinstance(options.calls, pd.DataFrame) assert isinstance(options.puts, pd.DataFrame) - assert isinstance(options.underlying, dict) + assert isinstance(options.underlying, UnderlyingAsset) def test_get_stock_history(): - hist_df = get_stock_history('MSFT', 1) + hist_df = get_stock_history("MSFT", 1) assert isinstance(hist_df, pd.DataFrame) diff --git a/tests/test_core.py b/tests/test_core.py index 287759f..3bcae16 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -18,13 +18,13 @@ def test_covered_call(nvidia): "premium": 4.1, "n": 100, "action": "sell", + "expiration": nvidia["target_date"], }, ], ) ) - st = StrategyEngine() - st.getdata(inputs) + st = StrategyEngine(inputs) outputs = st.run() # Print useful information on screen @@ -53,20 +53,20 @@ def test_covered_call_w_prev_position(nvidia): | dict( # The covered call strategy is defined strategy=[ - {"type": "stock", "n": 100, "action": "buy", "prevpos": 158.99}, + {"type": "stock", "n": 100, "action": "buy", "prev_pos": 158.99}, { "type": "call", "strike": 185.0, "premium": 4.1, "n": 100, "action": "sell", + "expiration": nvidia["target_date"], }, ] ) ) - st = StrategyEngine() - st.getdata(inputs) + st = StrategyEngine(inputs) outputs = st.run() # Print useful information on screen @@ -100,7 +100,8 @@ def test_100_perc_itm(nvidia): "premium": 12.65, "n": 100, "action": "buy", - "prevpos": 7.5, + "prev_pos": 7.5, + "expiration": nvidia["target_date"], }, { "type": "call", @@ -108,13 +109,13 @@ def test_100_perc_itm(nvidia): "premium": 9.9, "n": 100, "action": "sell", + "expiration": nvidia["target_date"], }, ] ) ) - st = StrategyEngine() - st.getdata(inputs) + st = StrategyEngine(inputs) outputs = st.run() # Print useful information on screen From c7f06394a3441e0ebf53b9c566a825aab2de789e Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Feb 2024 19:24:21 -0500 Subject: [PATCH 2/3] small fixes --- optionsmonkey/engine.py | 46 +++++++++++++---------------------------- optionsmonkey/models.py | 1 - 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/optionsmonkey/engine.py b/optionsmonkey/engine.py index 38f454b..4dad0c0 100644 --- a/optionsmonkey/engine.py +++ b/optionsmonkey/engine.py @@ -46,22 +46,9 @@ def __init__(self, inputs: Inputs): self.profit_target_range: list[float] = [] self.loss_limit_ranges: list[float] = [] self.days_to_maturity: list[float] = [] - self.stock_price = None - self.volatility = None self.start_date = inputs.start_date - self.r = None - self.y = 0.0 - self.profit_target = None - self.loss_limit = None - self.opt_commission = 0.0 - self.stock_commission = 0.0 - self.min_stock = None - self.max_stock = None - self.distribution = "black-scholes" self.country = "US" self.days_to_target = 30 - self.nmc_prices = 100e3 - self.compute_expectation = False self.discard_nonbusinessdays = True self.days_in_year = 252 self.impvol: list[float] = [] @@ -74,13 +61,22 @@ def __init__(self, inputs: Inputs): self.profitprob = 0.0 self.profittargprob = 0.0 self.losslimitprob = 0.0 - + self.distribution = inputs.distribution + self.stock_price = inputs.stock_price + self.volatility = inputs.volatility + self.r = inputs.interest_rate + self.y = inputs.dividend_yield + self.min_stock = inputs.min_stock + self.max_stock = inputs.max_stock + self.profit_target = inputs.profit_target + self.loss_limit = inputs.loss_limit + self.opt_commission = inputs.opt_commission + self.stock_commission = inputs.stock_commission + self.nmc_prices = inputs.nmc_prices + self.compute_expectation = inputs.compute_expectation self.discard_nonbusinessdays = inputs.discard_nonbusiness_days - if self.discard_nonbusinessdays: - self.days_in_year = 252 - else: - self.days_in_year = 365 + self.days_in_year = 252 if self.discard_nonbusinessdays else 365 self.country = inputs.country @@ -157,20 +153,6 @@ def __init__(self, inputs: Inputs): else: raise ValueError("Type must be 'call', 'put', 'stock' or 'closed'!") - self.distribution = inputs.distribution - self.stock_price = inputs.stock_price - self.volatility = inputs.volatility - self.r = inputs.interest_rate - self.y = inputs.dividend_yield - self.min_stock = inputs.min_stock - self.max_stock = inputs.max_stock - self.profit_target = inputs.profit_target - self.loss_limit = inputs.loss_limit - self.opt_commission = inputs.opt_commission - self.stock_commission = inputs.stock_commission - self.nmc_prices = inputs.nmc_prices - self.compute_expectation = inputs.compute_expectation - self.use_dates = inputs.use_dates def run(self): """ diff --git a/optionsmonkey/models.py b/optionsmonkey/models.py index 78ec3d3..1a79359 100644 --- a/optionsmonkey/models.py +++ b/optionsmonkey/models.py @@ -161,7 +161,6 @@ class Inputs(BaseModel): opt_commission: float = 0.0 stock_commission: float = 0.0 compute_expectation: bool = False - use_dates: bool = True discard_nonbusiness_days: bool = True country: Country = "US" start_date: dt.date = Field(default_factory=dt.date.today) From c41a34da864e754f6d8e19dffd6e046c90c2dc1c Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Feb 2024 19:25:34 -0500 Subject: [PATCH 3/3] blacked --- optionsmonkey/engine.py | 1 - 1 file changed, 1 deletion(-) diff --git a/optionsmonkey/engine.py b/optionsmonkey/engine.py index 4dad0c0..d6b905b 100644 --- a/optionsmonkey/engine.py +++ b/optionsmonkey/engine.py @@ -153,7 +153,6 @@ def __init__(self, inputs: Inputs): else: raise ValueError("Type must be 'call', 'put', 'stock' or 'closed'!") - def run(self): """ run -> runs calculations for an options strategy.