Merge branch 'develop' into release/3.2.3

This commit is contained in:
James Maslek
2023-10-03 10:31:14 -04:00
17 changed files with 646 additions and 9 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -2,6 +2,7 @@ etf
load spy
overview
holdings
holdings_perf --limit 3
weights
weights --raw
candle

View File

@@ -504,7 +504,8 @@
"holdings": ["StockAnalysis"],
"weights": ["FinancialModelingPrep"],
"news": ["NewsApi"],
"compare": ["StockAnalysis"]
"compare": ["StockAnalysis"],
"holding_perf": ["FinancialModelingPrep"]
},
"economy": {
"overview": ["WallStreetJournal"],

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +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
1 Ticker Name Percent Change
2 4 EXAS EXACT SCIENCES CORP 134.98896247240614
3 7 SHOP SHOPIFY INC - CLASS A 113.33546530220659
4 9 DKNG DRAFTKINGS INC-CL A 85.71428571428571
5 2 ROKU ROKU INC 19.648609077598834
6 3 COIN COINBASE GLOBAL INC -CLASS A 19.01419197314206
7 6 PATH UIPATH INC - CLASS A -1.670792079207918
8 0 TSLA TESLA INC -11.599797950642241
9 1 ZM ZOOM VIDEO COMMUNICATIONS-A -11.609563717032293
10 5 SQ BLOCK INC -15.389090909090905
11 8 TDOC TELADOC HEALTH INC -25.746753246753247

View File

@@ -0,0 +1 @@
,Ticker,Name,Percent Change
1 Ticker Name Percent Change

View File

@@ -0,0 +1 @@
[]

View File

@@ -28,3 +28,67 @@ 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.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 not 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)

View File

@@ -24,3 +24,28 @@ 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=""
)

View File

@@ -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]