diff options
author | James Maslek <jmaslek11@gmail.com> | 2023-10-03 10:31:14 -0400 |
---|---|---|
committer | James Maslek <jmaslek11@gmail.com> | 2023-10-03 10:31:14 -0400 |
commit | 71dff15965cd1fbabf9f82c2d3a0b0141d85093b (patch) | |
tree | 8757b611f8c8f858bd2f980dae51adf78ddc53c0 | |
parent | 580db7dc08ae1485cc29ab6466634bacaeeff7c1 (diff) | |
parent | c23f2c8b73d71cb87186b66d38975ab9772dd0e8 (diff) |
Merge branch 'develop' into release/3.2.3
17 files changed, 646 insertions, 9 deletions
diff --git a/openbb_terminal/etf/etf_controller.py b/openbb_terminal/etf/etf_controller.py index 26a4311208e..589829f2802 100644 --- a/openbb_terminal/etf/etf_controller.py +++ b/openbb_terminal/etf/etf_controller.py @@ -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,60 @@ class ETFController(BaseController): if ns_parser.sheet_name else None, ) + + @log_start_end(log=logger) + def call_holding_perf(self, other_args: List[str]): + """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-date", + 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-date", + 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_allowed=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, + export=ns_parser.export, + sheet_name=ns_parser.sheet_name, + raw=ns_parser.raw, + ) + else: + console.print("Please load a ticker using <load name>. \n") diff --git a/openbb_terminal/etf/fmp_model.py b/openbb_terminal/etf/fmp_model.py index 26b8cfc99db..39720586efc 100644 --- a/openbb_terminal/etf/fmp_model.py +++ b/openbb_terminal/etf/fmp_model.py @@ -3,13 +3,16 @@ __docformat__ = "numpy" import json import logging -from typing import Dict +from typing import Any, 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.rich_config import console +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, optional_rich_track logger = logging.getLogger(__name__) @@ -47,3 +50,158 @@ 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( + tickers: List[str], start_date: str, end_date: str +) -> Dict[str, float]: + """Get stock's price percent change over specified time period. + + Parameters + ---------- + tickers : List[str] + Ticker(s) to get information for. + 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 + ------- + 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() + + 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 dict() + + 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, 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 + ------- + 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 [] + + 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, + limit: int = 10, +) -> pd.DataFrame: + """Calculate percent change for each holding in ETF. + + Parameters + ---------- + 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, 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 = 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_changes.get(stock["asset"], 0), + }, + index=[0], + ) + + 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 6499aa3fc96..42f02cc93e2 100644 --- a/openbb_terminal/etf/fmp_view.py +++ b/openbb_terminal/etf/fmp_view.py @@ -8,12 +8,9 @@ from typing import Optional import pandas as pd 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, - 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__) @@ -120,3 +117,80 @@ 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, +): + """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 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. + """ + data = fmp_model.get_holdings_pct_change(ticker, start_date, end_date, limit)[::-1] + + if raw: + print_rich_table( + data, + show_index=False, + headers=["Ticker", "Name", "Percent Change"], + title="ETF Holdings' Performance", + limit=limit, + export=bool(export), + ) + + 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"] + ], + ) + + fig.update_layout( + 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"), + ) + + 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 + + fig.show(external=raw or bool(export)) + + return 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 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/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"], 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..8858b828d78 --- /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,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 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 +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: |