From 419c3691db220c467d2979b19ca308b3b800c0bd Mon Sep 17 00:00:00 2001 From: jmaslek Date: Sun, 25 Jul 2021 11:30:38 -0400 Subject: [PATCH] Options menu updates (#560) * Add `calculator` function to show pnl for calls and puts * calculator improvement * breaking up op into different submenus * Yfinance functions/plots added * Refactored Tradier menu into new controller * Added tradier historical options and fix crypto candles * Clean up files and get rid of selenium dependecne * ReadME 1 * calculator plot * Redefine main menu structure to get rid of sub menus. * Merge main + update gst readme * Add coloring to chain command and update readme images * Add historical greeks * added ion * Fix up basic calculator * Clean up error messages and yf defaults when tradier key not present. Extend `exp` to allow date entry * add new line --- exports/options/.gitkeep | 0 gamestonk_terminal/README.md | 17 +- .../cryptocurrency/binance/binance_view.py | 2 +- .../coinpaprika/coinpaprika_view.py | 2 +- gamestonk_terminal/options/README.md | 214 ++++-- gamestonk_terminal/options/__init__.py | 0 gamestonk_terminal/options/barchart_view.py | 47 +- .../options/calculator_model.py | 143 ++++ gamestonk_terminal/options/op_controller.py | 284 ++++---- gamestonk_terminal/options/op_helpers.py | 412 +++++++++++ gamestonk_terminal/options/syncretism_view.py | 206 +++++- gamestonk_terminal/options/tradier_model.py | 188 +++++ gamestonk_terminal/options/tradier_view.py | 660 +++++++++++++++--- gamestonk_terminal/options/yahoo_view.py | 517 -------------- gamestonk_terminal/options/yfinance_model.py | 42 ++ gamestonk_terminal/options/yfinance_view.py | 367 ++++++++++ terminal.py | 13 +- 17 files changed, 2218 insertions(+), 896 deletions(-) create mode 100644 exports/options/.gitkeep create mode 100644 gamestonk_terminal/options/__init__.py create mode 100644 gamestonk_terminal/options/calculator_model.py create mode 100644 gamestonk_terminal/options/op_helpers.py create mode 100644 gamestonk_terminal/options/tradier_model.py delete mode 100644 gamestonk_terminal/options/yahoo_view.py create mode 100644 gamestonk_terminal/options/yfinance_model.py create mode 100644 gamestonk_terminal/options/yfinance_view.py diff --git a/exports/options/.gitkeep b/exports/options/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 4e06f5f025f..f0bcfef5977 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -424,13 +424,18 @@ Command|Description|Source Command|Description|Source ------ | --------|---- +`disp` | Display all preset screeners filters +`scr` | Output screener options +`load` | Load new ticker +`info` | Display option information | [Barchart](https://barchart.com/) +`calc` | Basic option PnL calculator `exp` | see/set expiry date -`volume` | volume + open interest options trading plot |[Yahoo Finance](https://finance.yahoo.com/) -`vcalls` | calls volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/) -`vputs` | puts volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/) `chains` | displays option chains |[Tradier](https://developer.tradier.com/) -`info` | display option information | [Barchart](https://barchart.com/) - +`oi` | Plot open interest +`vol` | Plot volume +`voi` | Plot volume and open interest +`hist` | Plot historical options data | [Tradier](https://developer.tradier.com/) +`gr_hist` | Plot historical option greek | [Syncretism](ops.syncretism.io)   ## Screener [»](screener/README.md) @@ -470,7 +475,7 @@ lis | latest insider sales blis | big latest insider sales ($100k+) blos | big latest officer sales ($100k+) blcs | big latest CEO/CFO sales ($100k+) -Top | +Top | topt | top officer purchases today toppw | top officer purchases past week toppm | top officer purchases past month diff --git a/gamestonk_terminal/cryptocurrency/binance/binance_view.py b/gamestonk_terminal/cryptocurrency/binance/binance_view.py index 8ddbebee199..86e77576b2f 100644 --- a/gamestonk_terminal/cryptocurrency/binance/binance_view.py +++ b/gamestonk_terminal/cryptocurrency/binance/binance_view.py @@ -62,7 +62,7 @@ def plot_candles(candles_df: pd.DataFrame, title: str): candles_df, type="candle", volume=True, - title=f"{title}", + title=f"\n{title}", xrotation=20, style="binance", figratio=(10, 7), diff --git a/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py b/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py index f653640006e..c0104f5e511 100644 --- a/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py +++ b/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py @@ -1319,7 +1319,7 @@ def chart(coin_id: str, other_args: List[str]): ] df = df.set_index(pd.to_datetime(df["Time0"])).drop("Time0", axis=1) title = ( - f"{coin_id}/{ns_parser.vs} from {df.index[0].strftime('%Y/%m/%d')} to {df.index[-1].strftime('%Y/%m/%d')}", + f"\n{coin_id}/{ns_parser.vs} from {df.index[0].strftime('%Y/%m/%d')} to {df.index[-1].strftime('%Y/%m/%d')}", ) df["Volume"] = df["Volume"] / 1_000_000 mpf.plot( diff --git a/gamestonk_terminal/options/README.md b/gamestonk_terminal/options/README.md index 4717c256616..0e66e05003b 100644 --- a/gamestonk_terminal/options/README.md +++ b/gamestonk_terminal/options/README.md @@ -8,19 +8,27 @@ This menu aims to give insight on options. Options can cause significant share p * Display all preset screeners filters * [scr](#scr) * Output screener options -* [exp](#exp) - * See/set expiry date [Yahoo Finance] -* [voi](#voi) - * Volume + open interest options trading plot [Yahoo Finance] -* [vcalls](#vcalls) - * Calls volume + open interest plot [Yahoo Finance] -* [vputs](#vputs) - * Puts volume + open interest plot [Yahoo Finance] -* [chains](#chains) - * Display option chains [Source: Tradier] +* [load](#load) + * Load new ticker * [info](#info) - * _**This requires selenium webdriver installed**_ * Display option information [Source: Barchart.com] +* [calc](#calc) + * basic call/put PnL calculator +* [exp](#exp) + * See/set expiry date +* [chains](#chains) + * Display option chains with greeks [Source: Tradier.com] +* [oi](#oi) + * Plot open interest +* [vol](#vol) + * Plot volume +* [voi](#voi) + * Plot volume and open interest +* [hist](#hist) + * Plot option history [Source: Tradier.com] +* [gr_hist](#gr_hist) + * Plot historical option greek [Source: ops.syncretism.io] + ## disp @@ -35,7 +43,6 @@ View available presets under [presets folder](/gamestonk_terminal/options/preset Captura de ecrã 2021-06-27, às 02 32 21 - ## scr ```text @@ -50,67 +57,64 @@ Day Low; SMDH: Regular Market Day High; LU: Last Trade Date; LC: Last Crawl; ITM Captura de ecrã 2021-06-27, às 02 31 28 +## load +Load new ticker +```python +usage: load [-t Ticker] +``` +* -t/--ticker: Ticker to load. Flag is optional, as `load aapl` will also work + +## info +This scrapes the options information from barchart.com/stocks overview. This includes information such as Historical Volatility and IV Rank. + +```` +usage: info +```` + +opinfo + +## calc +Basic profit calculator for options +```python +usage: calc [-s STRIKE] [-p PREMIUM] [--put] [--sell] [-h] +``` +* -s/--strike : Strike price to consider +* -p/--premium: Premium being paid +* --put: Flag to calculate for put option +* --sell: Flag to calculate for selling option + +![calc](https://user-images.githubusercontent.com/18151143/126883982-c54cb5ec-7eb2-41c1-812e-75e830e3703d.png) ## exp - +See/set expiry dates. ```text -usage: exp [-d {0,1,2,3,4,5,6,7,8,9,10,11}] +usage: exp [-d] [-D ``` -See/set expiry dates. [Source: Yahoo Finance] - * -d : Expiry date index to set. +* -D : Date to set -exp +Running `exp` will show available option expirations +```python +(✨) (op)> exp - -## voi - -```text -usage: voi [-v MIN_VOL] [-m MIN_SP] [-M MAX_SP] +Available expiry dates: + 0. 2021-07-30 + 1. 2021-08-06 + 2. 2021-08-13 + 3. 2021-08-20 + 4. 2021-08-27 + 5. 2021-09-03 ``` - -Plots Volume + Open Interest of calls vs puts. [Source: Yahoo Finance] - -* -v : Minimum volume (considering open interest) threshold of the plot. -* -m : Minimum strike price to consider in the plot. -* -M : Maximum strike price to consider in the plot. - -![voinio](https://user-images.githubusercontent.com/25267873/115161878-f7a19600-a097-11eb-889b-e9cc945f174d.png) - - -## vcalls - -```text -usage: vcalls [-m MIN_SP] [-M MAX_SP] +To set 2021-08-13, the following two options are equivalent +```python +exp 2 +exp -D 2021-08-13 ``` - -Plots Calls Volume + Open Interest. [Source: Yahoo Finance] - -* -m : Minimum strike price to consider in the plot. -* -M : Maximum strike price to consider in the plot. - -![vcalls](https://user-images.githubusercontent.com/25267873/115161874-f5d7d280-a097-11eb-93e2-3a2b20292f09.png) - - -## vputs - -```text -usage: vputs [-m MIN_SP] [-M MAX_SP] -``` - -Plots Puts Volume + Open Interest. [Source: Yahoo Finance] - -* -m : Minimum strike price to consider in the plot. -* -M : Maximum strike price to consider in the plot. - -![vputs](https://user-images.githubusercontent.com/25267873/115161873-f40e0f00-a097-11eb-8334-3d4f14b56766.png) - - ## chains ```` -usage: chains [--calls] [--puts] [-m MIN_SP] [-M MAX_SP] +usage: chains [--calls] [--puts] [-m MIN_SP] [-M MAX_SP] [-d] [--export] ```` Display options chains. [Source: Tradier] @@ -119,23 +123,81 @@ Display options chains. [Source: Tradier] * --puts : Flag to show puts only * -m : Minimum strike price to consider. * -M : Maximum strike price to consider. -* -d : Columns to display. Should be comma separated. Must be in * -d : Columns to display from the following selection: bid, ask, strike, bidsize, asksize, volume, open_interest, delta, gamma, theta, vega, ask_iv, bid_iv, mid_iv. Default: mid_iv,vega_theta_gamma,delta,volume,open_interest,bid,ask. Note that they are separated by comma. +* -d : Columns to display. Should be comma separated. Must be in * -d : Columns to display from the following selection: symbol, bid, ask, strike, bidsize, asksize, volume, open_interest, delta, gamma, theta, vega, iv. Note that they are separated by comma. +* --export: Flag to export. Options are {csv,json,xlsx}. + +![chains](https://user-images.githubusercontent.com/18151143/126855845-4f156b6d-a368-4e66-8dc9-b971792128d8.png) + +## oi +Plot open interest. +```python +usage: oi [-m MIN] [-M MAX] [--calls] [--puts] [--source {tr,yf}] [-h] +``` +* -m/--min: Min strike to plot. Defaults to .75 * current price +* -M/--max: Max strike to plot. Defaults to 1.25 * current price +* --calls: Flag to plot calls only +* --puts: Flag to plot puts only +* --source: Data source. Can be ['yf','tr']. Defaults to 'tr' for Tradier. + +![oi](https://user-images.githubusercontent.com/18151143/126855897-87eadd56-cca0-4eb0-8e55-6b7035762cbc.png) + +## vol +Plot volume. +```python +usage: vol [-m MIN] [-M MAX] [--calls] [--puts] [--source {tr,yf}] [-h] +``` +* -m/--min: Min strike to plot. Defaults to .75 * current price +* -M/--max: Max strike to plot. Defaults to 1.25 * current price +* --calls: Flag to plot calls only +* --puts: Flag to plot puts only +* --source: Data source. Can be ['yf','tr']. Defaults to 'tr' for Tradier. +![vol](https://user-images.githubusercontent.com/18151143/126855840-8fc3446d-081e-4961-9925-eb6c6e51c136.png) + +## voi + +```text +usage: voi [-v MIN_VOL] [-m MIN_SP] [-M MAX_SP] [--source] +``` + +Plots Volume + Open Interest of calls vs puts. + +* -v : Minimum volume (considering open interest) threshold of the plot. +* -m : Minimum strike price to consider in the plot. +* -M : Maximum strike price to consider in the plot. +* --source: Data source. Can be ['yf','tr']. Defaults to 'tr' for Tradier. + +![voi](https://user-images.githubusercontent.com/18151143/126855842-927ca7ff-78ca-46ce-9cc0-2a8dff4593af.png) + +## hist +Shows historical option chain +```python +usage: hist [-s STRIKE] [--put] [--chain CHAIN_ID] [--raw] [--export {csv,json,xlsx}] [-h] +``` +* -s/--strike: Strike to show history for +* --put: Flag to indicate the option is a put +* --chain: OCC Chain ID. Example: AAPL210730C00144000. This flag overwrites the strike and put option +* --raw: Show raw output +* --export: Export file. Can be {csv, json, xlsx}. + +Note that the chain ID can be obtained by using `chains -d symbol` + +![hist](https://user-images.githubusercontent.com/18151143/126855841-6884b19b-63c8-4746-a63f-82ad1a523c0c.png) -chains +## gr_hist +Plot historical option greeks. Data from ops.syncretism.io +```python +usage: gr_hist [-s STRIKE] [--put] [-g {iv,gamma,theta,vega,delta,rho,premium}] [--chain CHAIN_ID] [--raw] [--export {csv,json,xlsx}] [-h] +``` +* -s/--strike: Strike to show history for +* -g/--greek: Greek to get data for. Can be {iv,gamma,theta,vega,delta,rho}. Also can be premium +* --put: Flag to indicate the option is a put +* --chain: OCC Chain ID. Example: AAPL210730C00144000. This flag overwrites the strike and put option +* --raw: Show raw output (last 20 entries) +* --export: Export file. Can be {csv, json, xlsx}. -## info -This scrapes the options information from barchart.com/stocks overview. This includes information such as Historical Volatility and IV Rank. - -In order to run, the selenium webdriver must be installed. -Currently, this runs on either Chrome or Firefox. The path to the driver should be defined in [config_terminal.py](#config_terminal.py). - -Note this may take more time than other commands to process. - -```` -usage: info [-d DRIVER] -```` -* -d/--driver : One of {chrome, firefox}. This indicates which driver you have installed. - -opinfo +```python +(✨) (op)> gr_hist -s 145 --put -g delta +``` +![gr_hist](https://user-images.githubusercontent.com/18151143/126881002-c516d94b-5c87-43b1-9e16-dce304795e1c.png) diff --git a/gamestonk_terminal/options/__init__.py b/gamestonk_terminal/options/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gamestonk_terminal/options/barchart_view.py b/gamestonk_terminal/options/barchart_view.py index e3f366e86e5..dc003098c6d 100644 --- a/gamestonk_terminal/options/barchart_view.py +++ b/gamestonk_terminal/options/barchart_view.py @@ -3,20 +3,13 @@ __docformat__ = "numpy" import argparse from typing import List -from selenium import webdriver -from bs4 import BeautifulSoup -# from selenium.webdriver.chrome.options import Options as cOpts -# from selenium.webdriver.firefox.options import Options as fOpts +import requests +from bs4 import BeautifulSoup import pandas as pd from tabulate import tabulate -from gamestonk_terminal.config_terminal import ( - PATH_TO_SELENIUM_DRIVER as path_to_driver, - WEBDRIVER_TO_USE as web, -) -from gamestonk_terminal.helper_funcs import parse_known_args_and_warn -browsers = ["chrome", "firefox"] +from gamestonk_terminal.helper_funcs import parse_known_args_and_warn def print_options_data(stock: str, other_args: List[str]): @@ -29,9 +22,6 @@ def print_options_data(stock: str, other_args: List[str]): other_args: List[str] Other arguments. Currently just accepts a browser flag for selenium """ - if path_to_driver is None: - print("Please specify your selenium driver path in config_terminal.py", "\n") - return parser = argparse.ArgumentParser( add_help=False, @@ -39,38 +29,16 @@ def print_options_data(stock: str, other_args: List[str]): prog="info", description="Display option data [Source: Barchart.com]", ) - parser.add_argument( - "-b", - "--browser", - dest="browser", - help="selenium webdriver to use", - choices=browsers, - default=web, - ) try: ns_parser = parse_known_args_and_warn(parser, other_args) - if ns_parser.browser: - browser = ns_parser.browser - - if browser == "chrome": - # commenting this because it breaks when in usage - # the downside is that the browser will pop up to get the data - # options = cOpts() - # options.headless = True - driver = webdriver.Chrome(executable_path=path_to_driver) - - elif browser == "firefox": - # commenting this because it breaks when in usage - # the downside is that the browser will pop up to get the data - # options = fOpts() - # options.headless = True - driver = webdriver.Firefox(executable_path=path_to_driver) + if not ns_parser: + return page = f"https://www.barchart.com/stocks/quotes/{stock}/overview" - driver.get(page) - soup = BeautifulSoup(driver.page_source, "html.parser") + r = requests.get(page, headers={"User-Agent": "Mozilla/5.0"}) + soup = BeautifulSoup(r.text, "html.parser") tags = soup.find( "div", attrs={ @@ -80,7 +48,6 @@ def print_options_data(stock: str, other_args: List[str]): data = tags.find_all("li") labels = [] values = [] - print("") for row in data: labels.append(row.find_all("span")[0].getText()) values.append(row.find_all("span")[1].getText()) diff --git a/gamestonk_terminal/options/calculator_model.py b/gamestonk_terminal/options/calculator_model.py new file mode 100644 index 00000000000..6015cfe38ff --- /dev/null +++ b/gamestonk_terminal/options/calculator_model.py @@ -0,0 +1,143 @@ +"""Calculator Model""" +___docformat__ = "numpy" + +import argparse +from typing import List + +import numpy as np +from matplotlib import pyplot as plt + +from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale +from gamestonk_terminal import config_plot as cfp +from gamestonk_terminal import feature_flags as gtff + + +def pnl_calculator(other_args: List[str]): + """Plot profit/loss for different option variables + + Parameters + ---------- + other_args: List[str] + Argparse arguments + """ + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="calc", + description="Calculate profit or loss for given option settings.", + ) + + parser.add_argument( + "--put", + action="store_true", + default=False, + dest="put", + help="Flag to calculate put option", + ) + + parser.add_argument( + "--sell", + action="store_true", + default=False, + dest="sell", + help="Flag to get profit chart of selling contract", + ) + + parser.add_argument( + "-s", + "--strike", + type=float, + dest="strike", + help="Option strike price", + default=10, + ) + + parser.add_argument( + "-p", "--premium", type=float, dest="premium", help="Premium price", default=1 + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + + if not ns_parser: + return + + strike = ns_parser.strike + premium = ns_parser.premium + + price_at_expiry = np.linspace(ns_parser.strike / 2, 1.5 * ns_parser.strike, 301) + + sell = [1, -1][ns_parser.sell] + if ns_parser.put: + break_even = strike - sell * premium + pnl = strike - premium - price_at_expiry + pnl = sell * 100 * np.where(price_at_expiry < strike, pnl, -premium) + + else: + break_even = strike + sell * premium + pnl = price_at_expiry - strike - premium + pnl = sell * 100 * np.where(price_at_expiry > strike, pnl, -premium) + + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + ax.plot(price_at_expiry, pnl, alpha=0.1, c="k") + ax.fill_between( + price_at_expiry, 0, pnl, where=(pnl > 0), facecolor="green", alpha=0.5 + ) + ax.fill_between( + price_at_expiry, 0, pnl, where=(pnl < 0), facecolor="red", alpha=0.5 + ) + ax.axvline( + x=break_even, c="black", lw=3, alpha=0.6, label=f"Breakeven: ${break_even}" + ) + ax.axvline( + x=strike, c="dodgerblue", lw=3, alpha=0.6, label=f"Strike: ${strike}" + ) + if ns_parser.sell: + ax.axhline( + y=100 * premium, + c="seagreen", + lw=3, + alpha=0.6, + label=f"Max Profit: ${100*premium}", + ) + else: + ax.axhline( + y=-100 * premium, + c="firebrick", + lw=3, + alpha=0.6, + label=f"Max Loss: ${-100 * premium}", + ) + + ax.set_xlabel("Price at Expiry") + + ax.set_ylabel("Profit") + ax.set_title( + f"Profit for {['Buying','Selling'][ns_parser.sell]} {['Call', 'Put'][ns_parser.put]} option" + ) + ax.grid(True) + + if gtff.USE_ION: + plt.ion() + + plt.legend(loc=0) + fig.tight_layout(pad=1) + plt.show() + print(f"Strike: ${strike}") + print(f"Premium: ${premium}") + print(f"Breakeven price: ${break_even}") + if ns_parser.sell: + print(f"Max profit: ${100 * premium}") + print("Max loss: Unlimited") + else: + print("Max profit: Unlimited") + print(f"Max loss: ${-100*premium}") + print("") + + except Exception as e: + print(e, "\n") + return + + except SystemExit: + print("") + return diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index 90e5a46473d..f2fd9589560 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -5,18 +5,22 @@ import argparse import os from typing import List import matplotlib.pyplot as plt -import pandas as pd -import yfinance as yf from prompt_toolkit.completion import NestedCompleter -from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn +from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.options import ( - yahoo_view, - tradier_view, barchart_view, syncretism_view, + calculator_model, + op_helpers, + yfinance_view, + yfinance_model, + tradier_view, + tradier_model, ) + +from gamestonk_terminal.config_terminal import TRADIER_TOKEN from gamestonk_terminal.menu import session @@ -32,86 +36,39 @@ class OptionsController: "quit", "disp", "scr", - ] - - CHOICES_TICKER_DEPENDENT = [ - "exp", - "voi", - "vcalls", - "vputs", - "chains", + "calc", + "yf", + "tr", "info", + "load", + "exp", + "vol", + "voi", + "oi", + "hist", + "chains", + "gr_hist", ] - def __init__(self, ticker: str, stock: pd.DataFrame): + def __init__(self, ticker: str): """Construct data.""" - if ticker: - self.ticker = ticker - self.yf_ticker_data = yf.Ticker(self.ticker) - self.expiry_date = self.yf_ticker_data.options[0] - self.options = self.yf_ticker_data.option_chain(self.expiry_date) - self.last_adj_close_price = stock["Adj Close"].values[-1] - - self.op_parser = argparse.ArgumentParser(add_help=False, prog="op") - self.op_parser.add_argument( - "cmd", - choices=self.CHOICES + self.CHOICES_TICKER_DEPENDENT, - ) - else: - self.expiry_date = "" - self.op_parser = argparse.ArgumentParser(add_help=False, prog="op") - self.op_parser.add_argument( - "cmd", - choices=self.CHOICES, - ) - - def expiry_dates(self, other_args: List[str]): - """Print all available expiry dates.""" - parser = argparse.ArgumentParser( - add_help=False, - prog="exp", - description="""See/set expiry dates. [Source: Yahoo Finance]""", - ) - parser.add_argument( - "-d", - "--date", - dest="n_date", - action="store", - type=int, - default=-1, - choices=range(len(self.yf_ticker_data.options)), - help=f"Expiry date index for {self.ticker}.", - ) - - try: - if other_args: - if "-" not in other_args[0]: - other_args.insert(0, "-d") - - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - - # Print possible expiry dates - if ns_parser.n_date == -1: - print("\nAvailable expiry dates:") - for i, d in enumerate(self.yf_ticker_data.options): - print(f" {(2-len(str(i)))*' '}{i}. {d}") - - # It means an expiry date was correctly selected + self.ticker = ticker + if self.ticker: + if TRADIER_TOKEN == "REPLACE_ME": + self.expiry_dates = yfinance_model.option_expirations(self.ticker) else: - self.expiry_date = self.yf_ticker_data.options[ns_parser.n_date] - self.options = self.yf_ticker_data.option_chain(self.expiry_date) - print(f"\nSelected expiry date : {self.expiry_date}") + self.expiry_dates = tradier_model.option_expirations(self.ticker) + else: + self.expiry_dates = [] - except Exception as e: - print(e) + self.selected_date = "" + self.op_parser = argparse.ArgumentParser(add_help=False, prog="op") + self.op_parser.add_argument( + "cmd", + choices=self.CHOICES, + ) - print("") - return - - @staticmethod - def print_help(expiry_date): + def print_help(self): """Print help.""" print( "https://github.com/GamestonkTerminal/GamestonkTerminal/tree/main/gamestonk_terminal/options" @@ -125,18 +82,22 @@ class OptionsController: print(" disp display all preset screeners filters") print(" scr output screener options") print("") - if expiry_date: - print(f"Selected expiry date: {expiry_date}") - print("") - print(" exp see/set expiry date") - print(" voi volume + open interest options trading plot") - print(" vcalls calls volume + open interest plot") - print(" vputs puts volume + open interest plot") - print("") - print(" chains display option chains") - print( - " info display option information (volatility, IV rank etc)" - ) + print(f"Current Ticker: {self.ticker or None}") + print(" load load new ticker") + print(" info display option information (volatility, IV rank etc)") + print("") + print(" calc basic call/put PnL calculator") + print("") + print(f"Current Expiration: {self.selected_date or None}") + print(" exp see and set expiration dates") + print("") + if self.selected_date and self.ticker: + print(" chains display option chains with greeks [Tradier]]") + print(" oi plot open interest [Tradier/YF]") + print(" vol plot volume [Tradier/YF]") + print(" voi plot volume and open interest [Tradier/YF]") + print(" hist plot option history [Tradier") + print(" gr_hist plot option greek history [Syncretism]") print("") def switch(self, an_input: str): @@ -159,7 +120,7 @@ class OptionsController: # Help menu again if known_args.cmd == "?": - self.print_help(self.expiry_date) + self.print_help() return None # Clear screen @@ -173,7 +134,7 @@ class OptionsController: def call_help(self, _): """Process Help command.""" - self.print_help(self.expiry_date) + self.print_help() def call_q(self, _): """Process Q command - quit the menu.""" @@ -183,59 +144,114 @@ class OptionsController: """Process Quit command - quit the program.""" return True - def call_exp(self, other_args: List[str]): - """Process exp command.""" - self.expiry_dates(other_args) + def call_calc(self, other_args: List[str]): + """Process calc command""" + calculator_model.pnl_calculator(other_args) - def call_voi(self, other_args: List[str]): - """Process voi command.""" - yahoo_view.plot_volume_open_interest( - other_args, - self.ticker, - self.expiry_date, - self.last_adj_close_price, - self.options.calls, - self.options.puts, - ) - - def call_vcalls(self, other_args: List[str]): - """Process vcalls command.""" - yahoo_view.plot_calls_volume_open_interest( - other_args, - self.ticker, - self.expiry_date, - self.last_adj_close_price, - self.options.calls, - ) - - def call_vputs(self, other_args: List[str]): - """Process vcalls command.""" - yahoo_view.plot_puts_volume_open_interest( - other_args, - self.ticker, - self.expiry_date, - self.last_adj_close_price, - self.options.puts, - ) - - def call_chains(self, other_args): - tradier_view.display_chains(self.ticker, self.expiry_date, other_args) - - def call_info(self, other_args): + def call_info(self, other_args: List[str]): + """Process info command""" barchart_view.print_options_data(self.ticker, other_args) - def call_disp(self, other_args): + def call_disp(self, other_args: List[str]): + """Process disp command""" syncretism_view.view_available_presets(other_args) - def call_scr(self, other_args): + def call_scr(self, other_args: List[str]): + """Process scr command""" syncretism_view.screener_output(other_args) + def call_load(self, other_args: List[str]): + """Process load command""" + self.ticker = op_helpers.load(other_args) + if TRADIER_TOKEN == "REPLACE_ME": + self.expiry_dates = yfinance_model.option_expirations(self.ticker) + else: + self.expiry_dates = tradier_model.option_expirations(self.ticker) -def menu(ticker: str, stock: pd.DataFrame): + def call_exp(self, other_args: List[str]): + """Process exp command""" + if self.ticker: + self.selected_date = op_helpers.select_option_date( + self.expiry_dates, other_args + ) + else: + print("Please select a ticker using load {ticker}", "\n") + + def call_hist(self, other_args: List[str]): + if TRADIER_TOKEN != "REPLACE_ME": + tradier_view.display_historical(self.ticker, self.selected_date, other_args) + else: + print("TRADIER TOKEN not supplied. \n") + + def call_chains(self, other_args: List[str]): + """Process chains command""" + if TRADIER_TOKEN != "REPLACE_ME": + tradier_view.display_chains(self.ticker, self.selected_date, other_args) + else: + print("TRADIER TOKEN not supplied. \n") + + def call_vol(self, other_args: List[str]): + """Process vol command""" + if not self.ticker and not self.selected_date: + print("Ticker and expiration required.\n") + return + parsed = op_helpers.vol(other_args) + if not parsed: + return + if parsed.source == "tr" and TRADIER_TOKEN != "REPLACE_ME": + options = tradier_model.get_option_chains(self.ticker, self.selected_date) + tradier_view.plot_vol(options, self.ticker, self.selected_date, parsed) + else: + options = yfinance_model.get_option_chain(self.ticker, self.selected_date) + yfinance_view.plot_vol( + options.calls, options.puts, self.ticker, self.selected_date, parsed + ) + + def call_voi(self, other_args: List[str]): + """Process voi command""" + if not self.ticker and not self.selected_date: + print("Ticker and expiration required.") + return + parsed = op_helpers.voi(other_args) + if not parsed: + return + if parsed.source == "tr" and TRADIER_TOKEN != "REPLACE_ME": + options = tradier_model.get_option_chains(self.ticker, self.selected_date) + tradier_view.plot_volume_open_interest( + self.ticker, self.selected_date, options, parsed + ) + else: + options = yfinance_model.get_option_chain(self.ticker, self.selected_date) + yfinance_view.plot_volume_open_interest( + self.ticker, self.selected_date, options.calls, options.puts, parsed + ) + + def call_oi(self, other_args: List[str]): + """Process oi command""" + if not self.ticker and not self.selected_date: + print("Ticker and expiration required.") + return + parsed = op_helpers.oi(other_args) + if not parsed: + return + if parsed.source == "tr" and TRADIER_TOKEN != "REPLACE_ME": + options = tradier_model.get_option_chains(self.ticker, self.selected_date) + tradier_view.plot_oi(options, self.ticker, self.selected_date, parsed) + else: + options = yfinance_model.get_option_chain(self.ticker, self.selected_date) + yfinance_view.plot_oi( + options.calls, options.puts, self.ticker, self.selected_date, parsed + ) + + def call_gr_hist(self, other_args: List[str]): + syncretism_view.historical_greeks(self.ticker, self.selected_date, other_args) + + +def menu(ticker: str): """Options Menu.""" try: - op_controller = OptionsController(ticker, stock) + op_controller = OptionsController(ticker) op_controller.call_help(None) except IndexError: print("No options found for " + ticker) diff --git a/gamestonk_terminal/options/op_helpers.py b/gamestonk_terminal/options/op_helpers.py new file mode 100644 index 00000000000..f701fae7234 --- /dev/null +++ b/gamestonk_terminal/options/op_helpers.py @@ -0,0 +1,412 @@ +"""Option helper functions""" +__docformat__ = "numpy" + +import argparse +from typing import List + +import pandas as pd +import numpy as np + +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + check_non_negative, +) + +# pylint: disable=R1710 + + +def load(other_args: List[str]) -> str: + """Load ticker into object + + Parameters + ---------- + other_args: List[str] + Agrparse arguments + + Returns + ------- + str: + Ticker + """ + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="opload", + description="Load a ticker into option menu", + ) + + parser.add_argument( + "-t", + "--ticker", + action="store", + dest="ticker", + required="-h" not in other_args, + help="Stock ticker", + ) + + try: + if other_args: + if "-t" not in other_args and "-h" not in other_args: + other_args.insert(0, "-t") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return "" + print("") + return ns_parser.ticker + except Exception as e: + print(e, "\n") + return "" + except SystemExit: + print("") + return "" + + +# pylint: disable=no-else-return + + +def select_option_date(avalaiable_dates: List[str], other_args: List[str]) -> str: + """Select an option date out of a supplied list + + Parameters + ---------- + avalaiable_dates: List[str] + Possible date options + other_args: List[str] + Arparse arguments + Returns + ------- + expiry_date: str + Selected expiry date + """ + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="exp", + description="See and set expiration date", + ) + parser.add_argument( + "-d", + "--date", + dest="n_date", + action="store", + type=int, + default=-1, + choices=range(len(avalaiable_dates)), + help="Select index for expiry date.", + ) + + parser.add_argument( + "-D", + dest="date", + type=str, + choices=avalaiable_dates + [""], + help="Select date (YYYY-MM-DD)", + default="", + ) + + try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-d") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return "" + + # Print possible expiry dates + if ns_parser.n_date == -1 and not ns_parser.date: + print("\nAvailable expiry dates:") + for i, d in enumerate(avalaiable_dates): + print(f" {(2 - len(str(i))) * ' '}{i}. {d}") + print("") + return "" + + # It means an expiry date was correctly selected + else: + if ns_parser.date: + if ns_parser.date in avalaiable_dates: + print(f"Expiraration set to {ns_parser.date} \n") + return ns_parser.date + else: + print("Expiration not an option") + return "" + else: + expiry_date = avalaiable_dates[ns_parser.n_date] + print(f"Expiraration set to {expiry_date} \n") + return expiry_date + + except Exception as e: + print(e, "\n") + return "" + + +def get_loss_at_strike(strike: float, chain: pd.DataFrame) -> float: + """Function to get the loss at the given expiry + + Parameters + ---------- + strike: Union[int,float] + Value to calculate total loss at + chain: Dataframe: + Dataframe containing at least strike and openInterest + + Returns + ------- + loss: Union[float,int] + Total loss + """ + + itm_calls = chain[chain.index < strike][["OI_call"]] + itm_calls["loss"] = (strike - itm_calls.index) * itm_calls["OI_call"] + call_loss = itm_calls["loss"].sum() + + itm_puts = chain[chain.index > strike][["OI_put"]] + itm_puts["loss"] = (itm_puts.index - strike) * itm_puts["OI_put"] + put_loss = itm_puts.loss.sum() + loss = call_loss + put_loss + + return loss + + +def calculate_max_pain(chain: pd.DataFrame) -> int: + """Returns the max pain for a given call/put dataframe + + Parameters + ---------- + chain: DataFrame + Dataframe to calculate value from + + Returns + ------- + max_pain : int + Max pain value + """ + + strikes = np.array(chain.index) + if ("OI_call" not in chain.columns) or ("OI_put" not in chain.columns): + print("Incorrect columns. Unable to parse max pain") + return np.nan + + loss = [] + for price_at_exp in strikes: + loss.append(get_loss_at_strike(price_at_exp, chain)) + + chain["loss"] = loss + max_pain = chain["loss"].idxmin() + + return max_pain + + +def vol(other_args: List[str]): + """Parse volume argparse + + Parameters + ---------- + other_args: List[str] + Argparse arguments + + Returns + ------- + ns_parser: argparse.Namespace + Parsed namespace + """ + + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="vol", + description="Plot volume. Volume refers to the number of contracts traded today.", + ) + + parser.add_argument( + "-m", + "--min", + default=-1, + type=check_non_negative, + help="Min strike to plot", + dest="min", + ) + parser.add_argument( + "-M", + "--max", + default=-1, + type=check_non_negative, + help="Max strike to plot", + dest="max", + ) + + parser.add_argument( + "--calls", + action="store_true", + default=False, + dest="calls", + help="Flag to plot call options only", + ) + + parser.add_argument( + "--puts", + action="store_true", + default=False, + dest="puts", + help="Flag to plot put options only", + ) + + parser.add_argument( + "--source", + type=str, + default="tr", + choices=["tr", "yf"], + dest="source", + help="Source to get data from", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + return ns_parser + + except Exception as e: + print(e, "\n") + + +def voi(other_args: List[str]): + """Parse Volume + open interest argparse + + Parameters + ---------- + other_args: List[str] + Argparse arguments + + Returns + ------- + ns_parser: argparse.Namespace + Parsed namespace + """ + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="voi", + description=""" + Plots Volume + Open Interest of calls vs puts. + """, + ) + parser.add_argument( + "-v", + "--minv", + dest="min_vol", + type=check_non_negative, + default=-1, + help="minimum volume (considering open interest) threshold of the plot.", + ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider in the plot.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider in the plot.", + ) + parser.add_argument( + "--source", + type=str, + default="tr", + choices=["tr", "yf"], + dest="source", + help="Source to get data from", + ) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + return ns_parser + + except Exception as e: + print(e, "\n") + return None + + +def oi(other_args: List[str]): + """Parse Open Interest argparse + + Parameters + ---------- + other_args: List[str] + Argparse arguments + + Returns + ------- + ns_parser: argparse.Namespace + Parsed namespace + """ + + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="oi", + description="Plot open interest. Open interest represents the number of contracts that exist.", + ) + + parser.add_argument( + "-m", + "--min", + default=-1, + type=check_non_negative, + help="Min strike to plot", + dest="min", + ) + parser.add_argument( + "-M", + "--max", + default=-1, + type=check_non_negative, + help="Max strike to plot", + dest="max", + ) + + parser.add_argument( + "--calls", + action="store_true", + default=False, + dest="calls", + help="Flag to plot call options only", + ) + + parser.add_argument( + "--puts", + action="store_true", + default=False, + dest="puts", + help="Flag to plot put options only", + ) + parser.add_argument( + "--source", + type=str, + default="tr", + choices=["tr", "yf"], + dest="source", + help="Source to get data from", + ) + + try: + + ns_parser = parse_known_args_and_warn(parser, other_args) + + if not ns_parser: + return None + + return ns_parser + + except Exception as e: + print(e, "\n") + return None diff --git a/gamestonk_terminal/options/syncretism_view.py b/gamestonk_terminal/options/syncretism_view.py index ccc9df5e09b..9a515e63653 100644 --- a/gamestonk_terminal/options/syncretism_view.py +++ b/gamestonk_terminal/options/syncretism_view.py @@ -5,10 +5,20 @@ import argparse import os from typing import List import configparser + import requests import pandas as pd +import matplotlib.pyplot as plt from tabulate import tabulate -from gamestonk_terminal.helper_funcs import parse_known_args_and_warn + +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + export_data, + plot_autoscale, +) +from gamestonk_terminal.options import yfinance_model +from gamestonk_terminal import config_plot as cfp +from gamestonk_terminal import feature_flags as gtff presets_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets/") @@ -191,3 +201,197 @@ PC: Price Change; PB: Price-to-book. """, except Exception as e: print(e, "\n") + + +possible_greeks = [ + "iv", + "gamma", + "theta", + "vega", + "delta", + "rho", + "premium", +] + + +def check_valid_option_greek_header(headers: str) -> List[str]: + """Check valid greek selection + + Parameters + ---------- + headers : str + Option chains headers + + Returns + ---------- + List[str] + List of columns string + """ + columns = [str(item) for item in headers.split(",")] + + for header in columns: + if header not in possible_greeks: + raise argparse.ArgumentTypeError("Invalid option chains header selected!") + + return columns + + +def historical_greeks(ticker: str, expiry: str, other_args: List[str]): + """Get historical greeks + + Parameters + ---------- + ticker: str + Ticker + expiry: str + Expiration date + other_args: List[str] + Argparse arguments + """ + + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="gr_hist", + description="Plot historical option greeks.", + ) + + parser.add_argument( + "-s", + "--strike", + dest="strike", + type=float, + required="--chain" not in other_args or "-h" not in other_args, + help="Strike price to look at", + ) + parser.add_argument( + "--put", + dest="put", + action="store_true", + default=False, + help="Flag for showing put option", + ) + + parser.add_argument( + "-g", + "--greek", + dest="greek", + type=str, + choices=possible_greeks, + default="delta", + help="Greek column to select", + ) + + parser.add_argument("--chain", dest="chain_id", type=str, help="OCC option symbol") + + parser.add_argument( + "--raw", dest="raw", action="store_true", default=False, help="Display raw data" + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + if not ns_parser.chain_id: + options = yfinance_model.get_option_chain(ticker, expiry) + + if ns_parser.put: + options = options.puts + else: + options = options.calls + + chain_id = options.loc[ + options.strike == ns_parser.strike, "contractSymbol" + ].values[0] + else: + chain_id = ns_parser.chain_id + + r = requests.get(f"https://api.syncretism.io/ops/historical/{chain_id}") + + if r.status_code != 200: + print("Error in request.") + return + + history = r.json() + + iv, delta, gamma, theta, rho, vega, premium, price, time = ( + [], + [], + [], + [], + [], + [], + [], + [], + [], + ) + + for entry in history: + + time.append(pd.to_datetime(entry["timestamp"], unit="s")) + iv.append(entry["impliedVolatility"]) + gamma.append(entry["gamma"]) + delta.append(entry["delta"]) + theta.append(entry["theta"]) + rho.append(entry["rho"]) + vega.append(entry["vega"]) + premium.append(entry["premium"]) + price.append(entry["regularMarketPrice"]) + + data = { + "iv": iv, + "gamma": gamma, + "delta": delta, + "theta": theta, + "rho": rho, + "vega": vega, + "premium": premium, + "price": price, + } + + df = pd.DataFrame(data, index=time) + + if ns_parser.raw: + print(df.tail(20)) + if ns_parser.export: + export_data( + ns_parser.export, + os.path.dirname(os.path.abspath(__file__)), + f"historical_greek_{ticker}_{expiry}_{ns_parser.greek}_{str(ns_parser.strike).replace('.', 'p')}" + f"_{['Call','Put'][ns_parser.put]}", + df, + ) + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + im1 = ax.plot(time, df[ns_parser.greek], c="firebrick", label=ns_parser.greek) + ax.set_ylabel(ns_parser.greek) + ax1 = ax.twinx() + im2 = ax1.plot(time, price, c="dodgerblue", label="Stock Price") + ax1.set_ylabel(f"{ticker} Price") + ax1.set_xlabel("Date") + ax.grid("on") + ax.set_title( + f"{ns_parser.greek} historical for {ticker.upper()} {ns_parser.strike} {['Call','Put'][ns_parser.put]}" + ) + plt.gcf().autofmt_xdate() + + if gtff.USE_ION: + plt.ion() + + ims = im1 + im2 + labels = [lab.get_label() for lab in ims] + plt.legend(ims, labels, loc=0) + fig.tight_layout(pad=1) + plt.show() + print("") + + except Exception as e: + print(e, "\n") diff --git a/gamestonk_terminal/options/tradier_model.py b/gamestonk_terminal/options/tradier_model.py new file mode 100644 index 00000000000..036b3bbef38 --- /dev/null +++ b/gamestonk_terminal/options/tradier_model.py @@ -0,0 +1,188 @@ +"""Tradier options model""" +__docformat__ = "numpy" + +import requests +import pandas as pd + +from gamestonk_terminal import config_terminal as cfg + +option_columns = [ + "symbol", + "bid", + "ask", + "strike", + "bidsize", + "asksize", + "volume", + "open_interest", + "option_type", +] +greek_columns = ["delta", "gamma", "theta", "vega", "ask_iv", "bid_iv", "mid_iv"] +df_columns = option_columns + greek_columns + +default_columns = [ + "mid_iv", + "vega", + "delta", + "gamma", + "theta", + "volume", + "open_interest", + "bid", + "ask", +] + +# pylint: disable=no-else-return + + +def option_expirations(ticker: str): + """Get available expiration dates for given ticker + + Parameters + ---------- + ticker: str + Ticker to get expirations for + + Returns + ------- + dates: List[str] + List of of available expirations + """ + r = requests.get( + "https://sandbox.tradier.com/v1/markets/options/expirations", + params={"symbol": ticker, "includeAllRoots": "true", "strikes": "false"}, + headers={ + "Authorization": f"Bearer {cfg.TRADIER_TOKEN}", + "Accept": "application/json", + }, + ) + if r.status_code == 200: + dates = r.json()["expirations"]["date"] + return dates + else: + print("Tradier request failed. Check token. \n") + return [] + + +def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame: + """Display option chains [Source: Tradier]" + + Parameters + ---------- + symbol : str + Ticker to get options for + expiry : str + Expiration date in the form of "YYYY-MM-DD" + + Returns + ------- + chains: pd.DataFrame + Dataframe with options for the given Symbol and Expiration date + """ + params = {"symbol": symbol, "expiration": expiry, "greeks": "true"} + + headers = { + "Authorization": f"Bearer {cfg.TRADIER_TOKEN}", + "Accept": "application/json", + } + + response = requests.get( + "https://sandbox.tradier.com/v1/markets/options/chains", + params=params, + headers=headers, + ) + if response.status_code != 200: + print("Error in request. Check TRADIER_TOKEN\n") + return pd.DataFrame() + + chains = process_chains(response) + return chains + + +def process_chains(response: requests.models.Response) -> pd.DataFrame: + """Function to take in the requests.get and return a DataFrame + + Parameters + ---------- + response: requests.models.Response + This is the response from tradier api. + + Returns + ------- + opt_chain: pd.DataFrame + Dataframe with all available options + """ + json_response = response.json() + options = json_response["options"]["option"] + + opt_chain = pd.DataFrame(columns=df_columns) + for idx, option in enumerate(options): + data = [option[col] for col in option_columns] + data += [option["greeks"][col] for col in greek_columns] + opt_chain.loc[idx, :] = data + + return opt_chain + + +def last_price(ticker: str): + """Makes api request for last price + + Parameters + ---------- + ticker: str + Ticker + + Returns + ------- + float: + Last price + """ + r = requests.get( + "https://sandbox.tradier.com/v1/markets/quotes", + params={"symbols": ticker, "includeAllRoots": "true", "strikes": "false"}, + headers={ + "Authorization": f"Bearer {cfg.TRADIER_TOKEN}", + "Accept": "application/json", + }, + ) + if r.status_code == 200: + return float(r.json()["quotes"]["quote"]["last"]) + else: + print("Error getting last price") + return None + + +def historical_prices(symbol: str) -> pd.DataFrame: + """Get historical options prices + + Parameters + ---------- + symbol: str + OCC option chain symbol + + Returns + ------- + df_hist: pd.DataFrame + Dataframe of historical options + """ + response = requests.get( + "https://sandbox.tradier.com/v1/markets/history", + params={"symbol": {symbol}, "interval": "daily"}, + headers={ + "Authorization": f"Bearer {cfg.TRADIER_TOKEN}", + "Accept": "application/json", + }, + ) + + if response.status_code != 200: + print("Error with request") + return pd.DataFrame() + + data = response.json()["history"] + if not data: + print("No historical data available") + return pd.DataFrame() + + df_hist = pd.DataFrame(data["day"]).set_index("date") + + return df_hist diff --git a/gamestonk_terminal/options/tradier_view.py b/gamestonk_terminal/options/tradier_view.py index 6aeee175cd4..4043b2bd43f 100644 --- a/gamestonk_terminal/options/tradier_view.py +++ b/gamestonk_terminal/options/tradier_view.py @@ -1,43 +1,63 @@ -"""Helper functions for the options menu""" +"""Tradier options view""" __docformat__ = "numpy" import argparse from typing import List -import requests -import pandas as pd -import numpy as np -from tabulate import tabulate -from gamestonk_terminal.config_terminal import TRADIER_TOKEN -from gamestonk_terminal.helper_funcs import ( - check_non_negative, - parse_known_args_and_warn, -) +from bisect import bisect_left +import os + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import mplfinance as mpf +from colorama import Fore, Style +from tabulate import tabulate + +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + check_non_negative, + export_data, + plot_autoscale, + patch_pandas_text_adjustment, +) +from gamestonk_terminal.options import op_helpers, tradier_model +from gamestonk_terminal import config_plot as cfp +from gamestonk_terminal import feature_flags as gtff -option_columns = [ - "bid", - "ask", - "strike", - "bidsize", - "asksize", - "volume", - "open_interest", - "option_type", -] -greek_columns = ["delta", "gamma", "theta", "vega", "ask_iv", "bid_iv", "mid_iv"] -df_columns = option_columns + greek_columns column_map = {"mid_iv": "iv", "open_interest": "oi", "volume": "vol"} -default_columns = [ - "mid_iv", - "vega", - "delta", - "gamma", - "theta", - "volume", - "open_interest", - "bid", - "ask", -] + +def red_highlight(val): + """Red highlight + + Parameters + ---------- + val + dataframe values to color + + Returns + ---------- + str + colored dataframes values + """ + return f"{Fore.RED}{val}{Style.RESET_ALL}" + + +def green_highlight(val): + """Green highlight + + Parameters + ---------- + values : List[str] + dataframe values to color + + Returns + ---------- + List[str] + colored dataframes values + """ + return f"{Fore.GREEN}{val}{Style.RESET_ALL}" def check_valid_option_chains_headers(headers: str) -> List[str]: @@ -56,87 +76,29 @@ def check_valid_option_chains_headers(headers: str) -> List[str]: columns = [str(item) for item in headers.split(",")] for header in columns: - if header not in df_columns: + if header not in tradier_model.df_columns: raise argparse.ArgumentTypeError("Invalid option chains header selected!") return columns -def process_chains(response: requests.models.Response) -> pd.DataFrame: - """Function to take in the requests.get and return a DataFrame +def display_chains(ticker: str, expiry: str, other_args: List[str]): + """Display option chain Parameters ---------- - response: requests.models.Response - This is the response from tradier api. - - Returns - ------- - opt_chain: pd.DataFrame - Dataframe with all available options - """ - json_response = response.json() - options = json_response["options"]["option"] - - opt_chain = pd.DataFrame(columns=df_columns) - for idx, option in enumerate(options): - data = [option[col] for col in option_columns] - data += [option["greeks"][col] for col in greek_columns] - opt_chain.loc[idx, :] = data - - return opt_chain - - -def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame: - """Display option chains [Source: Tradier]" - - Parameters - ---------- - symbol : str - Ticker to get options for - expiry : str - Expiration date in the form of "YYYY-MM-DD" - - Returns - ------- - pd.DataFrame - Dataframe with options for the gievn Symbol and Expiration date - """ - params = {"symbol": symbol, "expiration": expiry, "greeks": "true"} - - headers = {"Authorization": f"Bearer {TRADIER_TOKEN}", "Accept": "application/json"} - - response = requests.get( - "https://sandbox.tradier.com/v1/markets/options/chains", - params=params, - headers=headers, - ) - if response.status_code != 200: - print("Error in request. Check TRADIER_TOKEN\n") - return pd.DataFrame() - - chains = process_chains(response) - return chains - - -def display_chains(symbol: str, expiry: str, other_args: List[str]): - """Check valid option chains headers - - Parameters - ---------- - headers : str - Option chains headers - - Returns - ---------- - List[str] - List of columns string + ticker: str + Stock ticker + expiry: str + Expiration date of option + other_args: List[str] + Argparse arguments """ parser = argparse.ArgumentParser( prog="chains", add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, - description="Display option chains [Source: Tradier]", + description="Display option chains", ) parser.add_argument( "--calls", @@ -172,22 +134,25 @@ def display_chains(symbol: str, expiry: str, other_args: List[str]): "-d", "--display", dest="to_display", - default=default_columns, + default=tradier_model.default_columns, type=check_valid_option_chains_headers, help="columns to look at. Columns can be: {bid, ask, strike, bidsize, asksize, volume, open_interest, delta, " "gamma, theta, vega, ask_iv, bid_iv, mid_iv} ", ) + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - chains_df = get_option_chains(symbol, expiry) - - if chains_df.empty: - return - + chains_df = tradier_model.get_option_chains(ticker, expiry) columns = ns_parser.to_display + ["strike", "option_type"] chains_df = chains_df[columns].rename(columns=column_map) @@ -205,6 +170,16 @@ def display_chains(symbol: str, expiry: str, other_args: List[str]): chains_df = chains_df[chains_df["strike"] >= min_strike] chains_df = chains_df[chains_df["strike"] <= max_strike] + + if ns_parser.export: + # Note the extra dirname needed due to the subfolder in options + export_data( + ns_parser.export, + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + f"chains_{ticker}_{expiry}", + chains_df, + ) + calls_df = chains_df[chains_df.option_type == "call"].drop( columns=["option_type"] ) @@ -238,6 +213,16 @@ def display_chains(symbol: str, expiry: str, other_args: List[str]): puts_df = puts_df[puts_df.columns[::-1]] chain_table = calls_df.merge(puts_df, on="strike") + if gtff.USE_COLOR: + call_cols = [col for col in chain_table if col.endswith("_x")] + put_cols = [col for col in chain_table if col.endswith("_y")] + patch_pandas_text_adjustment() + pd.set_option("display.max_colwidth", 0) + pd.set_option("display.max_rows", None) + for cc in call_cols: + chain_table[cc] = chain_table[cc].astype(str).apply(green_highlight) + for pc in put_cols: + chain_table[pc] = chain_table[pc].astype(str).apply(red_highlight) headers = [ col.strip("_x") if col.endswith("_x") @@ -255,7 +240,466 @@ def display_chains(symbol: str, expiry: str, other_args: List[str]): floatfmt=".2f", ) ) - print("") + print("") except Exception as e: print(e, "\n") + + +def plot_oi( + options: pd.DataFrame, + ticker: str, + expiry: str, + ns_parser: argparse.Namespace, +): + """Plot open interest + + Parameters + ---------- + options: pd.DataFrame + Options dataframe with both calls and puts + ticker: str + Ticker + expiry: str + Expiry date for options + ns_parser: argparse.Namespace + Parsed namespace + """ + + current_price = tradier_model.last_price(ticker) + + if ns_parser.min == -1: + min_strike = 0.75 * current_price + else: + min_strike = ns_parser.min + + if ns_parser.max == -1: + max_strike = 1.25 * current_price + else: + max_strike = ns_parser.max + + if ns_parser.calls and ns_parser.puts: + print("Both flags selected, please select one", "\n") + return + + calls = options[options.option_type == "call"][["strike", "open_interest"]] + puts = options[options.option_type == "put"][["strike", "open_interest"]] + call_oi = calls.set_index("strike")["open_interest"] / 1000 + put_oi = puts.set_index("strike")["open_interest"] / 1000 + + df_opt = pd.merge(call_oi, put_oi, left_index=True, right_index=True) + df_opt = df_opt.rename( + columns={"open_interest_x": "OI_call", "open_interest_y": "OI_put"} + ) + + max_pain = op_helpers.calculate_max_pain(df_opt) + plt.style.use("classic") + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + + if not ns_parser.calls: + put_oi.plot( + x="strike", + y="open_interest", + label="Puts", + ax=ax, + marker="o", + ls="-", + c="r", + ) + if not ns_parser.puts: + call_oi.plot( + x="strike", + y="open_interest", + label="Calls", + ax=ax, + marker="o", + ls="-", + c="g", + ) + ax.axvline( + current_price, lw=2, c="k", ls="--", label="Current Price", alpha=0.7 + ) + ax.axvline(max_pain, lw=3, c="k", label=f"Max Pain: {max_pain}", alpha=0.7) + ax.grid("on") + ax.set_xlabel("Strike Price") + ax.set_ylabel("Open Interest (1k) ") + ax.set_xlim(min_strike, max_strike) + + if gtff.USE_ION: + plt.ion() + + ax.set_title(f"Open Interest for {ticker.upper()} expiring {expiry}") + plt.legend(loc=0) + fig.tight_layout(pad=1) + + plt.show() + plt.style.use("default") + print("") + + +def plot_vol( + options: pd.DataFrame, + ticker: str, + expiry: str, + ns_parser: argparse.Namespace, +): + """Plot volume + + Parameters + ---------- + options: pd.DataFrame + Options dataframe with both calls and puts + ticker: str + Ticker + expiry: str + Expiry date for options + ns_parser: argparse.Namespace + Parsed namespace + """ + + current_price = tradier_model.last_price(ticker) + + if ns_parser.min == -1: + min_strike = 0.75 * current_price + else: + min_strike = ns_parser.min + + if ns_parser.max == -1: + max_strike = 1.25 * current_price + else: + max_strike = ns_parser.max + + if ns_parser.calls and ns_parser.puts: + print("Both flags selected, please select one", "\n") + return + + calls = options[options.option_type == "call"][["strike", "volume"]] + puts = options[options.option_type == "put"][["strike", "volume"]] + call_v = calls.set_index("strike")["volume"] / 1000 + put_v = puts.set_index("strike")["volume"] / 1000 + plt.style.use("classic") + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + + if not ns_parser.calls: + put_v.plot( + x="strike", + y="volume", + label="Puts", + ax=ax, + marker="o", + ls="-", + c="r", + ) + if not ns_parser.puts: + call_v.plot( + x="strike", + y="volume", + label="Calls", + ax=ax, + marker="o", + ls="-", + c="g", + ) + ax.axvline(current_price, lw=2, c="k", ls="--", label="Current Price", alpha=0.7) + ax.grid("on") + ax.set_xlabel("Strike Price") + ax.set_ylabel("Volume (1k) ") + ax.set_xlim(min_strike, max_strike) + + if gtff.USE_ION: + plt.ion() + + ax.set_title(f"Volume for {ticker.upper()} expiring {expiry}") + plt.legend(loc=0) + fig.tight_layout(pad=1) + + plt.show() + plt.style.use("default") + print("") + + +def plot_volume_open_interest( + ticker: str, exp_date: str, options: pd.DataFrame, ns_parser: argparse.Namespace +): + """Plot volume and open interest + + Parameters + ---------- + ticker : str + Main ticker to compare income + exp_date : str + Expiry date of the option + Options: pd.DataFrame + Options dataframe with both calls and puts + ns_parser: argparse.Namespace + Parsed namespace + """ + + current_price = tradier_model.last_price(ticker) + calls = options[options.option_type == "call"][ + ["strike", "volume", "open_interest"] + ] + puts = options[options.option_type == "put"][["strike", "volume", "open_interest"]] + + # Process Calls Data + df_calls = calls.pivot_table( + index="strike", values=["volume", "open_interest"], aggfunc="sum" + ).reindex() + df_calls["strike"] = df_calls.index + df_calls["type"] = "calls" + df_calls["open_interest"] = df_calls["open_interest"] + df_calls["volume"] = df_calls["volume"] + df_calls["oi+v"] = df_calls["open_interest"] + df_calls["volume"] + df_calls["spot"] = round(current_price, 2) + + df_puts = puts.pivot_table( + index="strike", values=["volume", "open_interest"], aggfunc="sum" + ).reindex() + df_puts["strike"] = df_puts.index + df_puts["type"] = "puts" + df_puts["open_interest"] = df_puts["open_interest"] + df_puts["volume"] = -df_puts["volume"] + df_puts["open_interest"] = -df_puts["open_interest"] + df_puts["oi+v"] = df_puts["open_interest"] + df_puts["volume"] + df_puts["spot"] = round(current_price, 2) + + call_oi = calls.set_index("strike")["open_interest"] / 1000 + put_oi = puts.set_index("strike")["open_interest"] / 1000 + + df_opt = pd.merge(call_oi, put_oi, left_index=True, right_index=True) + df_opt = df_opt.rename( + columns={"open_interest_x": "OI_call", "open_interest_y": "OI_put"} + ) + + max_pain = op_helpers.calculate_max_pain(df_opt) + + if ns_parser.min_vol == -1 and ns_parser.min_sp == -1 and ns_parser.max_sp == -1: + # If no argument provided, we use the percentile 50 to get 50% of upper volume data + volume_percentile_threshold = 50 + min_vol_calls = np.percentile(df_calls["oi+v"], volume_percentile_threshold) + min_vol_puts = np.percentile(df_puts["oi+v"], volume_percentile_threshold) + + df_calls = df_calls[df_calls["oi+v"] > min_vol_calls] + df_puts = df_puts[df_puts["oi+v"] < min_vol_puts] + + else: + if ns_parser.min_vol > -1: + df_calls = df_calls[df_calls["oi+v"] > ns_parser.min_vol] + df_puts = df_puts[df_puts["oi+v"] < -ns_parser.min_vol] + + if ns_parser.min_sp > -1: + df_calls = df_calls[df_calls["strike"] > ns_parser.min_sp] + df_puts = df_puts[df_puts["strike"] > ns_parser.min_sp] + + if ns_parser.max_sp > -1: + df_calls = df_calls[df_calls["strike"] < ns_parser.max_sp] + df_puts = df_puts[df_puts["strike"] < ns_parser.max_sp] + + if df_calls.empty and df_puts.empty: + print( + "The filtering applied is too strong, there is no data available for such conditions.\n" + ) + return + + # Initialize the matplotlib figure + _, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + + # make x axis symmetric + axis_origin = max(abs(max(df_puts["oi+v"])), abs(max(df_calls["oi+v"]))) + ax.set_xlim(-axis_origin, +axis_origin) + + sns.set_style(style="darkgrid") + + g = sns.barplot( + x="oi+v", + y="strike", + data=df_calls, + label="Calls: Open Interest", + color="lightgreen", + orient="h", + ) + + g = sns.barplot( + x="volume", + y="strike", + data=df_calls, + label="Calls: Volume", + color="green", + orient="h", + ) + + g = sns.barplot( + x="oi+v", + y="strike", + data=df_puts, + label="Puts: Open Interest", + color="pink", + orient="h", + ) + + g = sns.barplot( + x="volume", + y="strike", + data=df_puts, + label="Puts: Volume", + color="red", + orient="h", + ) + + # draw spot line + s = [float(strike.get_text()) for strike in ax.get_yticklabels()] + spot_index = bisect_left(s, current_price) # find where the spot is on the graph + spot_line = ax.axhline(spot_index, ls="--", color="dodgerblue", alpha=0.3) + + # draw max pain line + max_pain_index = bisect_left(s, max_pain) + max_pain_line = ax.axhline(max_pain_index, ls="-", color="black", alpha=0.3) + max_pain_line.set_linewidth(3) + + # format ticklabels without - for puts + g.set_xticks(g.get_xticks()) + xlabels = [f"{x:,.0f}".replace("-", "") for x in g.get_xticks()] + g.set_xticklabels(xlabels) + + plt.title( + f"{ticker} volumes for {exp_date} (open interest displayed only during market hours)" + ) + ax.invert_yaxis() + + _ = ax.legend() + handles, _ = ax.get_legend_handles_labels() + handles.append(spot_line) + handles.append(max_pain_line) + + # create legend labels + add to graph + labels = [ + "Calls open interest", + "Calls volume ", + "Puts open interest", + "Puts volume", + "Current stock price", + f"Max pain = {max_pain}", + ] + + plt.legend(handles=handles[:], labels=labels) + sns.despine(left=True, bottom=True) + + if gtff.USE_ION: + plt.ion() + plt.show() + plt.style.use("default") + print("") + + +def display_historical(ticker: str, expiry: str, other_args: List[str]): + """Plot historical option data + + Parameters + ---------- + ticker: str + Ticker + expiry: str + Expiration of option + other_args: List[str] + Argparse arguments + """ + + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="hist", + description="Gets historical quotes for given option chain", + ) + parser.add_argument( + "-s", + "--strike", + dest="strike", + type=float, + required="--chain" not in other_args or "-h" not in other_args, + help="Strike price to look at", + ) + parser.add_argument( + "--put", + dest="put", + action="store_true", + default=False, + help="Flag for showing put option", + ) + parser.add_argument("--chain", dest="chain_id", type=str, help="OCC option symbol") + + parser.add_argument( + "--raw", dest="raw", action="store_true", default=False, help="Display raw data" + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + if not ns_parser.chain_id: + strike = float(ns_parser.strike) + op_type = ["call", "put"][ns_parser.put] + chain = tradier_model.get_option_chains(ticker, expiry) + try: + symbol = chain[ + (chain.strike == strike) & (chain.option_type == op_type) + ]["symbol"].values[0] + except IndexError: + print(f"Strike: {strike}, Option type: {op_type} not not found \n") + return + else: + symbol = ns_parser.chain_id + + df_hist = tradier_model.historical_prices(symbol) + df_hist.index = pd.DatetimeIndex(df_hist.index) + + if ns_parser.raw: + print(df_hist) + + mc = mpf.make_marketcolors( + up="green", down="red", edge="black", wick="black", volume="in", ohlc="i" + ) + + s = mpf.make_mpf_style(marketcolors=mc, gridstyle=":", y_on_right=True) + + if gtff.USE_ION: + plt.ion() + + mpf.plot( + df_hist, + type="candle", + volume=True, + title=f"\n{ticker.upper()} {strike} {op_type} expiring {expiry} Historical", + style=s, + figratio=(10, 7), + figscale=1.10, + figsize=(plot_autoscale()), + update_width_config=dict( + candle_linewidth=1.0, candle_width=0.8, volume_linewidth=1.0 + ), + ) + + print("") + + if ns_parser.export: + export_data( + ns_parser.export, + os.path.dirname(os.path.abspath(__file__)), + f"historical_op_{ticker}_{expiry}_{str(strike).replace('.','p')}_{op_type}", + df_hist, + ) + + except Exception as e: + print(e, "\n") + + except SystemExit: + print("") diff --git a/gamestonk_terminal/options/yahoo_view.py b/gamestonk_terminal/options/yahoo_view.py deleted file mode 100644 index 37d995b1c2a..00000000000 --- a/gamestonk_terminal/options/yahoo_view.py +++ /dev/null @@ -1,517 +0,0 @@ -""" Volume view """ -__docformat__ = "numpy" - -from bisect import bisect_left -from typing import List -import argparse -import pandas as pd -import matplotlib.pyplot as plt -import numpy as np -import seaborn as sns - -from gamestonk_terminal.helper_funcs import ( - plot_autoscale, - check_non_negative, - parse_known_args_and_warn, -) -from gamestonk_terminal import config_plot as cfgPlot -from gamestonk_terminal import feature_flags as gtff - - -def plot_volume_open_interest( - other_args: List[str], - ticker: str, - exp_date: str, - last_adj_close_price: float, - op_calls: pd.DataFrame, - op_puts: pd.DataFrame, -): - """Plot volume open interest - - Parameters - ---------- - other_args : List[str] - Command line arguments to be processed with argparse - ticker : str - Main ticker to compare income - exp_date : str - Expiry date of the option - last_adj_close_price: float - Last adjusted closing price - op_calls: pd.DataFrame - Option data calls - op_puts: pd.DataFrame - Option data puts - """ - parser = argparse.ArgumentParser( - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="voi", - description=""" - Plots Volume + Open Interest of calls vs puts. [Source: Yahoo Finance] - """, - ) - parser.add_argument( - "-v", - "--minv", - dest="min_vol", - type=check_non_negative, - default=-1, - help="minimum volume (considering open interest) threshold of the plot.", - ) - parser.add_argument( - "-m", - "--min", - dest="min_sp", - type=check_non_negative, - default=-1, - help="minimum strike price to consider in the plot.", - ) - parser.add_argument( - "-M", - "--max", - dest="max_sp", - type=check_non_negative, - default=-1, - help="maximum strike price to consider in the plot.", - ) - - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - - df_calls, df_puts, max_pain = get_calls_puts_maxpain( - op_calls, op_puts, last_adj_close_price - ) - - if ( - ns_parser.min_vol == -1 - and ns_parser.min_sp == -1 - and ns_parser.max_sp == -1 - ): - # If no argument provided, we use the percentile 50 to get 50% of upper volume data - volume_percentile_threshold = 50 - min_vol_calls = np.percentile(df_calls["oi+v"], volume_percentile_threshold) - min_vol_puts = np.percentile(df_puts["oi+v"], volume_percentile_threshold) - - df_calls = df_calls[df_calls["oi+v"] > min_vol_calls] - df_puts = df_puts[df_puts["oi+v"] < min_vol_puts] - - else: - if ns_parser.min_vol > -1: - df_calls = df_calls[df_calls["oi+v"] > ns_parser.min_vol] - df_puts = df_puts[df_puts["oi+v"] < -ns_parser.min_vol] - - if ns_parser.min_sp > -1: - df_calls = df_calls[df_calls["strike"] > ns_parser.min_sp] - df_puts = df_puts[df_puts["strike"] > ns_parser.min_sp] - - if ns_parser.max_sp > -1: - df_calls = df_calls[df_calls["strike"] < ns_parser.max_sp] - df_puts = df_puts[df_puts["strike"] < ns_parser.max_sp] - - if df_calls.empty and df_puts.empty: - print( - "The filtering applied is too strong, there is no data available for such conditions.\n" - ) - return - - # Initialize the matplotlib figure - _, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) - - # make x axis symmetric - axis_origin = max(abs(max(df_puts["oi+v"])), abs(max(df_calls["oi+v"]))) - ax.set_xlim(-axis_origin, +axis_origin) - - sns.set_style(style="darkgrid") - - g = sns.barplot( - x="oi+v", - y="strike", - data=df_calls, - label="Calls: Open Interest", - color="lightgreen", - orient="h", - ) - - g = sns.barplot( - x="volume", - y="strike", - data=df_calls, - label="Calls: Volume", - color="green", - orient="h", - ) - - g = sns.barplot( - x="oi+v", - y="strike", - data=df_puts, - label="Puts: Open Interest", - color="pink", - orient="h", - ) - - g = sns.barplot( - x="volume", - y="strike", - data=df_puts, - label="Puts: Volume", - color="red", - orient="h", - ) - - # draw spot line - s = [float(strike.get_text()) for strike in ax.get_yticklabels()] - spot_index = bisect_left( - s, last_adj_close_price - ) # find where the spot is on the graph - spot_line = ax.axhline(spot_index, ls="--", color="dodgerblue", alpha=0.3) - - # draw max pain line - max_pain_index = bisect_left(s, max_pain) - max_pain_line = ax.axhline(max_pain_index, ls="-", color="black", alpha=0.3) - max_pain_line.set_linewidth(5) - - # format ticklabels without - for puts - g.set_xticks(g.get_xticks()) - xlabels = [f"{x:,.0f}".replace("-", "") for x in g.get_xticks()] - g.set_xticklabels(xlabels) - - plt.title( - f"{ticker} volumes for {exp_date} (open interest displayed only during market hours)" - ) - ax.invert_yaxis() - - _ = ax.legend() - handles, _ = ax.get_legend_handles_labels() - handles.append(spot_line) - handles.append(max_pain_line) - - # create legend labels + add to graph - labels = [ - "Calls open interest", - "Calls volume ", - "Puts open interest", - "Puts volume", - "Current stock price", - f"Max pain = {max_pain}", - ] - - plt.legend(handles=handles[:], labels=labels) - sns.despine(left=True, bottom=True) - - if gtff.USE_ION: - plt.ion() - plt.show() - print("") - - except Exception as e: - print(e, "\n") - - -def get_calls_puts_maxpain( - op_calls: pd.DataFrame, op_puts: pd.DataFrame, last_adj_close_price: float -): - """Get calls and puts dataframes, and max pain - - Parameters - ---------- - op_calls: pd.DataFrame - Option data calls - op_puts: pd.DataFrame - Option data puts - last_adj_close_price: float - Last adjusted closing price - - Returns - ---------- - pd.DataFrame - Processed calls dataframe - pd.DataFrame - Processed puts dataframe - float - Max pain - """ - df_calls = pd.DataFrame() - df_puts = pd.DataFrame() - max_pain = 0 - - if not op_calls.empty: - # Process Calls Data - df_calls = op_calls.pivot_table( - index="strike", values=["volume", "openInterest"], aggfunc="sum" - ).reindex() - df_calls["strike"] = df_calls.index - df_calls["type"] = "calls" - df_calls["openInterest"] = df_calls["openInterest"] - df_calls["volume"] = df_calls["volume"] - df_calls["oi+v"] = df_calls["openInterest"] + df_calls["volume"] - df_calls["spot"] = round(last_adj_close_price, 2) - - if not op_puts.empty: - # Process Puts Data - df_puts = op_puts.pivot_table( - index="strike", values=["volume", "openInterest"], aggfunc="sum" - ).reindex() - df_puts["strike"] = df_puts.index - df_puts["type"] = "puts" - df_puts["openInterest"] = df_puts["openInterest"] - df_puts["volume"] = -df_puts["volume"] - df_puts["openInterest"] = -df_puts["openInterest"] - df_puts["oi+v"] = df_puts["openInterest"] + df_puts["volume"] - df_puts["spot"] = round(last_adj_close_price, 2) - - if not op_calls.empty and not op_puts.empty: - # Get max pain - df_opt = pd.merge(df_calls, df_puts, left_index=True, right_index=True) - df_opt = df_opt[["openInterest_x", "openInterest_y"]].rename( - columns={"openInterest_x": "OI_call", "openInterest_y": "OI_put"} - ) - max_pain = get_max_pain(df_opt) - - return df_calls, df_puts, max_pain - - -def plot_calls_volume_open_interest( - other_args: List[str], - ticker: str, - exp_date: str, - last_adj_close_price: float, - op_calls: pd.DataFrame, -): - """Plot calls volume open interest - - Parameters - ---------- - other_args : List[str] - Command line arguments to be processed with argparse - ticker : str - Main ticker to compare income - exp_date : str - Expiry date of the option - last_adj_close_price: float - Last adjusted closing price - op_calls: pd.DataFrame - Option data calls - """ - parser = argparse.ArgumentParser( - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="vcalls", - description=""" - Plots Calls Volume + Open Interest. [Source: Yahoo Finance] - """, - ) - parser.add_argument( - "-m", - "--min", - dest="min_sp", - type=check_non_negative, - default=-1, - help="minimum strike price to consider in the plot.", - ) - parser.add_argument( - "-M", - "--max", - dest="max_sp", - type=check_non_negative, - default=-1, - help="maximum strike price to consider in the plot.", - ) - - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - - df_calls, _, _ = get_calls_puts_maxpain( - op_calls, pd.DataFrame(), last_adj_close_price - ) - - if ns_parser.min_sp == -1: - min_strike = 0.75 * last_adj_close_price - else: - min_strike = ns_parser.min_sp - - if ns_parser.max_sp == -1: - max_strike = 1.25 * last_adj_close_price - else: - max_strike = ns_parser.max_sp - - plt.figure(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) - - plt.axvline(last_adj_close_price, lw=2) - plt.bar( - df_calls["strike"], - df_calls["volume"] + df_calls["openInterest"], - color="lightgreen", - width=0.45, - ) - plt.bar(df_calls["strike"], df_calls["volume"], color="green", width=0.45) - - plt.title(f"{ticker} calls volumes for {exp_date} ") - plt.legend(["Stock Price", "Open Interest", "Volume"]) - plt.xlabel("Strike Price") - plt.ylabel("Volume") - plt.xlim([min_strike, max_strike]) - - if gtff.USE_ION: - plt.ion() - plt.show() - - print("") - - except Exception as e: - print(e, "\n") - - -def plot_puts_volume_open_interest( - other_args: List[str], - ticker: str, - exp_date: str, - last_adj_close_price: float, - op_puts: pd.DataFrame, -): - """Plot puts volume open interest - - Parameters - ---------- - other_args : List[str] - Command line arguments to be processed with argparse - ticker : str - Main ticker to compare income - exp_date : str - Expiry date of the option - last_adj_close_price: float - Last adjusted closing price - op_puts: pd.DataFrame - Option data puts - """ - parser = argparse.ArgumentParser( - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="vputs", - description=""" - Plots Puts Volume + Open Interest. [Source: Yahoo Finance] - """, - ) - parser.add_argument( - "-m", - "--min", - dest="min_sp", - type=check_non_negative, - default=-1, - help="minimum strike price to consider in the plot.", - ) - parser.add_argument( - "-M", - "--max", - dest="max_sp", - type=check_non_negative, - default=-1, - help="maximum strike price to consider in the plot.", - ) - - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - - _, df_puts, _ = get_calls_puts_maxpain( - pd.DataFrame(), op_puts, last_adj_close_price - ) - - if ns_parser.min_sp == -1: - min_strike = 0.75 * last_adj_close_price - else: - min_strike = ns_parser.min_sp - - if ns_parser.max_sp == -1: - max_strike = 1.25 * last_adj_close_price - else: - max_strike = ns_parser.max_sp - - plt.figure(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) - - plt.axvline(last_adj_close_price, lw=2) - plt.bar( - df_puts["strike"], - -df_puts["volume"] - df_puts["openInterest"], - color="pink", - width=0.45, - ) - plt.bar(df_puts["strike"], -df_puts["volume"], color="red", width=0.45) - - plt.title(f"{ticker} puts volumes for {exp_date} ") - plt.legend(["Stock Price", "Open Interest", "Volume"]) - plt.xlabel("Strike Price") - plt.ylabel("Volume") - plt.xlim([min_strike, max_strike]) - - if gtff.USE_ION: - plt.ion() - plt.show() - - print("") - - except Exception as e: - print(e, "\n") - - -def get_loss_at_strike(strike: float, chain: pd.DataFrame) -> float: - """Function to get the loss at the given expiry - - Parameters - ---------- - strike: Union[int,float] - Value to calculate total loss at - chain: Dataframe: - Dataframe containing at least strike and openInterest - - Returns - ------- - loss: Union[float,int] - Total loss - """ - - itm_calls = chain[chain.index < strike][["OI_call"]] - itm_calls["loss"] = (strike - itm_calls.index) * itm_calls["OI_call"] - call_loss = itm_calls["loss"].sum() - # The *-1 below is due to a sign change for plotting in the _view code - itm_puts = chain[chain.index > strike][["OI_put"]] - itm_puts["loss"] = (itm_puts.index - strike) * itm_puts["OI_put"] * -1 - put_loss = itm_puts.loss.sum() - loss = call_loss + put_loss - - return loss - - -def get_max_pain(chain: pd.DataFrame) -> int: - """Returns the max pain for a given call/put dataframe - - Parameters - ---------- - chain: DataFrame - Dataframe to calculate value from - - Returns - ------- - max_pain : int - Max pain value - """ - - strikes = np.array(chain.index) - if ("OI_call" not in chain.columns) or ("OI_put" not in chain.columns): - print("Incorrect columns. Unable to parse max pain") - return np.nan - - loss = [] - for price_at_exp in strikes: - loss.append(get_loss_at_strike(price_at_exp, chain)) - - chain["loss"] = loss - max_pain = chain["loss"].idxmin() - - return max_pain diff --git a/gamestonk_terminal/options/yfinance_model.py b/gamestonk_terminal/options/yfinance_model.py new file mode 100644 index 00000000000..90695917ccf --- /dev/null +++ b/gamestonk_terminal/options/yfinance_model.py @@ -0,0 +1,42 @@ +"""Yfinance options model""" +__docformat__ = "numpy" + +import yfinance as yf + + +def option_expirations(ticker: str): + """Get available expiration dates for given ticker + + Parameters + ---------- + ticker: str + Ticker to get expirations for + + Returns + ------- + dates: List[str] + List of of available expirations + """ + yf_ticker = yf.Ticker(ticker) + dates = list(yf_ticker.options) + return dates + + +def get_option_chain(ticker: str, expiration: str): + """Gets option chain from yf for given ticker and expiration + + Parameters + ---------- + ticker: str + Ticker to get options for + expiration: str + Date to get options for + + Returns + ------- + chains: yf.ticker.Options + Options chain + """ + yf_ticker = yf.Ticker(ticker) + chains = yf_ticker.option_chain(expiration) + return chains diff --git a/gamestonk_terminal/options/yfinance_view.py b/gamestonk_terminal/options/yfinance_view.py new file mode 100644 index 00000000000..32a6293d473 --- /dev/null +++ b/gamestonk_terminal/options/yfinance_view.py @@ -0,0 +1,367 @@ +"""Yfinance options view""" +__docformat__ = "numpy" + +import argparse +from bisect import bisect_left + +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +import yfinance as yf +import numpy as np + +import gamestonk_terminal.feature_flags as gtff +import gamestonk_terminal.config_plot as cfp +from gamestonk_terminal.helper_funcs import ( + plot_autoscale, +) +from gamestonk_terminal.options import op_helpers + + +def plot_oi( + calls: pd.DataFrame, + puts: pd.DataFrame, + ticker: str, + expiry: str, + ns_parser: argparse.Namespace, +): + """Plot open interest + + Parameters + ---------- + calls: pd.DataFrame + Dataframe of call options + puts: pd.DataFrame + Dataframe of put options + ticker: str + Ticker + expiry: str + Expiry date for options + ns_parser: argparse.Namespace + Parsed namespace + """ + + current_price = float(yf.Ticker(ticker).info["regularMarketPrice"]) + + if ns_parser.min == -1: + min_strike = 0.75 * current_price + else: + min_strike = ns_parser.min + + if ns_parser.max == -1: + max_strike = 1.25 * current_price + else: + max_strike = ns_parser.max + + if ns_parser.calls and ns_parser.puts: + print("Both flags selected, please select one", "\n") + return + + call_oi = calls.set_index("strike")["openInterest"] / 1000 + put_oi = puts.set_index("strike")["openInterest"] / 1000 + + df_opt = pd.merge(call_oi, put_oi, left_index=True, right_index=True) + df_opt = df_opt.rename( + columns={"openInterest_x": "OI_call", "openInterest_y": "OI_put"} + ) + + max_pain = op_helpers.calculate_max_pain(df_opt) + plt.style.use("classic") + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + + if not ns_parser.calls: + put_oi.plot( + x="strike", + y="openInterest", + label="Puts", + ax=ax, + marker="o", + ls="-", + c="r", + ) + if not ns_parser.puts: + call_oi.plot( + x="strike", + y="openInterest", + label="Calls", + ax=ax, + marker="o", + ls="-", + c="g", + ) + ax.axvline( + current_price, lw=2, c="k", ls="--", label="Current Price", alpha=0.7 + ) + ax.axvline(max_pain, lw=3, c="k", label=f"Max Pain: {max_pain}", alpha=0.7) + ax.grid("on") + ax.set_xlabel("Strike Price") + ax.set_ylabel("Open Interest (1k) ") + ax.set_xlim(min_strike, max_strike) + + if gtff.USE_ION: + plt.ion() + + ax.set_title(f"Open Interest for {ticker.upper()} expiring {expiry}") + plt.legend(loc=0) + fig.tight_layout(pad=1) + + plt.show() + plt.style.use("default") + print("") + + +def plot_vol( + calls: pd.DataFrame, + puts: pd.DataFrame, + ticker: str, + expiry: str, + ns_parser: argparse.Namespace, +): + """Plot volume + + Parameters + ---------- + calls: pd.DataFrame + Dataframe of call options + puts: pd.DataFrame + Dataframe of put options + ticker: str + Ticker + expiry: str + Expiry date for options + ns_parser: argparse.Namespace + Parsed namespace + """ + + current_price = float(yf.Ticker(ticker).info["regularMarketPrice"]) + + if ns_parser.min == -1: + min_strike = 0.75 * current_price + else: + min_strike = ns_parser.min + + if ns_parser.max == -1: + max_strike = 1.25 * current_price + else: + max_strike = ns_parser.max + + if ns_parser.calls and ns_parser.puts: + print("Both flags selected, please select one", "\n") + return + + call_v = calls.set_index("strike")["volume"] / 1000 + put_v = puts.set_index("strike")["volume"] / 1000 + plt.style.use("classic") + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + + if not ns_parser.calls: + put_v.plot( + x="strike", + y="volume", + label="Puts", + ax=ax, + marker="o", + ls="-", + c="r", + ) + if not ns_parser.puts: + call_v.plot( + x="strike", + y="volume", + label="Calls", + ax=ax, + marker="o", + ls="-", + c="g", + ) + ax.axvline(current_price, lw=2, c="k", ls="--", label="Current Price", alpha=0.7) + ax.grid("on") + ax.set_xlabel("Strike Price") + ax.set_ylabel("Volume (1k) ") + ax.set_xlim(min_strike, max_strike) + + if gtff.USE_ION: + plt.ion() + + ax.set_title(f"Volume for {ticker.upper()} expiring {expiry}") + plt.legend(loc=0) + fig.tight_layout(pad=1) + + plt.show() + plt.style.use("default") + print("") + + +def plot_volume_open_interest( + ticker: str, + exp_date: str, + calls: pd.DataFrame, + puts: pd.DataFrame, + ns_parser: argparse.Namespace, +): + """Plot volume and open interest + + Parameters + ---------- + ticker : str + Main ticker to compare income + exp_date : str + Expiry date of the option + calls: pd.DataFrame + Option data calls + puts: pd.DataFrame + Option data puts + ns_parser: argparse.Namespace + Parsed namespace + """ + + current_price = float(yf.Ticker(ticker).info["regularMarketPrice"]) + + # Process Calls Data + df_calls = calls.pivot_table( + index="strike", values=["volume", "openInterest"], aggfunc="sum" + ).reindex() + df_calls["strike"] = df_calls.index + df_calls["type"] = "calls" + df_calls["openInterest"] = df_calls["openInterest"] + df_calls["volume"] = df_calls["volume"] + df_calls["oi+v"] = df_calls["openInterest"] + df_calls["volume"] + df_calls["spot"] = round(current_price, 2) + + df_puts = puts.pivot_table( + index="strike", values=["volume", "openInterest"], aggfunc="sum" + ).reindex() + df_puts["strike"] = df_puts.index + df_puts["type"] = "puts" + df_puts["openInterest"] = df_puts["openInterest"] + df_puts["volume"] = -df_puts["volume"] + df_puts["openInterest"] = -df_puts["openInterest"] + df_puts["oi+v"] = df_puts["openInterest"] + df_puts["volume"] + df_puts["spot"] = round(current_price, 2) + + call_oi = calls.set_index("strike")["openInterest"] / 1000 + put_oi = puts.set_index("strike")["openInterest"] / 1000 + + df_opt = pd.merge(call_oi, put_oi, left_index=True, right_index=True) + df_opt = df_opt.rename( + columns={"openInterest_x": "OI_call", "openInterest_y": "OI_put"} + ) + + max_pain = op_helpers.calculate_max_pain(df_opt) + + if ns_parser.min_vol == -1 and ns_parser.min_sp == -1 and ns_parser.max_sp == -1: + # If no argument provided, we use the percentile 50 to get 50% of upper volume data + volume_percentile_threshold = 50 + min_vol_calls = np.percentile(df_calls["oi+v"], volume_percentile_threshold) + min_vol_puts = np.percentile(df_puts["oi+v"], volume_percentile_threshold) + + df_calls = df_calls[df_calls["oi+v"] > min_vol_calls] + df_puts = df_puts[df_puts["oi+v"] < min_vol_puts] + + else: + if ns_parser.min_vol > -1: + df_calls = df_calls[df_calls["oi+v"] > ns_parser.min_vol] + df_puts = df_puts[df_puts["oi+v"] < -ns_parser.min_vol] + + if ns_parser.min_sp > -1: + df_calls = df_calls[df_calls["strike"] > ns_parser.min_sp] + df_puts = df_puts[df_puts["strike"] > ns_parser.min_sp] + + if ns_parser.max_sp > -1: + df_calls = df_calls[df_calls["strike"] < ns_parser.max_sp] + df_puts = df_puts[df_puts["strike"] < ns_parser.max_sp] + + if df_calls.empty and df_puts.empty: + print( + "The filtering applied is too strong, there is no data available for such conditions.\n" + ) + return + + # Initialize the matplotlib figure + _, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + + # make x axis symmetric + axis_origin = max(abs(max(df_puts["oi+v"])), abs(max(df_calls["oi+v"]))) + ax.set_xlim(-axis_origin, +axis_origin) + + sns.set_style(style="darkgrid") + + g = sns.barplot( + x="oi+v", + y="strike", + data=df_calls, + label="Calls: Open Interest", + color="lightgreen", + orient="h", + ) + + g = sns.barplot( + x="volume", + y="strike", + data=df_calls, + label="Calls: Volume", + color="green", + orient="h", + ) + + g = sns.barplot( + x="oi+v", + y="strike", + data=df_puts, + label="Puts: Open Interest", + color="pink", + orient="h", + ) + + g = sns.barplot( + x="volume", + y="strike", + data=df_puts, + label="Puts: Volume", + color="red", + orient="h", + ) + + # draw spot line + s = [float(strike.get_text()) for strike in ax.get_yticklabels()] + spot_index = bisect_left(s, current_price) # find where the spot is on the graph + spot_line = ax.axhline(spot_index, ls="--", color="dodgerblue", alpha=0.3) + + # draw max pain line + max_pain_index = bisect_left(s, max_pain) + max_pain_line = ax.axhline(max_pain_index, ls="-", color="black", alpha=0.3) + max_pain_line.set_linewidth(3) + + # format ticklabels without - for puts + g.set_xticks(g.get_xticks()) + xlabels = [f"{x:,.0f}".replace("-", "") for x in g.get_xticks()] + g.set_xticklabels(xlabels) + + plt.title( + f"{ticker} volumes for {exp_date} (open interest displayed only during market hours)" + ) + ax.invert_yaxis() + + _ = ax.legend() + handles, _ = ax.get_legend_handles_labels() + handles.append(spot_line) + handles.append(max_pain_line) + + # create legend labels + add to graph + labels = [ + "Calls open interest", + "Calls volume ", + "Puts open interest", + "Puts volume", + "Current stock price", + f"Max pain = {max_pain}", + ] + + plt.legend(handles=handles[:], labels=labels) + sns.despine(left=True, bottom=True) + + if gtff.USE_ION: + plt.ion() + plt.show() + plt.style.use("default") + print("") diff --git a/terminal.py b/terminal.py index f42dd927fb0..0b608a5d76f 100644 --- a/terminal.py +++ b/terminal.py @@ -432,18 +432,7 @@ Contexts: def call_op(self, _): """Process op command""" - if self.interval == "1440min": - return op_controller.menu( - self.ticker, - self.stock, - ) - - df_stock = yf.download(self.ticker, start=self.start, progress=False) - - return op_controller.menu( - self.ticker, - df_stock, - ) + return op_controller.menu(self.ticker) def call_pred(self, _): """Process pred command"""