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:
Colin Delahunty
2021-10-11 20:47:29 -04:00
committed by GitHub
parent b16ad0014f
commit 6b09893bba
10 changed files with 544 additions and 1 deletions

View File

@@ -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(

View 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

View File

@@ -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

View File

@@ -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("")

View File

@@ -0,0 +1,5 @@
---
geekdocCollapseSection: true
---
{{< toc-tree >}}

View 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
```

View 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)
```

View File

@@ -0,0 +1,10 @@
```
usage: plot [-h]
```
Shows the options payoff diagram.
```
optional arguments:
-h, --help show this help message (default: False)
```

View 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
```

View File

@@ -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: