From d6ba260df9c868f189d247169c5db89ba145095c Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 17 Mar 2023 14:46:00 -0400 Subject: [PATCH 1/4] Step one: error handle Option class --- openbb_terminal/stocks/options/op_helpers.py | 20 +++++++--- .../stocks/options/options_sdk_helper.py | 4 +- .../stocks/options/test_op_helpers.py | 37 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 tests/openbb_terminal/stocks/options/test_op_helpers.py diff --git a/openbb_terminal/stocks/options/op_helpers.py b/openbb_terminal/stocks/options/op_helpers.py index 784eae3affe8..ca0614793e63 100644 --- a/openbb_terminal/stocks/options/op_helpers.py +++ b/openbb_terminal/stocks/options/op_helpers.py @@ -270,9 +270,9 @@ def get_greeks( strikes = [] for _, row in chain.iterrows(): vol = row["impliedVolatility"] - opt_type = 1 if row["optionType"] == "call" else -1 + is_call = row["optionType"] == "call" opt = Option( - current_price, row["strike"], risk_free, div_cont, dif, vol, opt_type + current_price, row["strike"], risk_free, div_cont, dif, vol, is_call ) tmp = [ opt.Delta(), @@ -393,7 +393,7 @@ def __init__( div_cont: float, expiry: float, vol: float, - opt_type: int = 1, + is_call: bool = True, ): """ Class for getting the greeks of options. Inspiration from: @@ -413,10 +413,18 @@ def __init__( The number of days until expiration vol : float The underlying volatility for an option - opt_type : int - put == -1; call == +1 + is_call : bool + True if call, False if put """ - self.Type = int(opt_type) + if expiry <= 0: + raise ValueError("Expiry must be greater than 0") + if vol <= 0: + raise ValueError("Volatility must be greater than 0") + if s <= 0: + raise ValueError("Price must be greater than 0") + if k <= 0: + raise ValueError("Strike must be greater than 0") + self.Type = 1 if is_call else -1 self.price = float(s) self.strike = float(k) self.risk_free = float(rf) diff --git a/openbb_terminal/stocks/options/options_sdk_helper.py b/openbb_terminal/stocks/options/options_sdk_helper.py index 7744b31e2e5d..a0f5a773c5e3 100644 --- a/openbb_terminal/stocks/options/options_sdk_helper.py +++ b/openbb_terminal/stocks/options/options_sdk_helper.py @@ -296,9 +296,9 @@ def get_greeks( strikes = [] for _, row in chain.iterrows(): vol = row["impliedVolatility"] - opt_type = 1 if row["optionType"] == "call" else -1 + is_call = row["optionType"] == "call" opt = Option( - current_price, row["strike"], risk_free, div_cont, dif, vol, opt_type + current_price, row["strike"], risk_free, div_cont, dif, vol, is_call ) tmp = [ opt.Delta(), diff --git a/tests/openbb_terminal/stocks/options/test_op_helpers.py b/tests/openbb_terminal/stocks/options/test_op_helpers.py new file mode 100644 index 000000000000..5facb92b9dd6 --- /dev/null +++ b/tests/openbb_terminal/stocks/options/test_op_helpers.py @@ -0,0 +1,37 @@ +import pytest + +from openbb_terminal.stocks.options.op_helpers import Option + + +@pytest.mark.parametrize("s", [0, 1]) +@pytest.mark.parametrize("k", [0, 1]) +@pytest.mark.parametrize("rf", [-1, 0, 0.05]) +@pytest.mark.parametrize("div_cont", [0, 0.05]) +@pytest.mark.parametrize("expiry", [0, 0.05]) +@pytest.mark.parametrize("vol", [0, 0.05]) +@pytest.mark.parametrize("is_call", [True, False]) +def test_option_class(s, k, rf, div_cont, expiry, vol, is_call): + if expiry <= 0 or vol <= 0 or s <= 0 or k <= 0: + with pytest.raises(ValueError): + opt = Option( + s=s, + k=k, + rf=rf, + div_cont=div_cont, + expiry=expiry, + vol=vol, + is_call=is_call, + ) + else: + opt = Option( + s=s, k=k, rf=rf, div_cont=div_cont, expiry=expiry, vol=vol, is_call=is_call + ) + opt.Delta() + opt.Gamma() + opt.Vega() + opt.Theta() + opt.Rho() + opt.Phi() + opt.Charm() + opt.Vanna(0.01) + opt.Vomma(0.01) From a3eb926abd4988788f2970092e09628fe3ad6df1 Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 17 Mar 2023 14:51:39 -0400 Subject: [PATCH 2/4] Step one: error handle Option class --- openbb_terminal/stocks/options/op_helpers.py | 42 +++++++++++-------- .../stocks/options/options_sdk_helper.py | 31 +++++++------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/openbb_terminal/stocks/options/op_helpers.py b/openbb_terminal/stocks/options/op_helpers.py index ca0614793e63..58c4182eefc5 100644 --- a/openbb_terminal/stocks/options/op_helpers.py +++ b/openbb_terminal/stocks/options/op_helpers.py @@ -271,30 +271,36 @@ def get_greeks( for _, row in chain.iterrows(): vol = row["impliedVolatility"] is_call = row["optionType"] == "call" - opt = Option( - current_price, row["strike"], risk_free, div_cont, dif, vol, is_call - ) - tmp = [ - opt.Delta(), - opt.Gamma(), - opt.Vega(), - opt.Theta(), - ] result = ( [row[col] for col in row.index.tolist()] if show_all else [row[col] for col in ["strike", "impliedVolatility"]] ) - result += tmp - - if show_extra_greeks: - result += [ - opt.Rho(), - opt.Phi(), - opt.Charm(), - opt.Vanna(0.01), - opt.Vomma(0.01), + try: + opt = Option( + current_price, row["strike"], risk_free, div_cont, dif, vol, is_call + ) + tmp = [ + opt.Delta(), + opt.Gamma(), + opt.Vega(), + opt.Theta(), ] + result += tmp + + if show_extra_greeks: + result += [ + opt.Rho(), + opt.Phi(), + opt.Charm(), + opt.Vanna(0.01), + opt.Vomma(0.01), + ] + except ValueError: + result += [np.nan] * 4 + + if show_extra_greeks: + result += [np.nan] * 5 strikes.append(result) greek_columns = [ diff --git a/openbb_terminal/stocks/options/options_sdk_helper.py b/openbb_terminal/stocks/options/options_sdk_helper.py index a0f5a773c5e3..c0cedb0e932b 100644 --- a/openbb_terminal/stocks/options/options_sdk_helper.py +++ b/openbb_terminal/stocks/options/options_sdk_helper.py @@ -297,20 +297,23 @@ def get_greeks( for _, row in chain.iterrows(): vol = row["impliedVolatility"] is_call = row["optionType"] == "call" - opt = Option( - current_price, row["strike"], risk_free, div_cont, dif, vol, is_call - ) - tmp = [ - opt.Delta(), - opt.Gamma(), - opt.Vega(), - opt.Theta(), - opt.Rho(), - opt.Phi(), - opt.Charm(), - opt.Vanna(0.01), - opt.Vomma(0.01), - ] + try: + opt = Option( + current_price, row["strike"], risk_free, div_cont, dif, vol, is_call + ) + tmp = [ + opt.Delta(), + opt.Gamma(), + opt.Vega(), + opt.Theta(), + opt.Rho(), + opt.Phi(), + opt.Charm(), + opt.Vanna(0.01), + opt.Vomma(0.01), + ] + except ValueError: + tmp = [np.nan] * 9 result = [row[col] for col in row.index.tolist()] result += tmp From ca77e68bfdac705af90d96cf4a205f94d56de2a9 Mon Sep 17 00:00:00 2001 From: colin99d Date: Tue, 21 Mar 2023 09:49:13 -0400 Subject: [PATCH 3/4] Got upcoming working --- .../stocks/discovery/disc_controller.py | 4 +- .../stocks/discovery/seeking_alpha_model.py | 57 ++++++++++-------- .../stocks/discovery/seeking_alpha_view.py | 59 ++++--------------- 3 files changed, 49 insertions(+), 71 deletions(-) diff --git a/openbb_terminal/stocks/discovery/disc_controller.py b/openbb_terminal/stocks/discovery/disc_controller.py index 52e3605022cd..628384ab9208 100644 --- a/openbb_terminal/stocks/discovery/disc_controller.py +++ b/openbb_terminal/stocks/discovery/disc_controller.py @@ -659,7 +659,7 @@ def call_upcoming(self, other_args: List[str]): action="store", dest="limit", type=check_positive, - default=1, + default=10, help="Limit of upcoming earnings release dates to display.", ) parser.add_argument( @@ -668,7 +668,7 @@ def call_upcoming(self, other_args: List[str]): action="store", dest="n_pages", type=check_positive, - default=10, + default=1, help="Number of pages to read upcoming earnings from in Seeking Alpha website.", ) if other_args and "-" not in other_args[0][0]: diff --git a/openbb_terminal/stocks/discovery/seeking_alpha_model.py b/openbb_terminal/stocks/discovery/seeking_alpha_model.py index cf11607300c4..215690daaac2 100644 --- a/openbb_terminal/stocks/discovery/seeking_alpha_model.py +++ b/openbb_terminal/stocks/discovery/seeking_alpha_model.py @@ -2,7 +2,8 @@ __docformat__ = "numpy" import logging -from typing import Dict, List +from datetime import date, timedelta +from typing import Dict, List, Optional import pandas as pd from bs4 import BeautifulSoup @@ -36,41 +37,51 @@ def get_earnings_html(url_next_earnings: str) -> str: return earnings_html +def get_filters(date_str: str) -> str: + text = f"?filter[selected_date]={date_str}&filter[with_rating]=false&filter[currency]=USD" + return text + + @log_start_end(log=logger) -def get_next_earnings(limit: int = 10) -> DataFrame: +def get_next_earnings(limit: int = 10, start: Optional[date] = None) -> DataFrame: """Returns a DataFrame with upcoming earnings Parameters ---------- limit : int Number of pages + date: Optional[date] + Date to start from Returns ------- DataFrame Upcoming earnings DataFrame """ - earnings = [] - url_next_earnings = "https://seekingalpha.com/earnings/earnings-calendar" - - for idx in range(0, limit): - text_soup_earnings = BeautifulSoup( - get_earnings_html(url_next_earnings), - "lxml", - ) - - for stock_rows in text_soup_earnings.findAll("tr", {"data-exchange": "NASDAQ"}): - stocks = [a_stock.text for a_stock in stock_rows.contents[:3]] - earnings.append(stocks) - - url_next_earnings = ( - f"https://seekingalpha.com/earnings/earnings-calendar/{idx+1}" - ) - - df_earnings = pd.DataFrame(earnings, columns=["Ticker", "Name", "Date"]) - df_earnings["Date"] = pd.to_datetime(df_earnings["Date"]) - df_earnings = df_earnings.set_index("Date") - + if start is None: + start = date.today() + base_url = "https://seekingalpha.com/api/v3/earnings_calendar/tickers" + df_earnings = pd.DataFrame() + + for _ in range(0, limit): + date_str = start.strftime("%Y-%m-%d") + response = request(base_url + get_filters(date_str), timeout=10) + data = response.json()["data"] + cleaned_data = [x["attributes"] for x in data] + temp_df = pd.DataFrame.from_records(cleaned_data) + temp_df = temp_df.drop(columns=["sector_id"]) + temp_df["Date"] = start + df_earnings = pd.concat([df_earnings, temp_df], join="outer", ignore_index=True) + start = start + timedelta(days=1) + + df_earnings = df_earnings.rename( + columns={ + "slug": "Ticker", + "name": "Name", + "release_time": "Release Time", + "exchange": "Exchange", + } + ) return df_earnings diff --git a/openbb_terminal/stocks/discovery/seeking_alpha_view.py b/openbb_terminal/stocks/discovery/seeking_alpha_view.py index 6b7c6043606d..055ac01ce9f0 100644 --- a/openbb_terminal/stocks/discovery/seeking_alpha_view.py +++ b/openbb_terminal/stocks/discovery/seeking_alpha_view.py @@ -17,8 +17,8 @@ @log_start_end(log=logger) def upcoming_earning_release_dates( - num_pages: int = 5, - limit: int = 1, + num_pages: int = 1, + limit: int = 10, export: str = "", sheet_name: Optional[str] = None, ): @@ -27,66 +27,33 @@ def upcoming_earning_release_dates( Parameters ---------- num_pages: int - Number of pages to scrape + Number of pages to scrape, each page is one day limit: int Number of upcoming earnings release dates export : str Export dataframe data to csv,json,xlsx file """ - # TODO: Check why there are repeated companies - # TODO: Create a similar command that returns not only upcoming, but antecipated earnings - # i.e. companies where expectation on their returns are high - df_earnings = seeking_alpha_model.get_next_earnings(num_pages) if df_earnings.empty: console.print("No upcoming earnings release dates found") + return - if export: - l_earnings = [] - l_earnings_dates = [] - - for n_days, earning_date in enumerate(df_earnings.index.unique()): - if n_days > (limit - 1): - break - - # TODO: Potentially extract Market Cap for each Ticker, and sort - # by Market Cap. Then cut the number of tickers shown to 10 with - # bigger market cap. Didier attempted this with yfinance, but - # the computational time involved wasn't worth pursuing that solution. - - df_earn = ( - df_earnings[earning_date == df_earnings.index][["Ticker", "Name"]] - .dropna() - .drop_duplicates() - ) - - if export: - l_earnings_dates.append(earning_date.date()) - l_earnings.append(df_earn) - - df_earn.index = df_earn["Ticker"].values - df_earn.drop(columns=["Ticker"], inplace=True) - - print_rich_table( - df_earn, - show_index=True, - headers=[f"Earnings on {earning_date.date()}"], - title="Upcoming Earnings Releases", - export=bool(export), - ) + print_rich_table( + df_earnings, + show_index=False, + headers=df_earnings.columns, + title="Upcoming Earnings Releases", + export=bool(export), + limit=limit, + ) if export: - for item in l_earnings: - item.reset_index(drop=True, inplace=True) - df_data = pd.concat(l_earnings, axis=1, ignore_index=True) - df_data.columns = l_earnings_dates - export_data( export, os.path.dirname(os.path.abspath(__file__)), "upcoming", - df_data, + df_earnings, sheet_name, ) From 7fcd7f3380bcee7482cc763da7e19fb3b77467f3 Mon Sep 17 00:00:00 2001 From: colin99d Date: Tue, 21 Mar 2023 14:33:03 -0400 Subject: [PATCH 4/4] Fixed --- .github/workflows/intel_macos_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/intel_macos_build.yml b/.github/workflows/intel_macos_build.yml index b9352f74bf1e..fc1def70762a 100644 --- a/.github/workflows/intel_macos_build.yml +++ b/.github/workflows/intel_macos_build.yml @@ -127,6 +127,7 @@ jobs: /usr/bin/codesign --deep --force --verify --verbose --options runtime --entitlements "build/pyinstaller/entitlements.plist" -s $MACOS_CODESIGN_IDENTITY DMG/OpenBB\ Terminal/.OpenBB/torch/bin/torch_shm_manager echo "Code Sign OpenBB Executable File" /usr/bin/codesign --deep --force --verify --verbose --options runtime --entitlements "build/pyinstaller/entitlements.plist" -s $MACOS_CODESIGN_IDENTITY DMG/OpenBB\ Terminal/.OpenBB/OpenBBTerminal + /usr/bin/codesign --deep --force --verify --verbose --options runtime --entitlements "build/pyinstaller/entitlements.plist" -s $MACOS_CODESIGN_IDENTITY DMG/OpenBB\ Terminal/.OpenBB/OpenBBPlotsBackend - name: Create DMG run: create-dmg --volname "OpenBB Terminal" --volicon "images/dmg_volume.icns" --background "images/openbb_dmg_background.png" --icon "OpenBB Terminal" 190 250 --window-pos 190 120 --window-size 800 400 --icon-size 100 --text-size 14 --app-drop-link 600 250 --eula LICENSE --format UDZO --no-internet-enable "OpenBB Terminal".dmg DMG - name: Code Sign DMG Installer