mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 04:52:07 +00:00
Option Payoff Diagrams (#800)
* Began creating payoff matrix * Updated parser abilities * Finalized ability to add, remove, and show options * Added graph generator * Cleaned code * Attempting to fix black * Attempting to fix black * Fixed typings * Added James' changes * Made changes, added documentation * Added changes * Fixed my silly mistake * Didier requests added Co-authored-by: Colin Delahunty <colindelahunty@Colins-MacBook-Air.local> Co-authored-by: Colin Delahunty <colindelahunty@Colins-Air.indiana.edu>
This commit is contained in:
@@ -30,6 +30,7 @@ from gamestonk_terminal.options import (
|
||||
yfinance_view,
|
||||
)
|
||||
from gamestonk_terminal.stocks import stocks_controller
|
||||
from gamestonk_terminal.options import payoff_controller
|
||||
|
||||
|
||||
class OptionsController:
|
||||
@@ -60,6 +61,7 @@ class OptionsController:
|
||||
"grhist",
|
||||
"unu",
|
||||
"stocks",
|
||||
"payoff",
|
||||
"smile",
|
||||
]
|
||||
|
||||
@@ -136,6 +138,7 @@ Current Expiry: {self.selected_date or None}
|
||||
hist plot option history [Tradier]
|
||||
grhist plot option greek history [Syncretism.io]
|
||||
smile plot the volatility smile for the expiration date [Yfinance]
|
||||
> payoff shows payoff diagram for a selection of options [Yfinance]
|
||||
{Style.RESET_ALL if not colored else ''}"""
|
||||
print(help_text)
|
||||
|
||||
@@ -945,6 +948,18 @@ Current Expiry: {self.selected_date or None}
|
||||
print("Ticker and expiration required.")
|
||||
return
|
||||
|
||||
def call_payoff(self, _):
|
||||
"""Process payoff command"""
|
||||
if not self.ticker or not self.selected_date:
|
||||
print("Ticker and expiration required.\n")
|
||||
return None
|
||||
ret = payoff_controller.menu(self.ticker, self.selected_date)
|
||||
if ret is False:
|
||||
self.print_help()
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def call_oi(self, other_args: List[str]):
|
||||
"""Process oi command"""
|
||||
parser = argparse.ArgumentParser(
|
||||
|
||||
371
gamestonk_terminal/options/payoff_controller.py
Normal file
371
gamestonk_terminal/options/payoff_controller.py
Normal file
@@ -0,0 +1,371 @@
|
||||
""" Payoff Controller Module """
|
||||
__docformat__ = "numpy"
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from typing import List, Dict
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from prompt_toolkit.completion import NestedCompleter
|
||||
|
||||
from gamestonk_terminal import feature_flags as gtff
|
||||
from gamestonk_terminal.helper_funcs import (
|
||||
get_flair,
|
||||
parse_known_args_and_warn,
|
||||
)
|
||||
from gamestonk_terminal.menu import session
|
||||
from gamestonk_terminal.options.yfinance_model import get_option_chain, get_price
|
||||
from gamestonk_terminal.options.yfinance_view import plot_payoff
|
||||
|
||||
|
||||
class Payoff:
|
||||
|
||||
CHOICES = ["cls", "?", "help", "q", "quit", "list"]
|
||||
CHOICES_COMMANDS = [
|
||||
"list",
|
||||
"add",
|
||||
"rmv",
|
||||
"pick",
|
||||
"plot",
|
||||
]
|
||||
CHOICES += CHOICES_COMMANDS
|
||||
|
||||
# pylint: disable=dangerous-default-value
|
||||
def __init__(self, ticker: str, expiration: str):
|
||||
"""Construct Payoff"""
|
||||
|
||||
self.po_parser = argparse.ArgumentParser(add_help=False, prog="po")
|
||||
self.po_parser.add_argument("cmd", choices=self.CHOICES)
|
||||
self.chain = get_option_chain(ticker, expiration)
|
||||
self.calls = list(
|
||||
zip(
|
||||
self.chain.calls["strike"].tolist(),
|
||||
self.chain.calls["lastPrice"].tolist(),
|
||||
)
|
||||
)
|
||||
self.puts = list(
|
||||
zip(
|
||||
self.chain.puts["strike"].tolist(),
|
||||
self.chain.puts["lastPrice"].tolist(),
|
||||
)
|
||||
)
|
||||
self.ticker = ticker
|
||||
self.expiration = expiration
|
||||
self.options: List[Dict[str, str]] = []
|
||||
self.underlying = 0
|
||||
self.current_price = get_price(ticker)
|
||||
|
||||
@staticmethod
|
||||
def print_help(underlying):
|
||||
"""Print help"""
|
||||
if underlying == 1:
|
||||
text = "Long"
|
||||
elif underlying == 0:
|
||||
text = "None"
|
||||
elif underlying == -1:
|
||||
text = "Short"
|
||||
|
||||
help_text = f"""
|
||||
>>OPTION PAYOFF DIAGRAM<<
|
||||
|
||||
Underlying Asset: {text}
|
||||
|
||||
list list available strike prices for calls and puts
|
||||
|
||||
pick long, short, or none (default) underlying asset
|
||||
add add option to the list of the options to be plotted
|
||||
rmv remove option from the list of the options to be plotted
|
||||
|
||||
plot show the option payoff diagram
|
||||
"""
|
||||
print(help_text)
|
||||
|
||||
def switch(self, an_input: str):
|
||||
"""Process and dispatch input
|
||||
|
||||
Returns
|
||||
-------
|
||||
True, False or None
|
||||
False - quit the menu
|
||||
True - quit the program
|
||||
None - continue in the menu
|
||||
"""
|
||||
|
||||
# Empty command
|
||||
if not an_input:
|
||||
print("")
|
||||
return None
|
||||
|
||||
(known_args, other_args) = self.po_parser.parse_known_args(an_input.split())
|
||||
|
||||
# Help menu again
|
||||
if known_args.cmd == "?":
|
||||
self.print_help(self.underlying)
|
||||
return None
|
||||
|
||||
# Clear screen
|
||||
if known_args.cmd == "cls":
|
||||
os.system("cls||clear")
|
||||
return None
|
||||
|
||||
return getattr(
|
||||
self, "call_" + known_args.cmd, lambda: "Command not recognized!"
|
||||
)(other_args)
|
||||
|
||||
def call_help(self, _):
|
||||
"""Process Help command"""
|
||||
self.print_help(self.underlying)
|
||||
|
||||
def call_q(self, _):
|
||||
"""Process Q command - quit the menu"""
|
||||
return False
|
||||
|
||||
def call_quit(self, _):
|
||||
"""Process Quit command - quit the program"""
|
||||
return True
|
||||
|
||||
def call_list(self, _):
|
||||
"""Lists available calls and puts"""
|
||||
length = max(len(self.calls), len(self.puts)) - 1
|
||||
print(
|
||||
"Add an option using the index on the left, and not the actual strike price"
|
||||
)
|
||||
print("#\tcall\tput")
|
||||
for i in range(length):
|
||||
call = self.calls[i][0] if i < len(self.calls) else ""
|
||||
put = self.puts[i][0] if i < len(self.puts) else ""
|
||||
print(f"{i}\t{call}\t{put}")
|
||||
print("")
|
||||
|
||||
def call_add(self, other_args: List[str]):
|
||||
"""Process add command"""
|
||||
self.add_option(other_args)
|
||||
|
||||
def call_rmv(self, other_args: List[str]):
|
||||
"""Process rmv command"""
|
||||
self.rmv_option(other_args)
|
||||
|
||||
def call_pick(self, other_args: List[str]):
|
||||
"""Process pick command"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
prog="long",
|
||||
description="This function plots option payoff diagrams",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--type",
|
||||
dest="type",
|
||||
type=str,
|
||||
help="choose what you would like to do with the underlying asset, choose from: long, short, or none",
|
||||
required="-h" not in other_args,
|
||||
)
|
||||
|
||||
if other_args:
|
||||
if "-" not in other_args[0]:
|
||||
other_args.insert(0, "-t")
|
||||
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if not ns_parser:
|
||||
return
|
||||
|
||||
if ns_parser.type == "long":
|
||||
self.underlying = 1
|
||||
elif ns_parser.type == "none":
|
||||
self.underlying = 0
|
||||
elif ns_parser.type == "short":
|
||||
self.underlying = -1
|
||||
|
||||
self.print_help(self.underlying)
|
||||
|
||||
def call_plot(self, other_args):
|
||||
"""Process plot command"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
prog="plot",
|
||||
description="This function plots option payoff diagrams",
|
||||
)
|
||||
|
||||
try:
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if not ns_parser:
|
||||
return
|
||||
|
||||
plot_payoff(
|
||||
self.current_price,
|
||||
self.options,
|
||||
self.underlying,
|
||||
self.ticker,
|
||||
self.expiration,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(e, "\n")
|
||||
|
||||
def show_setup(self, nl: bool = False):
|
||||
"""Shows the current assets to display in the diagram"""
|
||||
print("#\tType\tHold\tStrike\tCost")
|
||||
for i, o in enumerate(self.options):
|
||||
sign = "Long" if o["sign"] == 1 else "Short"
|
||||
print(f"{i}\t{o['type']}\t{sign}\t{o['strike']}\t{o['cost']}")
|
||||
if nl:
|
||||
print("")
|
||||
|
||||
def add_option(self, other_args: List[str]):
|
||||
"""Add an option to the diagram"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
prog="add",
|
||||
description="""Add options to the diagram.""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--put",
|
||||
dest="put",
|
||||
action="store_true",
|
||||
help="buy a put instead of a call",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--short",
|
||||
dest="short",
|
||||
action="store_true",
|
||||
help="short the option instead of buying it",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--index",
|
||||
dest="index",
|
||||
type=int,
|
||||
help="list index of the option",
|
||||
required="-h" not in other_args and "-k" not in other_args,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--strike",
|
||||
dest="strike",
|
||||
type=float,
|
||||
help="strike price for the option",
|
||||
default=None,
|
||||
)
|
||||
try:
|
||||
|
||||
if other_args:
|
||||
if "-" not in other_args[0]:
|
||||
other_args.insert(0, "-i")
|
||||
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if not ns_parser:
|
||||
return
|
||||
|
||||
opt_type = "put" if ns_parser.put else "call"
|
||||
sign = -1 if ns_parser.short else 1
|
||||
options_list = self.puts if ns_parser.put else self.calls
|
||||
if ns_parser.strike is None:
|
||||
index = ns_parser.index
|
||||
elif float(ns_parser.strike) in [float(x[0]) for x in options_list]:
|
||||
index = filter(
|
||||
lambda x: float(x[0]) == float(ns_parser.strike), options_list
|
||||
)
|
||||
index = options_list.index(list(index)[0])
|
||||
print(index)
|
||||
else:
|
||||
print("Invalid strike price, please select a valid price from the list")
|
||||
return
|
||||
try:
|
||||
strike = options_list[index][0]
|
||||
cost = options_list[index][1]
|
||||
except IndexError:
|
||||
print("Please use the index, and not the strike price\n")
|
||||
return
|
||||
|
||||
option = {"type": opt_type, "sign": sign, "strike": strike, "cost": cost}
|
||||
self.options.append(option)
|
||||
|
||||
self.show_setup(True)
|
||||
|
||||
except Exception as e:
|
||||
print(e, "\n")
|
||||
|
||||
def rmv_option(self, other_args: List[str]):
|
||||
"""Remove one of the options from the diagram"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
prog="rmv",
|
||||
description="""Remove one of the options to be shown in the payoff.""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--strike",
|
||||
dest="strike",
|
||||
type=int,
|
||||
help="strike price for option",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--all",
|
||||
dest="all",
|
||||
action="store_true",
|
||||
help="remove all of the options",
|
||||
default=False,
|
||||
)
|
||||
try:
|
||||
if other_args:
|
||||
if "-" not in other_args[0]:
|
||||
other_args.insert(0, "-k")
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if not ns_parser:
|
||||
return
|
||||
|
||||
if ns_parser.all:
|
||||
self.options = []
|
||||
else:
|
||||
try:
|
||||
del self.options[ns_parser.strike]
|
||||
except IndexError:
|
||||
print("Please use the index, and not the strike price\n")
|
||||
return
|
||||
|
||||
self.show_setup(True)
|
||||
|
||||
except Exception as e:
|
||||
print(e, "\n")
|
||||
|
||||
|
||||
def menu(ticker: str, expiration: str):
|
||||
"""Portfolio Optimization Menu"""
|
||||
plt.close("all")
|
||||
po_controller = Payoff(ticker, expiration)
|
||||
po_controller.call_help(None)
|
||||
|
||||
while True:
|
||||
# Get input command from user
|
||||
if session and gtff.USE_PROMPT_TOOLKIT:
|
||||
completer = NestedCompleter.from_nested_dict(
|
||||
{c: None for c in po_controller.CHOICES}
|
||||
)
|
||||
an_input = session.prompt(
|
||||
f"{get_flair()} (options)>(payoff)> ",
|
||||
completer=completer,
|
||||
)
|
||||
else:
|
||||
an_input = input(f"{get_flair()} (options)>(payoff)> ")
|
||||
|
||||
try:
|
||||
plt.close("all")
|
||||
|
||||
process_input = po_controller.switch(an_input)
|
||||
|
||||
if process_input is not None:
|
||||
return process_input
|
||||
|
||||
except SystemExit:
|
||||
print("The command selected doesn't exist\n")
|
||||
continue
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Yfinance options model"""
|
||||
__docformat__ = "numpy"
|
||||
|
||||
from typing import List, Dict, Tuple, Any
|
||||
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def option_expirations(ticker: str):
|
||||
@@ -24,7 +27,7 @@ def option_expirations(ticker: str):
|
||||
return dates
|
||||
|
||||
|
||||
def get_option_chain(ticker: str, expiration: str):
|
||||
def get_option_chain(ticker: str, expiration: str) -> pd.DataFrame:
|
||||
"""Gets option chain from yf for given ticker and expiration
|
||||
|
||||
Parameters
|
||||
@@ -42,3 +45,63 @@ def get_option_chain(ticker: str, expiration: str):
|
||||
yf_ticker = yf.Ticker(ticker)
|
||||
chains = yf_ticker.option_chain(expiration)
|
||||
return chains
|
||||
|
||||
|
||||
def get_x_values(current_price: float, options: List[Dict[str, int]]) -> List[float]:
|
||||
"""Generates different price values that need to be tested"""
|
||||
x_list = list(range(101))
|
||||
mini = current_price
|
||||
maxi = current_price
|
||||
if len(options) == 0:
|
||||
mini *= 0.5
|
||||
maxi *= 1.5
|
||||
elif len(options) > 0:
|
||||
biggest = max(options, key=lambda x: x["strike"])
|
||||
smallest = min(options, key=lambda x: x["strike"])
|
||||
maxi = max(maxi, biggest["strike"]) * 1.2
|
||||
mini = min(mini, smallest["strike"]) * 0.8
|
||||
num_range = maxi - mini
|
||||
return [(x / 100) * num_range + mini for x in x_list]
|
||||
|
||||
|
||||
def get_y_values(
|
||||
base: float,
|
||||
price: float,
|
||||
options: List[Dict[Any, Any]],
|
||||
underlying: int,
|
||||
) -> float:
|
||||
"""Generates y values for corresponding x values"""
|
||||
option_change = 0
|
||||
change = price - base
|
||||
for option in options:
|
||||
if option["type"] == "call":
|
||||
abs_change = price - option["strike"] if price > option["strike"] else 0
|
||||
option_change += option["sign"] * abs_change
|
||||
elif option["type"] == "put":
|
||||
abs_change = option["strike"] - price if price < option["strike"] else 0
|
||||
option_change += option["sign"] * abs_change
|
||||
return (change * underlying) + option_change
|
||||
|
||||
|
||||
def generate_data(
|
||||
current_price: float, options: List[Dict[str, int]], underlying: int
|
||||
) -> Tuple[List[float], List[float], List[float]]:
|
||||
"""Gets x values, and y values before and after premiums"""
|
||||
x_vals = get_x_values(current_price, options)
|
||||
base = current_price
|
||||
total_cost = sum(x["cost"] for x in options)
|
||||
before = [get_y_values(base, x, options, underlying) for x in x_vals]
|
||||
if total_cost != 0:
|
||||
after = [
|
||||
get_y_values(base, x, options, underlying) - total_cost for x in x_vals
|
||||
]
|
||||
return x_vals, before, after
|
||||
return x_vals, before, []
|
||||
|
||||
|
||||
def get_price(ticker) -> float:
|
||||
"""Get current price for a given ticker"""
|
||||
ticker_yahoo = yf.Ticker(ticker)
|
||||
data = ticker_yahoo.history()
|
||||
last_quote = data.tail(1)["Close"].iloc[0]
|
||||
return last_quote
|
||||
|
||||
@@ -3,6 +3,7 @@ __docformat__ = "numpy"
|
||||
|
||||
import os
|
||||
from bisect import bisect_left
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
@@ -14,6 +15,7 @@ import gamestonk_terminal.config_plot as cfp
|
||||
import gamestonk_terminal.feature_flags as gtff
|
||||
from gamestonk_terminal.helper_funcs import export_data, plot_autoscale
|
||||
from gamestonk_terminal.options import op_helpers, yfinance_model
|
||||
from gamestonk_terminal.options.yfinance_model import generate_data
|
||||
|
||||
|
||||
def plot_oi(
|
||||
@@ -421,3 +423,28 @@ def plot_smile(ticker: str, expiration: str, put: bool) -> None:
|
||||
ax.yaxis.set_major_formatter("{x:.2f}%")
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
|
||||
def plot_payoff(
|
||||
current_price: float,
|
||||
options: List[Dict[Any, Any]],
|
||||
underlying: int,
|
||||
ticker: str,
|
||||
expiration: str,
|
||||
) -> None:
|
||||
"""Generate a graph showing the option payoff diagram"""
|
||||
x, yb, ya = generate_data(current_price, options, underlying)
|
||||
_, ax = plt.subplots()
|
||||
if ya:
|
||||
ax.plot(x, yb, label="Payoff Before Premium")
|
||||
ax.plot(x, ya, label="Payoff After Premium")
|
||||
else:
|
||||
ax.plot(x, yb, label="Payoff")
|
||||
ax.set_title(f"Option Payoff Diagram for {ticker} on {expiration}")
|
||||
ax.set_ylabel("Profit")
|
||||
ax.set_xlabel("Underlying Asset Price at Expiration")
|
||||
ax.xaxis.set_major_formatter("${x:.2f}")
|
||||
ax.yaxis.set_major_formatter("${x:.2f}")
|
||||
plt.legend()
|
||||
plt.show()
|
||||
print("")
|
||||
|
||||
5
website/content/options/payoff/_index.md
vendored
Normal file
5
website/content/options/payoff/_index.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
geekdocCollapseSection: true
|
||||
---
|
||||
|
||||
{{< toc-tree >}}
|
||||
17
website/content/options/payoff/add/_index.md
vendored
Normal file
17
website/content/options/payoff/add/_index.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
```
|
||||
usage: add [-k ADD_STRIKE] [-i ADD_INDEX] [-s] [-p] [-h]
|
||||
```
|
||||
|
||||
Add option to the diagram.
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-i ADD_INDEX, --index ADD_INDEX
|
||||
index of strike to be used in
|
||||
the diagram.
|
||||
-k ADD_STRIKE, --strike ADD_STRIKE
|
||||
strike to be used in the diagram.
|
||||
-s, --short short the option instead of buying it
|
||||
-p, --put buy a put instead of a call
|
||||
-h, --help show this help message
|
||||
```
|
||||
11
website/content/options/payoff/pick/_index.md
vendored
Normal file
11
website/content/options/payoff/pick/_index.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
```
|
||||
usage: pick [-t OPTION] [-h]
|
||||
```
|
||||
|
||||
Choose to long, short, or not hold the underlying asset.
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-t, --type choose the action you want to perform
|
||||
-h, --help show this help message (default: False)
|
||||
```
|
||||
10
website/content/options/payoff/plot/_index.md
vendored
Normal file
10
website/content/options/payoff/plot/_index.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
```
|
||||
usage: plot [-h]
|
||||
```
|
||||
|
||||
Shows the options payoff diagram.
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-h, --help show this help message (default: False)
|
||||
```
|
||||
13
website/content/options/payoff/rmv/_index.md
vendored
Normal file
13
website/content/options/payoff/rmv/_index.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
```
|
||||
usage: rmv [-k RMV_STRIKE] [-h]
|
||||
```
|
||||
|
||||
Remove one of the options from the diagram.
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-k RMV_STRIKE, --strike RMV_STRIKE
|
||||
remove option from diagram
|
||||
-a, --all remove all options
|
||||
-h, --help show this help message
|
||||
```
|
||||
11
website/data/menu/main.yml
vendored
11
website/data/menu/main.yml
vendored
@@ -582,6 +582,17 @@ main:
|
||||
ref: "/options/grhist"
|
||||
- name: smile
|
||||
ref: "/options/smile"
|
||||
- name: portfolio optimization
|
||||
ref: "/portfolio/portfolio_optimization"
|
||||
sub:
|
||||
- name: add
|
||||
ref: "/options/payoff/add"
|
||||
- name: pick
|
||||
ref: "/options/payoff/pick"
|
||||
- name: plot
|
||||
ref: "/options/payoff/plot"
|
||||
- name: rmv
|
||||
ref: "/options/payoff/rmv"
|
||||
- name: etf
|
||||
ref: "/etf"
|
||||
sub:
|
||||
|
||||
Reference in New Issue
Block a user