diff --git a/README.md b/README.md index c1df388..84797e5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,68 @@ ## Black-Scholes model in pure Python ### Without SciPy, NumPy or other external dependencies -Use **example_greeks.py** to calculate *Theo, Delta, Theta, Vega, Gamma* for single option +# PyOption Pricing & Greeks -Use **example_plot.py** to visualize your position with *Matplotlib* +This project provides a set of tools in Python for calculating the theoretical price of options using the Black-Scholes-Merton (BSM) model, as well as the main Greeks (Delta, Vega, Theta, and Gamma). Additionally, it offers functionalities to visualize the P&L (Profit and Loss) and the Greeks of an option portfolio under different price scenarios and expiration dates. + +## Key Features + +* **Option Pricing:** Implementation of the Black-Scholes-Merton model to calculate the theoretical price of call and put options. +* **Greeks Calculation:** Provides functions to calculate option sensitivities: + * **Delta ($\Delta$):** Measures the sensitivity of the option price to a change in the underlying asset price. + * **Vega ($\nu$):** Measures the sensitivity of the option price to a change in the implied volatility of the underlying asset. + * **Theta ($\Theta$):** Measures the sensitivity of the option price to the passage of time (time decay). + * **Gamma ($\Gamma$):** Measures the rate of change of Delta with respect to a change in the underlying asset price. +* **Portfolio Analysis:** Allows for the analysis of a portfolio composed of multiple options and the underlying asset, calculating the total Greeks and P&L of the portfolio. +* **Graphical Visualization:** Utilizes the `matplotlib` library to generate graphs of: + * Portfolio P&L as a function of the underlying asset price for different expiration dates. + * The Greeks (Delta, Theta, Vega, Gamma) of the portfolio as a function of the underlying asset price for different expiration dates. +* **(In Development) Delta Neutral Search:** The `searching.py` module appears to contain in-development functionalities for finding prices where a specific individual option neutralizes the portfolio's Delta. + +## How to Use + +### Prerequisites + +* Python 3.x +* `matplotlib` library + +### Installation + +1. Ensure you have Python 3 installed on your system. +2. Install the `matplotlib` library using pip if you haven't already: + ```bash + pip install matplotlib + ``` + +### Usage Examples + +1. **Calculating Greeks for an Option:** Run the `exemple_greeks.py` script: + ```bash + python exemple_greeks.py + ``` + This script demonstrates how to instantiate the `BSM` class and calculate the theoretical price, Delta, Theta, Vega, and Gamma for a Bitcoin option with example parameters. + +2. **Visualizing Portfolio P&L and Greeks:** Run the `exemple_plot.py` script: + ```bash + python exemple_plot.py + ``` + This script uses the `Plot` class to generate graphs showing the P&L, Delta, Theta, Vega, and Gamma of an option portfolio defined within the script, across different expiration dates. The legends on the graphs indicate the number of days until expiration. + +3. **(In Development) Delta Neutral Search:** The `searching.py` script contains a `Search` class with a `searchDelta` method. To use it (remember it's under development), you can execute the script and observe the output: + ```bash + python searching.py + ``` + The output displays a table with the underlying asset price, option type, strike price, quantity, individual option Delta, total portfolio Delta, and the theoretical option price, searching for points where the individual option's Delta offsets the total portfolio Delta. + +## Project Structure + +* `bsm.py`: Contains the implementation of the `BSM` class with methods to calculate the theoretical price (theo), the Greeks (delta, vega, theta, gamma), and the standard normal distribution functions (pdf and cdf). +* `pricing.py`: Extends the `BSM` class and implements methods to calculate the total Greeks (`deltaFull`, `vegaFull`, `thetaFull`, `gammaFull`) and the P&L (`p_l`) of a portfolio of assets and options. +* `plot.py`: Utilizes the `Pricing` class and the `matplotlib` library to generate graphs of the portfolio's P&L and Greeks as a function of the underlying asset price for different expiration dates. +* `exemple_greeks.py`: An example script demonstrating how to use the `BSM` class to calculate the Greeks of a single option. +* `exemple_plot.py`: An example script that uses the `Plot` class to generate graphical visualizations of an option portfolio. +* `searching.py`: A script containing a `Search` class with functionalities under development to assist in finding delta-neutral positions, comparing the delta of an individual option with the total portfolio delta. + +## Contributing + +Contributions are welcome! Feel free to open issues to report bugs or suggest improvements, and submit pull requests with your implementations. \ No newline at end of file diff --git a/example_plot.py b/example_plot.py index 0eb2006..daf7600 100644 --- a/example_plot.py +++ b/example_plot.py @@ -1,51 +1,72 @@ from plot import Plot import matplotlib.pyplot as plt +# Creates an instance of the Plot class, which inherits pricing functionalities plot = Plot() +# Defines the number of days in a year (used to convert time for the BSM model) god = 365 +# Defines the price increment to generate points on the graphs step = 10 +# Defines the depth (range) of the price around the current price for the graphs deep = 7500 -exp = 30 / god -exp2 = 15 / god -exp3 = 5 / god +# Defines different expiration times in years +exp = 30 / god # Approximately 30 days +exp2 = 15 / god # Approximately 15 days +exp3 = 5 / god # Approximately 5 days +# Defines the current price (spot price) of the underlying asset fPrice = 21000 +# Initializes a list to store the parameters of the positions in the portfolio params = [] -dType = 'F' -quant = 1 -price = 20000 -strike = 0 -vola = 0 -params.append({'dType': dType, 'price': price, 'quant': quant, 'strike': strike, 'vola': vola, 'exp': exp}) - -dType = 'C' -quant = 1 -price = 500 -strike = 25000 -vola = 0.75 -params.append({'dType': dType, 'price': price, 'quant': quant, 'strike': strike, 'vola': vola, 'exp': exp}) - -dType = 'P' -quant = 1 -price = 100 -strike = 15000 -vola = 0.75 -params.append({'dType': dType, 'price': price, 'quant': quant, 'strike': strike, 'vola': vola, 'exp': exp}) - -dType = 'P' -quant = -1 -price = 25 -strike = 10000 -vola = 1.5 -params.append({'dType': dType, 'price': price, 'quant': quant, 'strike': strike, 'vola': vola, 'exp': exp}) - +# Adds a position of the underlying asset (F - Future/Forward) +params.append({ + 'dType': 'F', # Type: Future/Forward asset + 'quant': 1, # Quantity: 1 (long 1 unit) + 'price': 20000, # Purchase price of the asset + 'strike': 0, # Strike not applicable for the asset + 'vola': 0, # Volatility not applicable for the asset + 'exp': exp # Expiration time (irrelevant for the asset, but needed in the structure) +}) + +# Adds a long position (buy - 1) of a call option (C - Call) +params.append({ + 'dType': 'C', # Type: Call Option + 'quant': 1, # Quantity: 1 (long 1 contract) + 'price': 500, # Premium paid for the option + 'strike': 25000, # Strike Price + 'vola': 0.75, # Implied Volatility (75%) + 'exp': exp # Expiration time +}) + +# Adds a long position (buy - 1) of a put option (P - Put) +params.append({ + 'dType': 'P', # Type: Put Option + 'quant': 1, # Quantity: 1 (long 1 contract) + 'price': 100, # Premium paid for the option + 'strike': 15000, # Strike Price + 'vola': 0.75, # Implied Volatility (75%) + 'exp': exp # Expiration time +}) + +# Adds a short position (sell - -1) of a put option (P - Put) +params.append({ + 'dType': 'P', # Type: Put Option + 'quant': -1, # Quantity: -1 (short 1 contract) + 'price': 25, # Premium received for selling the option + 'strike': 10000, # Strike Price + 'vola': 1.5, # Implied Volatility (150%) + 'exp': exp # Expiration time +}) + +# Defines the lower and upper price limits for the graphs bePriceS = fPrice - deep bePriceF = fPrice + deep +# Plots the Profit/Loss (P/L) of the portfolio for different expiration times plot.plotPL(bePriceS, bePriceF, params, exp, step) plot.plotPL(bePriceS, bePriceF, params, exp2, step) plot.plotPL(bePriceS, bePriceF, params, exp3, step) @@ -56,6 +77,7 @@ plt.legend() plt.show() +# Plots the Delta of the portfolio for different expiration times plot.plotDelta(bePriceS, bePriceF, params, exp, step) plot.plotDelta(bePriceS, bePriceF, params, exp2, step) plot.plotDelta(bePriceS, bePriceF, params, exp3, step) @@ -66,6 +88,7 @@ plt.ylabel("Delta") plt.show() +# Plots the Theta of the portfolio for different expiration times plot.plotTheta(bePriceS, bePriceF, params, exp, step) plot.plotTheta(bePriceS, bePriceF, params, exp2, step) plot.plotTheta(bePriceS, bePriceF, params, exp3, step) @@ -76,6 +99,7 @@ plt.ylabel("Theta") plt.show() +# Plots the Vega of the portfolio for different expiration times plot.plotVega(bePriceS, bePriceF, params, exp, step) plot.plotVega(bePriceS, bePriceF, params, exp2, step) plot.plotVega(bePriceS, bePriceF, params, exp3, step) @@ -86,6 +110,7 @@ plt.ylabel("Vega") plt.show() +# Plots the Gamma of the portfolio for different expiration times plot.plotGamma(bePriceS, bePriceF, params, exp, step) plot.plotGamma(bePriceS, bePriceF, params, exp2, step) plot.plotGamma(bePriceS, bePriceF, params, exp3, step) @@ -96,13 +121,9 @@ plt.ylabel("Gamma") plt.show() - -print('D:\t', round(plot.deltaFull(fPrice, params, exp), 2)) - -print('V:\t', round(plot.vegaFull(fPrice, params, exp), 2)) - -print('T:\t', round(plot.thetaFull(fPrice, params, exp), 2)) - -print('G:\t', round(plot.gammaFull(fPrice, params, exp), 2)) - +# Prints the aggregated (Full) values of the Greeks and P/L at the current price +print('Delta:\t', round(plot.deltaFull(fPrice, params, exp), 2)) +print('Vega:\t', round(plot.vegaFull(fPrice, params, exp), 2)) +print('Theta:\t', round(plot.thetaFull(fPrice, params, exp), 2)) +print('Gamma:\t', round(plot.gammaFull(fPrice, params, exp), 2)) print('P/L:\t', plot.p_l(fPrice, params, exp)) \ No newline at end of file diff --git a/test_calculator.py b/test_calculator.py new file mode 100644 index 0000000..18e8a15 --- /dev/null +++ b/test_calculator.py @@ -0,0 +1,40 @@ +import pytest +from bsm import BSM + +def test_call_option_positive_price(): + # Test for positive call option price + model = BSM() + price = model.theo(S=100, K=90, V=0.2, T=1, dT='C') + assert price > 0 + +def test_put_option_positive_price(): + # Test for positive put option price + model = BSM() + price = model.theo(S=90, K=100, V=0.2, T=1, dT='P') + assert price > 0 + +def test_call_price_increases_with_volatility(): + # Test call price sensitivity to volatility + model = BSM() + low_vola = model.theo(S=100, K=100, V=0.1, T=1, dT='C') + high_vola = model.theo(S=100, K=100, V=0.3, T=1, dT='C') + assert high_vola > low_vola + +def test_put_price_decreases_with_spot_price(): + # Test put price sensitivity to spot price + model = BSM() + high_spot = model.theo(S=110, K=100, V=0.2, T=1, dT='P') + low_spot = model.theo(S=90, K=100, V=0.2, T=1, dT='P') + assert low_spot > high_spot + +def test_invalid_option_type_raises_value_error(): + # Test handling of invalid option type + model = BSM() + with pytest.raises(ValueError, match="Tipo de opção inválido"): + model.theo(S=100, K=100, V=0.2, T=1, dT='X') + +def test_negative_spot_price_raises_value_error(): + # Test handling of negative spot price + model = BSM() + with pytest.raises(ValueError, match="Preço à vista não pode ser negativo"): + model.theo(-100, 100, 0.2, 1, 'C') \ No newline at end of file diff --git a/test_plot.py b/test_plot.py new file mode 100644 index 0000000..8ba757f --- /dev/null +++ b/test_plot.py @@ -0,0 +1,138 @@ +import pytest +from bsm import BSM +import matplotlib.pyplot as plt +from plot import Plot # Import the Plot class + +def test_call_option_pricing_positive_value(): + # Basic test to ensure the price of a call option is positive + model = BSM() + price = model.theo(S=100, K=90, V=0.2, T=1, dT='C') + assert price > 0 + +def test_put_option_pricing_positive_value(): + # Basic test to ensure the price of a put option is positive + model = BSM() + price = model.theo(S=90, K=100, V=0.2, T=1, dT='P') + assert price > 0 + +def test_call_option_price_higher_volatility(): + # Test to check if higher volatility results in a higher call option price + model = BSM() + low_vola_price = model.theo(S=100, K=100, V=0.1, T=1, dT='C') + high_vola_price = model.theo(S=100, K=100, V=0.3, T=1, dT='C') + assert high_vola_price > low_vola_price + +def test_put_option_price_longer_maturity(): + # Test to check if longer time to maturity results in a higher put option price (in some cases) + model = BSM() + short_expiry_price = model.theo(S=100, K=100, V=0.2, T=0.5, dT='P') + long_expiry_price = model.theo(S=100, K=100, V=0.2, T=1.5, dT='P') + assert long_expiry_price > short_expiry_price + +def test_invalid_option_type_raises_error(): + # Test to ensure an invalid option type raises an error + model = BSM() + with pytest.raises(ValueError, match="Tipo de opção inválido"): + model.theo(S=100, K=100, V=0.2, T=1, dT='X') + +def test_plot_pl_runs_without_error_basic_params(): + # Basic test to check if the P/L plotting function runs without error + plotter = Plot() + params = [{'dType': 'C', 'price': 1, 'quant': 1, 'strike': 105, 'vola': 0.2, 'exp': 1/365}] + plotter.plotPL(bePriceS=90, bePriceF=110, params=params, exp=1/365, step=5) + +import pytest +from bsm import BSM +import matplotlib.pyplot as plt +from plot import Plot # Import the Plot class + +def test_call_option_pricing_positive_value(): + # Basic test to ensure the price of a call option is positive + model = BSM() + price = model.theo(S=100, K=90, V=0.2, T=1, dT='C') + assert price > 0 + +def test_put_option_pricing_positive_value(): + # Basic test to ensure the price of a put option is positive + model = BSM() + price = model.theo(S=90, K=100, V=0.2, T=1, dT='P') + assert price > 0 + +def test_call_option_price_higher_volatility(): + # Test to check if higher volatility results in a higher call option price + model = BSM() + low_vola_price = model.theo(S=100, K=100, V=0.1, T=1, dT='C') + high_vola_price = model.theo(S=100, K=100, V=0.3, T=1, dT='C') + assert high_vola_price > low_vola_price + +def test_put_option_price_longer_maturity(): + # Test to check if longer time to maturity results in a higher put option price (in some cases) + model = BSM() + short_expiry_price = model.theo(S=100, K=100, V=0.2, T=0.5, dT='P') + long_expiry_price = model.theo(S=100, K=100, V=0.2, T=1.5, dT='P') + assert long_expiry_price > short_expiry_price + +def test_invalid_option_type_raises_error(): + # Test to ensure an invalid option type raises an error + model = BSM() + with pytest.raises(ValueError, match="Tipo de opção inválido"): + model.theo(S=100, K=100, V=0.2, T=1, dT='X') + +def test_plot_pl_runs_without_error_basic_params(): + # Basic test to check if the P/L plotting function runs without error + plotter = Plot() + params = [{'dType': 'C', 'price': 1, 'quant': 1, 'strike': 105, 'vola': 0.2, 'exp': 1/365}] + plotter.plotPL(bePriceS=90, bePriceF=110, params=params, exp=1/365, step=5) + +# Assuming the plotPL function returns the figure or axes for inspection (ideally) +# If it doesn't return, we can only check if there are no errors during execution. +def test_plot_pl_with_single_and_multiple_params(): + # Testing plotting with a single set of parameters + plotter = Plot() + params_single = [{'dType': 'C', 'price': 1, 'quant': 1, 'strike': 105, 'vola': 0.2, 'exp': 1/365}] + try: + plotter.plotPL(bePriceS=90, bePriceF=110, params=params_single, exp=1/365, step=5) + plt.close() # Closing the figure for the next test + assert True # If it reached here without error, we consider it a basic success + except Exception as e: + assert False, f"Error plotting with a single parameter: {e}" + + # Testing plotting with multiple sets of parameters + params_multiple = [ + {'dType': 'C', 'price': 1, 'quant': 1, 'strike': 105, 'vola': 0.2, 'exp': 1/365}, + {'dType': 'P', 'price': 1, 'quant': 1, 'strike': 95, 'vola': 0.25, 'exp': 30/365} + ] + try: + plotter.plotPL(bePriceS=80, bePriceF=120, params=params_multiple, exp=30/365, step=10) + plt.close() + assert True # If it reached here without error, we consider it a basic success + except Exception as e: + assert False, f"Error plotting with multiple parameters: {e}" + + # Testing plotting by passing a single element of the params list (indexed) + try: + plotter.plotPL(bePriceS=80, bePriceF=120, params=[params_multiple[0]], exp=1/365, step=5) + plt.close() + assert True # If it reached here without error, we consider it a basic success + except Exception as e: + assert False, f"Error plotting the first parameter individually: {e}" + + try: + plotter.plotPL(bePriceS=80, bePriceF=120, params=[params_multiple[1]], exp=30/365, step=10) + plt.close() + assert True # If it reached here without error, we consider it a basic success + except Exception as e: + assert False, f"Error plotting the second parameter individually: {e}" + +# We can create similar tests for plotDelta, plotTheta, plotVega, and plotGamma +# adapting the parameters as needed. + +def test_plot_delta_with_single_param_indexed(): + plotter = Plot() + params = [{'dType': 'C', 'price': 1, 'quant': 1, 'strike': 105, 'vola': 0.2, 'exp': 1/365}] + try: + plotter.plotDelta(bePriceS=90, bePriceF=110, params=[params[0]], exp=1/365, step=5) + plt.close() + assert True + except Exception as e: + assert False, f"Error plotting Delta with indexed parameter: {e}" \ No newline at end of file