From 29a5d273b89abb0da58d5d7c7e01d4887a8ad3f4 Mon Sep 17 00:00:00 2001 From: nectariosouzo Date: Sun, 24 Sep 2023 12:50:43 -0400 Subject: [PATCH 01/11] Create holdings' performance for ETFs --- openbb_terminal/etf/etf_controller.py | 54 ++++++++++- openbb_terminal/etf/fmp_model.py | 109 +++++++++++++++++++++- openbb_terminal/etf/fmp_view.py | 40 +++++++- openbb_terminal/miscellaneous/i18n/en.yml | 1 + 4 files changed, 200 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/etf/etf_controller.py b/openbb_terminal/etf/etf_controller.py index 26a4311208e..a6801d01fd3 100644 --- a/openbb_terminal/etf/etf_controller.py +++ b/openbb_terminal/etf/etf_controller.py @@ -4,7 +4,7 @@ __docformat__ = "numpy" import argparse import logging import os -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from typing import List, Optional import yfinance as yf @@ -50,6 +50,7 @@ class ETFController(BaseController): "load", "overview", "holdings", + "holding_perf", "news", "candle", "weights", @@ -106,6 +107,7 @@ class ETFController(BaseController): mt.add_raw("\n") mt.add_cmd("overview", self.etf_name) mt.add_cmd("holdings", self.etf_name) + mt.add_cmd("holding_perf", self.etf_name) mt.add_cmd("weights", self.etf_name) mt.add_cmd("news", self.etf_name) mt.add_cmd("candle", self.etf_name) @@ -638,3 +640,53 @@ class ETFController(BaseController): if ns_parser.sheet_name else None, ) + + @log_start_end(log=logger) + def call_holding_perf(self, other_args: List[str]): + print(self.etf_holdings) + """Process holdings performance command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="holding_perf", + description="Look at ETF company holdings' performance", + ) + parser.add_argument( + "-s", + "--start", + type=valid_date, + default=(datetime.now().date() - timedelta(days=366)), + dest="start", + help="The starting date (format YYYY-MM-DD) to get each holding's price" + ) + parser.add_argument( + "-e", + "--end", + type=valid_date, + default=datetime.now().date(), + dest="end", + help="The ending date (format YYYY-MM-DD) to get each holding's price" + ) + parser.add_argument( + "-l", + "--limit", + type=check_positive, + dest="limit", + help="Number of holdings to get", + default=20, + ) + + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-l") + + ns_parser = self.parse_known_args_and_warn(parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES, raw= True) + if ns_parser: + if self.etf_name: + fmp_view.view_etf_holdings_performance( + ticker=self.etf_name, + start_date=ns_parser.start, + end_date=ns_parser.end, + limit=ns_parser.limit, + ) + else: + console.print("Please load a ticker using . \n") diff --git a/openbb_terminal/etf/fmp_model.py b/openbb_terminal/etf/fmp_model.py index 26b8cfc99db..9f7bf49a606 100644 --- a/openbb_terminal/etf/fmp_model.py +++ b/openbb_terminal/etf/fmp_model.py @@ -3,12 +3,14 @@ __docformat__ = "numpy" import json import logging -from typing import Dict +from typing import Dict, List from urllib.error import HTTPError from urllib.request import urlopen +import pandas as pd from openbb_terminal.core.session.current_user import get_current_user -from openbb_terminal.decorators import log_start_end +from openbb_terminal.decorators import check_api_key, log_start_end +from openbb_terminal.helper_funcs import request from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) @@ -47,3 +49,106 @@ def get_etf_sector_weightings(name: str) -> Dict: raise ValueError(data["Error Message"]) return data + +@log_start_end(log=logger) +@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) +def get_stock_price_change(ticker: str, start_date: str, end_date: str) -> float: + """Get stock's price percent change over specified time period. + + Parameters + ---------- + ticker : str + Ticker to get information from. + start: str + Date from which data is fetched in format YYYY-MM-DD + end: str + Date from which data is fetched in format YYYY-MM-DD + + Returns + ------- + float + Percent change of closing price over time period + """ + current_user = get_current_user() + url = f'''https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}?from={start_date}&to={end_date}&serietype=line&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}''' + + response = request(url) + if response.status_code != 200 or "Error Message" in response.json(): + message = f"Error, Status Code: {response.status_code}." + message = ( + message + if "Error Message" not in response.json() + else message + "\n" + response.json()["Error Message"] + ".\n" + ) + console.print(message) + return None + data = response.json() + + close_end = data['historical'][0]['close'] + close_start = data['historical'][-1]['close'] + pct_change = 100 * (close_end - close_start) / close_start + + return pct_change + +@log_start_end(log=logger) +@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) +def get_etf_holdings(ticker: str) -> List[Dict[str,any]]: + """This endpoint returns all stocks held by a specific ETF. + + Parameters + ---------- + ticker : str + ETF ticker. + + Returns + ------- + List[Dict[str,any]] + Info for stock holdings in the ETF. + """ + + current_user = get_current_user() + url = f'''https://financialmodelingprep.com/api/v3/etf-holder/{ticker}?apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}''' + response = request(url) + if response.status_code != 200 or "Error Message" in response.json(): + message = f"Error, Status Code: {response.status_code}." + message = ( + message + if "Error Message" not in response.json() + else message + "\n" + response.json()["Error Message"] + ".\n" + ) + console.print(message) + return None + + return response.json() + +@log_start_end(log=logger) +@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) +def get_holdings_pct_change(ticker: str, start_date: str, end_date: str) -> pd.DataFrame: + """Calculate percent change for each holding in ETF. + + Parameters + ---------- + ticker : str + ETF ticker. + + Returns + ------- + pd.DataFrame + Calculated percentage change for each stock in the ETF, in descending order. + """ + holdings = get_etf_holdings(ticker=ticker) + data_list = [] + for stock in holdings: + if stock['asset']: + pct_change = get_stock_price_change(ticker=stock['asset'], + start_date=start_date, end_date=end_date) + data_list.append({"Ticker": stock['asset'], "Name": stock["name"], + "Percent Change": pct_change}) + else: + data_list.append({"Ticker": '', "Name": stock["name"], + "Percent Change": 0}) + + df = pd.DataFrame(data_list) + df.sort_values(by='Percent Change',ascending=False, inplace=True) + + return df diff --git a/openbb_terminal/etf/fmp_view.py b/openbb_terminal/etf/fmp_view.py index 6499aa3fc96..486d21c9d10 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -6,9 +6,11 @@ import os from typing import Optional import pandas as pd +import seaborn as sns +import matplotlib.pyplot as plt from openbb_terminal import OpenBBFigure, theme -from openbb_terminal.decorators import log_start_end +from openbb_terminal.decorators import check_api_key, log_start_end from openbb_terminal.etf import fmp_model from openbb_terminal.helper_funcs import ( export_data, @@ -120,3 +122,39 @@ def display_etf_weightings( ) return fig.show(external=external_axes) + + +@log_start_end(log=logger) +@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) +def view_etf_holdings_performance(ticker: str, start_date: str, end_date: str, limit: int = 10, raw: bool = False, export: str = "", sheet_name: Optional[str] = None): + + data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date)[0:limit] + + colors = ['g' if x > 0 else 'r' for x in data['Percent Change']] + + sns.barplot( + x= 'Percent Change', + y= 'Name', + data=data, + palette=colors, + ) + if raw: + print_rich_table( + data, + show_index=False, + headers=["Ticker", "Name", "Percent Change"], + title="ETF Holdings' Performance", + limit=limit, + export=bool(export), + ) + + if export: + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "holdings_perf", + data, + sheet_name, + ) + + return plt.show() diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index b133a06adba..16f148b90e2 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -702,6 +702,7 @@ en: etf/scr: screener ETFs overview/performance, using preset filters etf/overview: get overview etf/holdings: top company holdings + etf/holding_perf: Performance of holdings in ETF. etf/weights: sector weights allocation etf/candle: view a candle chart for ETF etf/news: latest news of the company From 0e4252efee7c6f6536ee5238784fc2696b7958ca Mon Sep 17 00:00:00 2001 From: nectariosouzo Date: Mon, 25 Sep 2023 14:11:11 -0400 Subject: [PATCH 02/11] Set up controller and view flow --- openbb_terminal/etf/etf_controller.py | 11 +++-- openbb_terminal/etf/fmp_view.py | 66 +++++++++++++++++++-------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/openbb_terminal/etf/etf_controller.py b/openbb_terminal/etf/etf_controller.py index a6801d01fd3..130a070aefb 100644 --- a/openbb_terminal/etf/etf_controller.py +++ b/openbb_terminal/etf/etf_controller.py @@ -643,8 +643,8 @@ class ETFController(BaseController): @log_start_end(log=logger) def call_holding_perf(self, other_args: List[str]): - print(self.etf_holdings) """Process holdings performance command""" + parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, @@ -675,11 +675,13 @@ class ETFController(BaseController): help="Number of holdings to get", default=20, ) - if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-l") - ns_parser = self.parse_known_args_and_warn(parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES, raw= True) + ns_parser = self.parse_known_args_and_warn(parser, other_args, + export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES, + raw=True + ) if ns_parser: if self.etf_name: fmp_view.view_etf_holdings_performance( @@ -687,6 +689,9 @@ class ETFController(BaseController): start_date=ns_parser.start, end_date=ns_parser.end, limit=ns_parser.limit, + export=ns_parser.export, + sheet_name=ns_parser.sheet_name, + raw=ns_parser.raw, ) else: console.print("Please load a ticker using . \n") diff --git a/openbb_terminal/etf/fmp_view.py b/openbb_terminal/etf/fmp_view.py index 486d21c9d10..7d6e77c4edc 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -6,8 +6,6 @@ import os from typing import Optional import pandas as pd -import seaborn as sns -import matplotlib.pyplot as plt from openbb_terminal import OpenBBFigure, theme from openbb_terminal.decorators import check_api_key, log_start_end @@ -127,17 +125,26 @@ def display_etf_weightings( @log_start_end(log=logger) @check_api_key(['API_KEY_FINANCIALMODELINGPREP']) def view_etf_holdings_performance(ticker: str, start_date: str, end_date: str, limit: int = 10, raw: bool = False, export: str = "", sheet_name: Optional[str] = None): - + '''Display ETF's holdings' performance over specified time. [Source: FinancialModelingPrep] + Parameters + ---------- + ticker: str + ETF ticker + start_date: str + Date from which data is fetched in format YYYY-MM-DD + end: str + Date from which data is fetched in format YYYY-MM-DD + limit: int + Limit number of holdings to view. Sorted by descending percent change. + raw: bool + Display holding performance + sheet_name: str + Optionally specify the name of the sheet the data is exported to. + export: str + Type of format to export data + ''' data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date)[0:limit] - colors = ['g' if x > 0 else 'r' for x in data['Percent Change']] - - sns.barplot( - x= 'Percent Change', - y= 'Name', - data=data, - palette=colors, - ) if raw: print_rich_table( data, @@ -148,13 +155,34 @@ def view_etf_holdings_performance(ticker: str, start_date: str, end_date: str, l export=bool(export), ) - if export: - export_data( - export, - os.path.dirname(os.path.abspath(__file__)), - "holdings_perf", - data, - sheet_name, + fig = OpenBBFigure() + + if not raw or fig.is_image_export(export): + + fig.add_bar( + hovertext=[f'{x:.2f}'+"%" for x in data['Percent Change']], + x=data['Percent Change'], + y=data['Name'], + name = 'Stock', + orientation='h', + marker_color=['darkgreen' if x > 0 else 'darkred' for x in data['Percent Change']], ) - return plt.show() + fig.update_layout( + title=f"Percent Change from {start_date} to {end_date}", + xaxis=dict(title="Percent Change"), + yaxis=dict(title="Asset Name"), + ) + + if export: + export_data( + export_type=export, + dir_path=os.path.dirname(os.path.abspath(__file__)), + func_name=f"{ticker}_holdings_perf", + df=data, + sheet_name=sheet_name, + figure=fig, + ) + return + + return fig.show(external= raw or export) From c57a82bed7b6c7d7656399bb9e8f78eee59f56c3 Mon Sep 17 00:00:00 2001 From: nectariosouzo Date: Wed, 27 Sep 2023 14:05:02 -0400 Subject: [PATCH 03/11] Add basic testing for new etf endpoints --- openbb_terminal/etf/fmp_model.py | 5 +- .../etf/test_etf.openbb | 1 + .../test_get_etf_holdings[ARKK].yaml | 48 ++++++++++ .../test_get_etf_holdings[VTI].yaml | 48 ++++++++++ ...ct_change[2023-09-01-2022-09-01-ARKK].yaml | 94 +++++++++++++++++++ ...ce_change[2023-09-01-2022-09-01-TSLA].yaml | 48 ++++++++++ ...rformance[2023-09-01-2022-09-01-TSLA].yaml | 48 ++++++++++ ...pct_change[2023-09-01-2022-09-01-ARKK].csv | 1 + ...ice_change[2023-09-01-2022-09-01-TSLA].csv | 1 + .../test_get_etf_holdings[ARKK].json | 1 + tests/openbb_terminal/etf/test_fmp_model.py | 63 +++++++++++++ tests/openbb_terminal/etf/test_fmp_view.py | 22 +++++ 12 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[ARKK].yaml create mode 100644 tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[VTI].yaml create mode 100644 tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml create mode 100644 tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].yaml create mode 100644 tests/openbb_terminal/etf/cassettes/test_fmp_view/test_view_etf_holdings_performance[2023-09-01-2022-09-01-TSLA].yaml create mode 100644 tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv create mode 100644 tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv create mode 100644 tests/openbb_terminal/etf/json/test_fmp_model/test_get_etf_holdings[ARKK].json diff --git a/openbb_terminal/etf/fmp_model.py b/openbb_terminal/etf/fmp_model.py index 9f7bf49a606..62db201957d 100644 --- a/openbb_terminal/etf/fmp_model.py +++ b/openbb_terminal/etf/fmp_model.py @@ -71,7 +71,6 @@ def get_stock_price_change(ticker: str, start_date: str, end_date: str) -> float """ current_user = get_current_user() url = f'''https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}?from={start_date}&to={end_date}&serietype=line&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}''' - response = request(url) if response.status_code != 200 or "Error Message" in response.json(): message = f"Error, Status Code: {response.status_code}." @@ -117,7 +116,7 @@ def get_etf_holdings(ticker: str) -> List[Dict[str,any]]: else message + "\n" + response.json()["Error Message"] + ".\n" ) console.print(message) - return None + return [] return response.json() @@ -137,6 +136,8 @@ def get_holdings_pct_change(ticker: str, start_date: str, end_date: str) -> pd.D Calculated percentage change for each stock in the ETF, in descending order. """ holdings = get_etf_holdings(ticker=ticker) + if len(holdings) == 0: + return pd.DataFrame() data_list = [] for stock in holdings: if stock['asset']: diff --git a/openbb_terminal/miscellaneous/integration_tests_scripts/etf/test_etf.openbb b/openbb_terminal/miscellaneous/integration_tests_scripts/etf/test_etf.openbb index eee0afe7816..f1376c391dc 100644 --- a/openbb_terminal/miscellaneous/integration_tests_scripts/etf/test_etf.openbb +++ b/openbb_terminal/miscellaneous/integration_tests_scripts/etf/test_etf.openbb @@ -2,6 +2,7 @@ etf load spy overview holdings +holdings_perf --limit 3 weights weights --raw candle diff --git a/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[ARKK].yaml b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[ARKK].yaml new file mode 100644 index 00000000000..8ef804515aa --- /dev/null +++ b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[ARKK].yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://financialmodelingprep.com/api/v3/etf-holder/ARKK?apikey=MOCK_API_KEY + response: + body: + string: "{\n \"Error Message\": \"Invalid API KEY. Please retry or visit our + documentation to create one FREE https://site.financialmodelingprep.com/developer/docs\"\n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - X-Requested-With, content-type, auth-token, Authorization, stripe-signature, + APPS + Access-Control-Allow-Methods: + - GET, POST, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Max-Age: + - '3600' + Connection: + - keep-alive + Content-Length: + - '154' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Sep 2023 14:51:51 GMT + ETag: + - W/"9a-ufKRTAmgqkT6Vv3bqU5trChBRE8" + Server: + - nginx/1.18.0 (Ubuntu) + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[VTI].yaml b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[VTI].yaml new file mode 100644 index 00000000000..a96eaf7e257 --- /dev/null +++ b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_etf_holdings[VTI].yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://financialmodelingprep.com/api/v3/etf-holder/VTI?apikey=MOCK_API_KEY + response: + body: + string: "{\n \"Error Message\": \"Invalid API KEY. Please retry or visit our + documentation to create one FREE https://site.financialmodelingprep.com/developer/docs\"\n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - X-Requested-With, content-type, auth-token, Authorization, stripe-signature, + APPS + Access-Control-Allow-Methods: + - GET, POST, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Max-Age: + - '3600' + Connection: + - keep-alive + Content-Length: + - '154' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Sep 2023 14:40:52 GMT + ETag: + - W/"9a-ufKRTAmgqkT6Vv3bqU5trChBRE8" + Server: + - nginx/1.18.0 (Ubuntu) + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml new file mode 100644 index 00000000000..22cfe40220c --- /dev/null +++ b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml @@ -0,0 +1,94 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://financialmodelingprep.com/api/v3/etf-holder/ARKK?apikey=MOCK_API_KEY + response: + body: + string: "{\n \"Error Message\": \"Invalid API KEY. Please retry or visit our + documentation to create one FREE https://site.financialmodelingprep.com/developer/docs\"\n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - X-Requested-With, content-type, auth-token, Authorization, stripe-signature, + APPS + Access-Control-Allow-Methods: + - GET, POST, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Max-Age: + - '3600' + Connection: + - keep-alive + Content-Length: + - '154' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Sep 2023 17:42:35 GMT + ETag: + - W/"9a-ufKRTAmgqkT6Vv3bqU5trChBRE8" + Server: + - nginx/1.18.0 (Ubuntu) + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + status: + code: 401 + message: Unauthorized +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://financialmodelingprep.com/api/v3/etf-holder/ARKK?apikey=MOCK_API_KEY + response: + body: + string: "{\n \"Error Message\": \"Invalid API KEY. Please retry or visit our + documentation to create one FREE https://site.financialmodelingprep.com/developer/docs\"\n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - X-Requested-With, content-type, auth-token, Authorization, stripe-signature, + APPS + Access-Control-Allow-Methods: + - GET, POST, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Max-Age: + - '3600' + Connection: + - keep-alive + Content-Length: + - '154' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Sep 2023 17:43:14 GMT + ETag: + - W/"9a-ufKRTAmgqkT6Vv3bqU5trChBRE8" + Server: + - nginx/1.18.0 (Ubuntu) + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].yaml b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].yaml new file mode 100644 index 00000000000..eb917028176 --- /dev/null +++ b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://financialmodelingprep.com/api/v3/etf-holder/TSLA?apikey=MOCK_API_KEY + response: + body: + string: "{\n \"Error Message\": \"Invalid API KEY. Please retry or visit our + documentation to create one FREE https://site.financialmodelingprep.com/developer/docs\"\n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - X-Requested-With, content-type, auth-token, Authorization, stripe-signature, + APPS + Access-Control-Allow-Methods: + - GET, POST, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Max-Age: + - '3600' + Connection: + - keep-alive + Content-Length: + - '154' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Sep 2023 17:42:36 GMT + ETag: + - W/"9a-ufKRTAmgqkT6Vv3bqU5trChBRE8" + Server: + - nginx/1.18.0 (Ubuntu) + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/openbb_terminal/etf/cassettes/test_fmp_view/test_view_etf_holdings_performance[2023-09-01-2022-09-01-TSLA].yaml b/tests/openbb_terminal/etf/cassettes/test_fmp_view/test_view_etf_holdings_performance[2023-09-01-2022-09-01-TSLA].yaml new file mode 100644 index 00000000000..12a096f5e88 --- /dev/null +++ b/tests/openbb_terminal/etf/cassettes/test_fmp_view/test_view_etf_holdings_performance[2023-09-01-2022-09-01-TSLA].yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://financialmodelingprep.com/api/v3/etf-holder/TSLA?apikey=MOCK_API_KEY + response: + body: + string: "{\n \"Error Message\": \"Invalid API KEY. Please retry or visit our + documentation to create one FREE https://site.financialmodelingprep.com/developer/docs\"\n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - X-Requested-With, content-type, auth-token, Authorization, stripe-signature, + APPS + Access-Control-Allow-Methods: + - GET, POST, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Max-Age: + - '3600' + Connection: + - keep-alive + Content-Length: + - '154' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Sep 2023 17:52:02 GMT + ETag: + - W/"9a-ufKRTAmgqkT6Vv3bqU5trChBRE8" + Server: + - nginx/1.18.0 (Ubuntu) + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv new file mode 100644 index 00000000000..e16c76dff88 --- /dev/null +++ b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv @@ -0,0 +1 @@ +"" diff --git a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv new file mode 100644 index 00000000000..e16c76dff88 --- /dev/null +++ b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv @@ -0,0 +1 @@ +"" diff --git a/tests/openbb_terminal/etf/json/test_fmp_model/test_get_etf_holdings[ARKK].json b/tests/openbb_terminal/etf/json/test_fmp_model/test_get_etf_holdings[ARKK].json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/tests/openbb_terminal/etf/json/test_fmp_model/test_get_etf_holdings[ARKK].json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/openbb_terminal/etf/test_fmp_model.py b/tests/openbb_terminal/etf/test_fmp_model.py index a542863d3a9..b21403dabdd 100644 --- a/tests/openbb_terminal/etf/test_fmp_model.py +++ b/tests/openbb_terminal/etf/test_fmp_model.py @@ -28,3 +28,66 @@ def test_get_etf_sector_weightings(recorder, name): result = fmp_model.get_etf_sector_weightings(name) recorder.capture(result) + + +@pytest.mark.vcr +@pytest.mark.parametrize( + "ticker", + [ + "ARKK", + ], +) +def test_get_etf_holdings(recorder, ticker): + result = fmp_model.get_etf_holdings(ticker) + + assert result.count != 0 + recorder.capture(result) + +@pytest.mark.vcr(record_mode='all') +@pytest.mark.parametrize( + "ticker", + [ + "ARKK", + ], +) +@pytest.mark.parametrize( + "start_date", + [ + "2022-09-01", + ], +) +@pytest.mark.parametrize( + "end_date", + [ + "2023-09-01", + ], +) +def test_get_holdings_pct_change(recorder, ticker: str, start_date: str, end_date: str): + result = fmp_model.get_holdings_pct_change(ticker, start_date, end_date) + + assert result.empty + recorder.capture(result) + +@pytest.mark.vcr +@pytest.mark.parametrize( + "ticker", + [ + "TSLA", + ], +) +@pytest.mark.parametrize( + "start_date", + [ + "2022-09-01", + ], +) +@pytest.mark.parametrize( + "end_date", + [ + "2023-09-01", + ], +) +def test_get_stock_price_change(recorder, ticker: str, start_date: str, end_date: str): + result = fmp_model.get_holdings_pct_change(ticker, start_date, end_date) + + recorder.capture(result) \ No newline at end of file diff --git a/tests/openbb_terminal/etf/test_fmp_view.py b/tests/openbb_terminal/etf/test_fmp_view.py index 22c7ea30664..21396ef9523 100644 --- a/tests/openbb_terminal/etf/test_fmp_view.py +++ b/tests/openbb_terminal/etf/test_fmp_view.py @@ -24,3 +24,25 @@ def vcr_config(): ) def test_display_etf_weightings(name): fmp_view.display_etf_weightings(name, raw=True, export="") + +@pytest.mark.vcr +@pytest.mark.parametrize( + "ticker", + [ + "TSLA", + ], +) +@pytest.mark.parametrize( + "start_date", + [ + "2022-09-01", + ], +) +@pytest.mark.parametrize( + "end_date", + [ + "2023-09-01", + ], +) +def test_view_etf_holdings_performance(ticker, start_date, end_date): + fmp_view.view_etf_holdings_performance(ticker, start_date, end_date, raw=True, export="") \ No newline at end of file From b932f1cceb49bda678a3a48094f228536ebdda8a Mon Sep 17 00:00:00 2001 From: nectariosouzo Date: Wed, 27 Sep 2023 15:45:18 -0400 Subject: [PATCH 04/11] Reformatting --- openbb_terminal/etf/etf_controller.py | 12 ++-- openbb_terminal/etf/fmp_model.py | 56 +++++++++------ openbb_terminal/etf/fmp_view.py | 72 ++++++++++--------- ...ct_change[2023-09-01-2022-09-01-ARKK].yaml | 46 ------------ tests/openbb_terminal/etf/test_fmp_model.py | 9 +-- tests/openbb_terminal/etf/test_fmp_view.py | 7 +- 6 files changed, 93 insertions(+), 109 deletions(-) diff --git a/openbb_terminal/etf/etf_controller.py b/openbb_terminal/etf/etf_controller.py index 130a070aefb..792179fdb04 100644 --- a/openbb_terminal/etf/etf_controller.py +++ b/openbb_terminal/etf/etf_controller.py @@ -4,7 +4,7 @@ __docformat__ = "numpy" import argparse import logging import os -from datetime import datetime, timedelta, date +from datetime import datetime, timedelta from typing import List, Optional import yfinance as yf @@ -657,7 +657,7 @@ class ETFController(BaseController): type=valid_date, default=(datetime.now().date() - timedelta(days=366)), dest="start", - help="The starting date (format YYYY-MM-DD) to get each holding's price" + help="The starting date (format YYYY-MM-DD) to get each holding's price", ) parser.add_argument( "-e", @@ -665,7 +665,7 @@ class ETFController(BaseController): type=valid_date, default=datetime.now().date(), dest="end", - help="The ending date (format YYYY-MM-DD) to get each holding's price" + help="The ending date (format YYYY-MM-DD) to get each holding's price", ) parser.add_argument( "-l", @@ -678,9 +678,11 @@ class ETFController(BaseController): if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-l") - ns_parser = self.parse_known_args_and_warn(parser, other_args, + ns_parser = self.parse_known_args_and_warn( + parser, + other_args, export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES, - raw=True + raw=True, ) if ns_parser: if self.etf_name: diff --git a/openbb_terminal/etf/fmp_model.py b/openbb_terminal/etf/fmp_model.py index 62db201957d..fa7d7fe0218 100644 --- a/openbb_terminal/etf/fmp_model.py +++ b/openbb_terminal/etf/fmp_model.py @@ -3,9 +3,10 @@ __docformat__ = "numpy" import json import logging -from typing import Dict, List +from typing import Any, Dict, List, Union from urllib.error import HTTPError from urllib.request import urlopen + import pandas as pd from openbb_terminal.core.session.current_user import get_current_user @@ -50,9 +51,12 @@ def get_etf_sector_weightings(name: str) -> Dict: return data + @log_start_end(log=logger) -@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) -def get_stock_price_change(ticker: str, start_date: str, end_date: str) -> float: +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def get_stock_price_change( + ticker: str, start_date: str, end_date: str +) -> Union[None, float]: """Get stock's price percent change over specified time period. Parameters @@ -66,11 +70,13 @@ def get_stock_price_change(ticker: str, start_date: str, end_date: str) -> float Returns ------- - float + Union[None, float] Percent change of closing price over time period """ current_user = get_current_user() - url = f'''https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}?from={start_date}&to={end_date}&serietype=line&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}''' + url = f"""https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}? + from={start_date}&to={end_date}&serietype=line + &apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}""" response = request(url) if response.status_code != 200 or "Error Message" in response.json(): message = f"Error, Status Code: {response.status_code}." @@ -83,15 +89,16 @@ def get_stock_price_change(ticker: str, start_date: str, end_date: str) -> float return None data = response.json() - close_end = data['historical'][0]['close'] - close_start = data['historical'][-1]['close'] + close_end = data["historical"][0]["close"] + close_start = data["historical"][-1]["close"] pct_change = 100 * (close_end - close_start) / close_start return pct_change + @log_start_end(log=logger) -@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) -def get_etf_holdings(ticker: str) -> List[Dict[str,any]]: +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def get_etf_holdings(ticker: str) -> List[Dict[str, Any]]: """This endpoint returns all stocks held by a specific ETF. Parameters @@ -106,7 +113,8 @@ def get_etf_holdings(ticker: str) -> List[Dict[str,any]]: """ current_user = get_current_user() - url = f'''https://financialmodelingprep.com/api/v3/etf-holder/{ticker}?apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}''' + url = f"""https://financialmodelingprep.com/api/v3/etf-holder/{ticker}? + apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}""" response = request(url) if response.status_code != 200 or "Error Message" in response.json(): message = f"Error, Status Code: {response.status_code}." @@ -120,9 +128,12 @@ def get_etf_holdings(ticker: str) -> List[Dict[str,any]]: return response.json() + @log_start_end(log=logger) -@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) -def get_holdings_pct_change(ticker: str, start_date: str, end_date: str) -> pd.DataFrame: +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def get_holdings_pct_change( + ticker: str, start_date: str, end_date: str +) -> pd.DataFrame: """Calculate percent change for each holding in ETF. Parameters @@ -140,16 +151,21 @@ def get_holdings_pct_change(ticker: str, start_date: str, end_date: str) -> pd.D return pd.DataFrame() data_list = [] for stock in holdings: - if stock['asset']: - pct_change = get_stock_price_change(ticker=stock['asset'], - start_date=start_date, end_date=end_date) - data_list.append({"Ticker": stock['asset'], "Name": stock["name"], - "Percent Change": pct_change}) + if stock["asset"]: + pct_change = get_stock_price_change( + ticker=stock["asset"], start_date=start_date, end_date=end_date + ) + data_list.append( + { + "Ticker": stock["asset"], + "Name": stock["name"], + "Percent Change": pct_change, + } + ) else: - data_list.append({"Ticker": '', "Name": stock["name"], - "Percent Change": 0}) + data_list.append({"Ticker": "", "Name": stock["name"], "Percent Change": 0}) df = pd.DataFrame(data_list) - df.sort_values(by='Percent Change',ascending=False, inplace=True) + df.sort_values(by="Percent Change", ascending=False, inplace=True) return df diff --git a/openbb_terminal/etf/fmp_view.py b/openbb_terminal/etf/fmp_view.py index 7d6e77c4edc..b875a0eb9b7 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -10,10 +10,7 @@ import pandas as pd from openbb_terminal import OpenBBFigure, theme from openbb_terminal.decorators import check_api_key, log_start_end from openbb_terminal.etf import fmp_model -from openbb_terminal.helper_funcs import ( - export_data, - print_rich_table, -) +from openbb_terminal.helper_funcs import export_data, print_rich_table from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) @@ -123,26 +120,34 @@ def display_etf_weightings( @log_start_end(log=logger) -@check_api_key(['API_KEY_FINANCIALMODELINGPREP']) -def view_etf_holdings_performance(ticker: str, start_date: str, end_date: str, limit: int = 10, raw: bool = False, export: str = "", sheet_name: Optional[str] = None): - '''Display ETF's holdings' performance over specified time. [Source: FinancialModelingPrep] - Parameters - ---------- - ticker: str - ETF ticker - start_date: str - Date from which data is fetched in format YYYY-MM-DD - end: str - Date from which data is fetched in format YYYY-MM-DD - limit: int - Limit number of holdings to view. Sorted by descending percent change. - raw: bool - Display holding performance - sheet_name: str - Optionally specify the name of the sheet the data is exported to. - export: str - Type of format to export data - ''' +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def view_etf_holdings_performance( + ticker: str, + start_date: str, + end_date: str, + limit: int = 10, + raw: bool = False, + export: str = "", + sheet_name: Optional[str] = None, +): + """Display ETF's holdings' performance over specified time. [Source: FinancialModelingPrep] + Parameters + ---------- + ticker: str + ETF ticker + start_date: str + Date from which data is fetched in format YYYY-MM-DD + end: str + Date from which data is fetched in format YYYY-MM-DD + limit: int + Limit number of holdings to view. Sorted by descending percent change. + raw: bool + Display holding performance + sheet_name: str + Optionally specify the name of the sheet the data is exported to. + export: str + Type of format to export data + """ data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date)[0:limit] if raw: @@ -158,14 +163,15 @@ def view_etf_holdings_performance(ticker: str, start_date: str, end_date: str, l fig = OpenBBFigure() if not raw or fig.is_image_export(export): - fig.add_bar( - hovertext=[f'{x:.2f}'+"%" for x in data['Percent Change']], - x=data['Percent Change'], - y=data['Name'], - name = 'Stock', - orientation='h', - marker_color=['darkgreen' if x > 0 else 'darkred' for x in data['Percent Change']], + hovertext=[f"{x:.2f}" + "%" for x in data["Percent Change"]], + x=data["Percent Change"], + y=data["Name"], + name="Stock", + orientation="h", + marker_color=[ + "darkgreen" if x > 0 else "darkred" for x in data["Percent Change"] + ], ) fig.update_layout( @@ -185,4 +191,6 @@ def view_etf_holdings_performance(ticker: str, start_date: str, end_date: str, l ) return - return fig.show(external= raw or export) + fig.show(external=raw or bool(export)) + + return diff --git a/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml index 22cfe40220c..8858b828d78 100644 --- a/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml +++ b/tests/openbb_terminal/etf/cassettes/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].yaml @@ -45,50 +45,4 @@ interactions: status: code: 401 message: Unauthorized -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - method: GET - uri: https://financialmodelingprep.com/api/v3/etf-holder/ARKK?apikey=MOCK_API_KEY - response: - body: - string: "{\n \"Error Message\": \"Invalid API KEY. Please retry or visit our - documentation to create one FREE https://site.financialmodelingprep.com/developer/docs\"\n}" - headers: - Access-Control-Allow-Credentials: - - 'true' - Access-Control-Allow-Headers: - - X-Requested-With, content-type, auth-token, Authorization, stripe-signature, - APPS - Access-Control-Allow-Methods: - - GET, POST, OPTIONS - Access-Control-Allow-Origin: - - '*' - Access-Control-Max-Age: - - '3600' - Connection: - - keep-alive - Content-Length: - - '154' - Content-Type: - - application/json; charset=utf-8 - Date: - - Wed, 27 Sep 2023 17:43:14 GMT - ETag: - - W/"9a-ufKRTAmgqkT6Vv3bqU5trChBRE8" - Server: - - nginx/1.18.0 (Ubuntu) - X-Frame-Options: - - SAMEORIGIN - X-Powered-By: - - Express - status: - code: 401 - message: Unauthorized version: 1 diff --git a/tests/openbb_terminal/etf/test_fmp_model.py b/tests/openbb_terminal/etf/test_fmp_model.py index b21403dabdd..3b6ef6dd919 100644 --- a/tests/openbb_terminal/etf/test_fmp_model.py +++ b/tests/openbb_terminal/etf/test_fmp_model.py @@ -43,7 +43,7 @@ def test_get_etf_holdings(recorder, ticker): assert result.count != 0 recorder.capture(result) -@pytest.mark.vcr(record_mode='all') + @pytest.mark.parametrize( "ticker", [ @@ -51,7 +51,7 @@ def test_get_etf_holdings(recorder, ticker): ], ) @pytest.mark.parametrize( - "start_date", + "start_date", [ "2022-09-01", ], @@ -68,6 +68,7 @@ def test_get_holdings_pct_change(recorder, ticker: str, start_date: str, end_dat assert result.empty recorder.capture(result) + @pytest.mark.vcr @pytest.mark.parametrize( "ticker", @@ -76,7 +77,7 @@ def test_get_holdings_pct_change(recorder, ticker: str, start_date: str, end_dat ], ) @pytest.mark.parametrize( - "start_date", + "start_date", [ "2022-09-01", ], @@ -90,4 +91,4 @@ def test_get_holdings_pct_change(recorder, ticker: str, start_date: str, end_dat def test_get_stock_price_change(recorder, ticker: str, start_date: str, end_date: str): result = fmp_model.get_holdings_pct_change(ticker, start_date, end_date) - recorder.capture(result) \ No newline at end of file + recorder.capture(result) diff --git a/tests/openbb_terminal/etf/test_fmp_view.py b/tests/openbb_terminal/etf/test_fmp_view.py index 21396ef9523..351a825f31d 100644 --- a/tests/openbb_terminal/etf/test_fmp_view.py +++ b/tests/openbb_terminal/etf/test_fmp_view.py @@ -25,6 +25,7 @@ def vcr_config(): def test_display_etf_weightings(name): fmp_view.display_etf_weightings(name, raw=True, export="") + @pytest.mark.vcr @pytest.mark.parametrize( "ticker", @@ -33,7 +34,7 @@ def test_display_etf_weightings(name): ], ) @pytest.mark.parametrize( - "start_date", + "start_date", [ "2022-09-01", ], @@ -45,4 +46,6 @@ def test_display_etf_weightings(name): ], ) def test_view_etf_holdings_performance(ticker, start_date, end_date): - fmp_view.view_etf_holdings_performance(ticker, start_date, end_date, raw=True, export="") \ No newline at end of file + fmp_view.view_etf_holdings_performance( + ticker, start_date, end_date, raw=True, export="" + ) From 83d46a55bf46c95f0200b82b8c913de22bc541cf Mon Sep 17 00:00:00 2001 From: nectariosouzo Date: Wed, 27 Sep 2023 19:34:56 -0400 Subject: [PATCH 05/11] Fix url issue and change logic for edge cases --- openbb_terminal/etf/fmp_model.py | 50 +++++++++---------- openbb_terminal/etf/fmp_view.py | 2 +- ...pct_change[2023-09-01-2022-09-01-ARKK].csv | 2 +- ...ice_change[2023-09-01-2022-09-01-TSLA].csv | 2 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/openbb_terminal/etf/fmp_model.py b/openbb_terminal/etf/fmp_model.py index fa7d7fe0218..4315d3dbacd 100644 --- a/openbb_terminal/etf/fmp_model.py +++ b/openbb_terminal/etf/fmp_model.py @@ -74,9 +74,10 @@ def get_stock_price_change( Percent change of closing price over time period """ current_user = get_current_user() - url = f"""https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}? - from={start_date}&to={end_date}&serietype=line - &apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}""" + + url = f"""https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}?\ +from={start_date}&to={end_date}&serietype=line\ +&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}""" response = request(url) if response.status_code != 200 or "Error Message" in response.json(): message = f"Error, Status Code: {response.status_code}." @@ -113,8 +114,8 @@ def get_etf_holdings(ticker: str) -> List[Dict[str, Any]]: """ current_user = get_current_user() - url = f"""https://financialmodelingprep.com/api/v3/etf-holder/{ticker}? - apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}""" + url = f"""https://financialmodelingprep.com/api/v3/etf-holder/{ticker}\ +?apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}""" response = request(url) if response.status_code != 200 or "Error Message" in response.json(): message = f"Error, Status Code: {response.status_code}." @@ -146,26 +147,25 @@ def get_holdings_pct_change( pd.DataFrame Calculated percentage change for each stock in the ETF, in descending order. """ - holdings = get_etf_holdings(ticker=ticker) - if len(holdings) == 0: - return pd.DataFrame() - data_list = [] + df = pd.DataFrame(columns=['Ticker', 'Name', 'Percent Change'], data=[]) + holdings = get_etf_holdings(ticker) + for stock in holdings: - if stock["asset"]: - pct_change = get_stock_price_change( - ticker=stock["asset"], start_date=start_date, end_date=end_date - ) - data_list.append( - { - "Ticker": stock["asset"], - "Name": stock["name"], - "Percent Change": pct_change, - } - ) - else: - data_list.append({"Ticker": "", "Name": stock["name"], "Percent Change": 0}) + pct_change = get_stock_price_change(stock["asset"], start_date, end_date) + if pct_change is None: + pct_change = 0 - df = pd.DataFrame(data_list) - df.sort_values(by="Percent Change", ascending=False, inplace=True) + new_df = pd.DataFrame( + { + "Ticker": stock["asset"], + "Name": stock["name"], + "Percent Change": pct_change, + }, + index=[0], + ) - return df + df = pd.concat([df, new_df], ignore_index=True) + + sorted_df = df.sort_values(by="Percent Change", ascending=False, inplace=False) + + return sorted_df diff --git a/openbb_terminal/etf/fmp_view.py b/openbb_terminal/etf/fmp_view.py index b875a0eb9b7..a0e5529ca38 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -148,7 +148,7 @@ def view_etf_holdings_performance( export: str Type of format to export data """ - data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date)[0:limit] + data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date).head(limit) if raw: print_rich_table( diff --git a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv index e16c76dff88..09bd5b648a0 100644 --- a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv +++ b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv @@ -1 +1 @@ -"" +,Ticker,Name,Percent Change diff --git a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv index e16c76dff88..09bd5b648a0 100644 --- a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv +++ b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_stock_price_change[2023-09-01-2022-09-01-TSLA].csv @@ -1 +1 @@ -"" +,Ticker,Name,Percent Change From e1bbe19754f7f683590b7b2020a3e35ca4621060 Mon Sep 17 00:00:00 2001 From: nectariosouzo Date: Wed, 27 Sep 2023 19:41:49 -0400 Subject: [PATCH 06/11] Reformat --- openbb_terminal/etf/fmp_model.py | 16 ++++++++-------- openbb_terminal/etf/fmp_view.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openbb_terminal/etf/fmp_model.py b/openbb_terminal/etf/fmp_model.py index 4315d3dbacd..e9aa69bbe85 100644 --- a/openbb_terminal/etf/fmp_model.py +++ b/openbb_terminal/etf/fmp_model.py @@ -147,7 +147,7 @@ def get_holdings_pct_change( pd.DataFrame Calculated percentage change for each stock in the ETF, in descending order. """ - df = pd.DataFrame(columns=['Ticker', 'Name', 'Percent Change'], data=[]) + df = pd.DataFrame(columns=["Ticker", "Name", "Percent Change"], data=[]) holdings = get_etf_holdings(ticker) for stock in holdings: @@ -156,13 +156,13 @@ def get_holdings_pct_change( pct_change = 0 new_df = pd.DataFrame( - { - "Ticker": stock["asset"], - "Name": stock["name"], - "Percent Change": pct_change, - }, - index=[0], - ) + { + "Ticker": stock["asset"], + "Name": stock["name"], + "Percent Change": pct_change, + }, + index=[0], + ) df = pd.concat([df, new_df], ignore_index=True) diff --git a/openbb_terminal/etf/fmp_view.py b/openbb_terminal/etf/fmp_view.py index a0e5529ca38..90c2f8e46d8 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -175,7 +175,7 @@ def view_etf_holdings_performance( ) fig.update_layout( - title=f"Percent Change from {start_date} to {end_date}", + title=f"Percent Change in Price for Each Holding from {start_date} to {end_date} for {ticker}", xaxis=dict(title="Percent Change"), yaxis=dict(title="Asset Name"), ) From 4836170cf0ff4d45c5c7797f1f8c332af94ff44a Mon Sep 17 00:00:00 2001 From: nectariosouzo Date: Mon, 2 Oct 2023 19:20:12 -0400 Subject: [PATCH 07/11] Add progress bar and get bulk prices --- openbb_terminal/etf/fmp_model.py | 102 +++++++++++++++++++++---------- openbb_terminal/etf/fmp_view.py | 12 ++-- 2 files changed, 75 insertions(+), 39 deletions(-) diff --git a/openbb_terminal/etf/fmp_model.py b/openbb_terminal/etf/fmp_model.py index e9aa69bbe85..39720586efc 100644 --- a/openbb_terminal/etf/fmp_model.py +++ b/openbb_terminal/etf/fmp_model.py @@ -3,7 +3,7 @@ __docformat__ = "numpy" import json import logging -from typing import Any, Dict, List, Union +from typing import Any, Dict, List from urllib.error import HTTPError from urllib.request import urlopen @@ -12,7 +12,7 @@ import pandas as pd from openbb_terminal.core.session.current_user import get_current_user from openbb_terminal.decorators import check_api_key, log_start_end from openbb_terminal.helper_funcs import request -from openbb_terminal.rich_config import console +from openbb_terminal.rich_config import console, optional_rich_track logger = logging.getLogger(__name__) @@ -55,14 +55,14 @@ def get_etf_sector_weightings(name: str) -> Dict: @log_start_end(log=logger) @check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) def get_stock_price_change( - ticker: str, start_date: str, end_date: str -) -> Union[None, float]: + tickers: List[str], start_date: str, end_date: str +) -> Dict[str, float]: """Get stock's price percent change over specified time period. Parameters ---------- - ticker : str - Ticker to get information from. + tickers : List[str] + Ticker(s) to get information for. start: str Date from which data is fetched in format YYYY-MM-DD end: str @@ -70,42 +70,65 @@ def get_stock_price_change( Returns ------- - Union[None, float] - Percent change of closing price over time period + Dict[str, float] + Percent change of closing price over time period, or dictionary of ticker, change pairs. """ + tickers_tracker = optional_rich_track( + tickers, False, "Gathering stock prices", len(tickers) + ) current_user = get_current_user() + data_aggregate = dict() - url = f"""https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}?\ + for tick in tickers_tracker: + tickers_req = str(tick) + "," + for _ in range(4): + try: + _tick = next(tickers_tracker) + tickers_req += _tick + "," + except StopIteration: + break + + url = f"""https://financialmodelingprep.com/api/v3/historical-price-full/{tickers_req}?\ from={start_date}&to={end_date}&serietype=line\ &apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}""" - response = request(url) - if response.status_code != 200 or "Error Message" in response.json(): - message = f"Error, Status Code: {response.status_code}." - message = ( - message - if "Error Message" not in response.json() - else message + "\n" + response.json()["Error Message"] + ".\n" - ) - console.print(message) - return None - data = response.json() - close_end = data["historical"][0]["close"] - close_start = data["historical"][-1]["close"] - pct_change = 100 * (close_end - close_start) / close_start + response = request(url) + if response.status_code != 200 or "Error Message" in response.json(): + message = f"Error, Status Code: {response.status_code}." + message = ( + message + if "Error Message" not in response.json() + else message + "\n" + response.json()["Error Message"] + ".\n" + ) + console.print(message) + return dict() - return pct_change + data = response.json() + stock_list = data + if "historicalStockList" in data: + stock_list = data["historicalStockList"] + + for stock in stock_list: + close_end = stock["historical"][0]["close"] + close_start = stock["historical"][-1]["close"] + pct_change = 100 * (close_end - close_start) / close_start + data_aggregate[stock["symbol"]] = pct_change + + return data_aggregate @log_start_end(log=logger) @check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) -def get_etf_holdings(ticker: str) -> List[Dict[str, Any]]: +def get_etf_holdings(ticker: str, limit: int = 10) -> List[Dict[str, Any]]: """This endpoint returns all stocks held by a specific ETF. Parameters ---------- ticker : str ETF ticker. + limit: int + Limit amount of stocks to return. FMP returns data + by descending weighting. Returns ------- @@ -127,13 +150,16 @@ def get_etf_holdings(ticker: str) -> List[Dict[str, Any]]: console.print(message) return [] - return response.json() + return response.json()[0:limit] @log_start_end(log=logger) @check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) def get_holdings_pct_change( - ticker: str, start_date: str, end_date: str + ticker: str, + start_date: str, + end_date: str, + limit: int = 10, ) -> pd.DataFrame: """Calculate percent change for each holding in ETF. @@ -141,25 +167,35 @@ def get_holdings_pct_change( ---------- ticker : str ETF ticker. + limit: int + Limit amount of stocks to return. FMP returns data + by descending weighting. Returns ------- pd.DataFrame Calculated percentage change for each stock in the ETF, in descending order. """ + df = pd.DataFrame(columns=["Ticker", "Name", "Percent Change"], data=[]) - holdings = get_etf_holdings(ticker) + holdings = get_etf_holdings(ticker, limit) + tickers = [] + for stock in holdings: + tickers.append(stock.get("asset", " ")) + + pct_changes = get_stock_price_change(tickers, start_date, end_date) for stock in holdings: - pct_change = get_stock_price_change(stock["asset"], start_date, end_date) - if pct_change is None: - pct_change = 0 - + pct_change = pct_changes.get(stock["asset"], 0) + if pct_change == 0: + console.print( + f"""Percent change not found for: {stock["asset"]}: {stock["name"]}""" + ) new_df = pd.DataFrame( { "Ticker": stock["asset"], "Name": stock["name"], - "Percent Change": pct_change, + "Percent Change": pct_changes.get(stock["asset"], 0), }, index=[0], ) diff --git a/openbb_terminal/etf/fmp_view.py b/openbb_terminal/etf/fmp_view.py index 90c2f8e46d8..14598cdbbbc 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -134,21 +134,21 @@ def view_etf_holdings_performance( Parameters ---------- ticker: str - ETF ticker + ETF ticker. start_date: str - Date from which data is fetched in format YYYY-MM-DD + Date from which data is fetched in format YYYY-MM-DD. end: str - Date from which data is fetched in format YYYY-MM-DD + Date from which data is fetched in format YYYY-MM-DD. limit: int - Limit number of holdings to view. Sorted by descending percent change. + Limit number of holdings to view. Sorted by holding percentage (desc). raw: bool Display holding performance sheet_name: str Optionally specify the name of the sheet the data is exported to. export: str - Type of format to export data + Type of format to export data. """ - data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date).head(limit) + data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date, limit) if raw: print_rich_table( From 6406451d73c51c78bc72bfe44bb47e2e39105e72 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Tue, 3 Oct 2023 10:09:47 -0400 Subject: [PATCH 08/11] Reverse chart and change params to start-date and end-date --- openbb_terminal/etf/etf_controller.py | 4 ++-- openbb_terminal/etf/fmp_view.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/etf/etf_controller.py b/openbb_terminal/etf/etf_controller.py index 792179fdb04..589829f2802 100644 --- a/openbb_terminal/etf/etf_controller.py +++ b/openbb_terminal/etf/etf_controller.py @@ -653,7 +653,7 @@ class ETFController(BaseController): ) parser.add_argument( "-s", - "--start", + "--start-date", type=valid_date, default=(datetime.now().date() - timedelta(days=366)), dest="start", @@ -661,7 +661,7 @@ class ETFController(BaseController): ) parser.add_argument( "-e", - "--end", + "--end-date", type=valid_date, default=datetime.now().date(), dest="end", diff --git a/openbb_terminal/etf/fmp_view.py b/openbb_terminal/etf/fmp_view.py index 14598cdbbbc..42f02cc93e2 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -148,7 +148,7 @@ def view_etf_holdings_performance( export: str Type of format to export data. """ - data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date, limit) + data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date, limit)[::-1] if raw: print_rich_table( From f319b2ab7c296191d22635b8271f4d6084b258ee Mon Sep 17 00:00:00 2001 From: James Maslek Date: Tue, 3 Oct 2023 10:14:45 -0400 Subject: [PATCH 09/11] default data source --- openbb_terminal/miscellaneous/sources/openbb_default.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/miscellaneous/sources/openbb_default.json b/openbb_terminal/miscellaneous/sources/openbb_default.json index 0a440cfd03c..4a97941bd7b 100644 --- a/openbb_terminal/miscellaneous/sources/openbb_default.json +++ b/openbb_terminal/miscellaneous/sources/openbb_default.json @@ -504,7 +504,8 @@ "holdings": ["StockAnalysis"], "weights": ["FinancialModelingPrep"], "news": ["NewsApi"], - "compare": ["StockAnalysis"] + "compare": ["StockAnalysis"], + "holding_perf": ["FinancialModelingPrep"] }, "economy": { "overview": ["WallStreetJournal"], From 6237bab7a9be38c6b2c8f774ce226a51b8e1c821 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Tue, 3 Oct 2023 10:17:50 -0400 Subject: [PATCH 10/11] pin codespell on action --- .github/workflows/linting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index c131fdf5294..37178c9c315 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -44,7 +44,7 @@ jobs: restore-keys: ${{ runner.os }}-linting-${{ hashFiles('**/poetry.lock') }} - run: | - pip install bandit black codespell mypy==1.5.1 pylint==2.17.0 ruff==0.0.285 + pip install bandit black codespell==2.2.5 mypy==1.5.1 pylint==2.17.0 ruff==0.0.285 pip install types-pytz types-requests types-termcolor types-tabulate types-PyYAML types-python-dateutil types-setuptools types-six - run: bandit -x ./tests -r . || true - run: black --diff --check . From db5e198a34e08e47340af666840079409d8ab09d Mon Sep 17 00:00:00 2001 From: James Maslek Date: Tue, 3 Oct 2023 10:29:31 -0400 Subject: [PATCH 11/11] tests --- ...holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv | 10 ++++++++++ tests/openbb_terminal/etf/test_fmp_model.py | 2 +- .../etf/txt/test_etf_controller/test_print_help.txt | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv index 09bd5b648a0..1bd705edd57 100644 --- a/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv +++ b/tests/openbb_terminal/etf/csv/test_fmp_model/test_get_holdings_pct_change[2023-09-01-2022-09-01-ARKK].csv @@ -1 +1,11 @@ ,Ticker,Name,Percent Change +4,EXAS,EXACT SCIENCES CORP,134.98896247240614 +7,SHOP,SHOPIFY INC - CLASS A,113.33546530220659 +9,DKNG,DRAFTKINGS INC-CL A,85.71428571428571 +2,ROKU,ROKU INC,19.648609077598834 +3,COIN,COINBASE GLOBAL INC -CLASS A,19.01419197314206 +6,PATH,UIPATH INC - CLASS A,-1.670792079207918 +0,TSLA,TESLA INC,-11.599797950642241 +1,ZM,ZOOM VIDEO COMMUNICATIONS-A,-11.609563717032293 +5,SQ,BLOCK INC,-15.389090909090905 +8,TDOC,TELADOC HEALTH INC,-25.746753246753247 diff --git a/tests/openbb_terminal/etf/test_fmp_model.py b/tests/openbb_terminal/etf/test_fmp_model.py index 3b6ef6dd919..868b4f35dbf 100644 --- a/tests/openbb_terminal/etf/test_fmp_model.py +++ b/tests/openbb_terminal/etf/test_fmp_model.py @@ -65,7 +65,7 @@ def test_get_etf_holdings(recorder, ticker): def test_get_holdings_pct_change(recorder, ticker: str, start_date: str, end_date: str): result = fmp_model.get_holdings_pct_change(ticker, start_date, end_date) - assert result.empty + assert not result.empty recorder.capture(result) diff --git a/tests/openbb_terminal/etf/txt/test_etf_controller/test_print_help.txt b/tests/openbb_terminal/etf/txt/test_etf_controller/test_print_help.txt index e469922e3cc..ef790344a67 100644 --- a/tests/openbb_terminal/etf/txt/test_etf_controller/test_print_help.txt +++ b/tests/openbb_terminal/etf/txt/test_etf_controller/test_print_help.txt @@ -9,6 +9,7 @@ Major holdings: [unvl] overview get overview[/unvl] [StockAnalysis] [unvl] holdings top company holdings[/unvl] [StockAnalysis] +[unvl] holding_perf Performance of holdings in ETF.[/unvl] [FinancialModelingPrep] [unvl] weights sector weights allocation[/unvl] [FinancialModelingPrep] [unvl] news latest news of the company[/unvl] [NewsApi] [unvl] candle view a candle chart for ETF[/unvl]