Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add etf holding performance #5488

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions openbb_terminal/etf/etf_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class ETFController(BaseController):
"load",
"overview",
"holdings",
"holding_perf",
"news",
"candle",
"weights",
Expand Down Expand Up @@ -106,6 +107,7 @@ def print_help(self):
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)
Expand Down Expand Up @@ -638,3 +640,60 @@ def call_compare(self, other_args):
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")
164 changes: 161 additions & 3 deletions openbb_terminal/etf/fmp_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@

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__)

Expand Down Expand Up @@ -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
84 changes: 79 additions & 5 deletions openbb_terminal/etf/fmp_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@
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__)
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions openbb_terminal/miscellaneous/i18n/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ etf
load spy
overview
holdings
holdings_perf --limit 3
weights
weights --raw
candle
Expand Down
Loading
Loading