From 655b1c861fc38ff386b3d5c8b03d1d6214a0741a Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Sun, 17 Mar 2024 06:42:20 -0700 Subject: [PATCH] [Docs] Add Streamlit News Dashboard To Examples (#6158) * add streamlit news dashboard example * black * ruff * now black again * import block? * ruff puts it back to exactly how it was before * try running linters again * noqa it is --------- Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> Co-authored-by: James Maslek --- examples/README.md | 16 + examples/content.json | 6 + examples/streamlit/news.py | 452 ++++++++++++++++++++++++++++ examples/streamlit/requirements.txt | 3 + examples/streamlit_news.webp | Bin 0 -> 9448 bytes 5 files changed, 477 insertions(+) create mode 100644 examples/streamlit/news.py create mode 100644 examples/streamlit/requirements.txt create mode 100644 examples/streamlit_news.webp diff --git a/examples/README.md b/examples/README.md index b50fc62499d..e7d5a583189 100644 --- a/examples/README.md +++ b/examples/README.md @@ -77,3 +77,19 @@ This notebook demonstrates how to calculate the implied earnings move using opti - Get the last price of the underlying stock. - Find the nearest call and put strikes to the last price of the stock. - Calculate the implied daily move using the price of a straddle. + +### streamlit/news + +This is an example Streamlit dashboard for news headlines with data from Biztoc, Benzinga, FMP, Intrinio, and Tiingo. + +:::warning +At least one API key is required. You can get a free Biztoc API key [here](https://rapidapi.com/thma/api/biztoc) +::: + +To run, copy the file to your system, open a terminal, navigate to where the file is, and with your `obb` Python environment active, enter: + +``` +pip install streamlit +pip install openbb-biztoc +streamlit run news.py +``` diff --git a/examples/content.json b/examples/content.json index fd257a4a4be..ff855e3e74e 100644 --- a/examples/content.json +++ b/examples/content.json @@ -47,5 +47,11 @@ "img": "https://raw.githubusercontent.com/OpenBB-finance/OpenBBTerminal/develop/examples/impliedEarningsMove.webp", "description": "Calculate the implied earnings move using options prices. This notebook demonstrates how to get the data from free sources and apply filters to arrive at the expected move, as a percent, in either direction." + }, + { + "title": "Streamlit News Headlines", + "url": "https://github.com/OpenBB-finance/OpenBBTerminal/blob/develop/examples/streamlit/news.py", + "img": "https://raw.githubusercontent.com/OpenBB-finance/OpenBBTerminal/develop/examples/streamlit_news.webp", + "description": "An example Streamlit dashboard for news headlines with data from Biztoc, Benzinga, FMP, Intrinio, and Tiingo." } ] diff --git a/examples/streamlit/news.py b/examples/streamlit/news.py new file mode 100644 index 00000000000..d4113a4dfcd --- /dev/null +++ b/examples/streamlit/news.py @@ -0,0 +1,452 @@ +"""Streamlit News Page""" + +# flake8: noqa: I001 + +from datetime import datetime, timedelta + +from openbb import obb +from openbb_biztoc.utils.helpers import get_all_tags, get_sources + +import streamlit as st + +st.set_page_config( + layout="wide", + page_title="News", + initial_sidebar_state="expanded", +) + +st.sidebar.markdown( + """ + +
+ +
Powered by Open Source
+
+""", + unsafe_allow_html=True, +) + + +button_pressed = False + +SUPPORTED_SOURCES = ["benzinga", "biztoc", "intrinio", "fmp", "tiingo"] + +providers = [ + d + for d in list(obb.user.credentials.__dict__.keys()) # type: ignore + if obb.user.credentials.__dict__[d] is not None # type: ignore +] +providers = [d.split("_")[0] for d in providers if d.split("_")[0] in SUPPORTED_SOURCES] +news_sources = [d.upper() if d == "fmp" else d.title() for d in providers] + +if "news" not in st.session_state: + st.session_state.news = None +if "biztoc_tags" not in st.session_state: + st.session_state.biztoc_tags = [] +try: + _all_tags = [] + _biztoc_tags = get_all_tags(api_key=obb.user.credentials.biztoc_api_key.get_secret_value()) # type: ignore + for key, value in _biztoc_tags.items(): + _all_tags.extend(_biztoc_tags[key]) + st.session_state.biztoc_tags = _all_tags +except Exception: + st.session_state.biztoc_tags = [] +try: + _sources = get_sources(api_key=obb.user.credentials.biztoc_api_key.get_secret_value()) # type: ignore + st.session_state.biztoc_sources = [d["id"] for d in _sources] +except Exception: + st.session_state.biztoc_sources = [] + +if "biztoc_sources" not in st.session_state: + st.session_state.biztoc_sources = [] +if "news_container" not in st.session_state: + st.session_state.news_container = st.empty() +if "selected_limit" not in st.session_state: + st.session_state.selected_limit = 100 +if "selected_provider" not in st.session_state: + if len(news_sources) == 0: + st.error( + f"No news sources available. Please check your credentials for one of: {SUPPORTED_SOURCES}" + ) + st.stop() + if len(news_sources) > 0: + st.session_state.selected_provider = ( + "Biztoc" if "Biztoc" in news_sources else news_sources[0] + ) +if "selected_tags" not in st.session_state: + st.session_state.selected_tags = "" +if "selected_term" not in st.session_state: + st.session_state.selected_term = "" +if "news_start_date" not in st.session_state: + st.session_state.news_start_date = (datetime.now() - timedelta(days=2)).date() +if "news_end_date" not in st.session_state: + st.session_state.news_end_date = datetime.now().date() +if "selected_biztoc_source" not in st.session_state: + st.session_state.selected_biztoc_source = "" +if "content_type" not in st.session_state: + st.session_state.content_type = "news" +if "benzinga_tickers" not in st.session_state: + st.session_state.benzinga_tickers = "" +if "selected_benzinga_channel" not in st.session_state: + st.session_state.selected_benzinga_channel = "" +if "fmp_tickers" not in st.session_state: + st.session_state.fmp_tickers = "" +if "intrinio_tickers" not in st.session_state: + st.session_state.intrinio_tickers = "" +if "tiingo_tickers" not in st.session_state: + st.session_state.tiingo_tickers = "" +if "tiingo_source" not in st.session_state: + st.session_state.tiingo_source = "" + + +def fetch_openbb(): + kwargs = { + "provider": st.session_state.selected_provider.lower(), + "limit": st.session_state.selected_limit, + } + if st.session_state.selected_provider == "Benzinga": + kwargs["start_date"] = st.session_state.news_start_date.strftime("%Y-%m-%d") + kwargs["end_date"] = st.session_state.news_end_date.strftime("%Y-%m-%d") + kwargs["topics"] = st.session_state.selected_tags + kwargs["display"] = "full" + kwargs["page_size"] = 100 + kwargs["channels"] = ( + st.session_state.selected_benzinga_channel.lower() + if st.session_state.selected_benzinga_channel + else None + ) + kwargs["symbol"] = ( + st.session_state.benzinga_tickers + if st.session_state.benzinga_tickers + else None + ) + + if st.session_state.selected_provider == "Biztoc": + kwargs["term"] = st.session_state.selected_term + kwargs["tag"] = st.session_state.selected_tags + kwargs["filter"] = "tag" if kwargs.get("tag") else None + if kwargs.get("filter") is None: + kwargs["filter"] = "latest" + kwargs["source"] = st.session_state.selected_biztoc_source + kwargs["filter"] = "source" if kwargs.get("source") else kwargs.get("filter") + if kwargs.get("filter") == "source": + kwargs.pop("tag") + + if st.session_state.selected_provider == "FMP": + kwargs["symbol"] = ( + st.session_state.fmp_tickers if st.session_state.fmp_tickers else None + ) + + if st.session_state.selected_provider == "Intrinio": + kwargs["symbol"] = ( + st.session_state.intrinio_tickers + if st.session_state.intrinio_tickers + else None + ) + + if st.session_state.selected_provider == "Tiingo": + kwargs["start_date"] = st.session_state.news_start_date.strftime("%Y-%m-%d") + kwargs["end_date"] = st.session_state.news_end_date.strftime("%Y-%m-%d") + kwargs["symbol"] = ( + st.session_state.tiingo_tickers if st.session_state.tiingo_tickers else None + ) + + kwargs = {key: value for key, value in kwargs.items() if value is not None} + + data = ( + obb.news.company(**kwargs) # type: ignore + if kwargs.get("symbol") + else obb.news.world(**kwargs) # type: ignore + ) + if data.results != []: + return data.to_df().sort_index(ascending=False).reset_index() + + +def update_data(): + st.session_state.news = fetch_openbb() + + +with st.sidebar: + c1, c2 = st.columns(2) + with c1: + old_start_date = st.session_state.news_start_date + old_provider = st.session_state.selected_provider + st.session_state.selected_provider = st.selectbox( + label="Provider", + options=news_sources, + index=news_sources.index(st.session_state.selected_provider), + ) + old_tags = st.session_state.selected_tags + if st.session_state.selected_provider == "Benzinga": + st.session_state.news_start_date = st.date_input( + "Start Date", value=old_start_date + ) + st.session_state.selected_tags = st.text_input( + label="Tag", value=st.session_state.selected_tags + ) + old_biztoc_tags = st.session_state.biztoc_tags + old_biztoc_source = st.session_state.selected_biztoc_source + if st.session_state.selected_provider == "Biztoc": + st.session_state.selected_tags = st.selectbox( + label="Tag", options=[None] + old_biztoc_tags + ) + st.session_state.selected_biztoc_source = st.selectbox( + label="Source", options=[None] + st.session_state.biztoc_sources + ) + old_benzinga_tickers = st.session_state.benzinga_tickers + if st.session_state.selected_provider == "Benzinga": + st.session_state.benzinga_tickers = st.text_input( + label="Tickers", value=old_benzinga_tickers + ) + old_fmp_tickers = st.session_state.fmp_tickers + if st.session_state.selected_provider == "FMP": + st.session_state.fmp_tickers = st.text_input( + label="Tickers", value=old_fmp_tickers + ) + old_intrinio_tickers = st.session_state.intrinio_tickers + if st.session_state.selected_provider == "Intrinio": + st.session_state.intrinio_tickers = st.text_input( + label="Tickers", value=old_intrinio_tickers + ) + old_tiingo_tickers = st.session_state.tiingo_tickers + if st.session_state.selected_provider == "Tiingo": + st.session_state.news_start_date = st.date_input( + "Start Date", value=old_start_date + ) + old_tiingo_tickers = st.session_state.tiingo_tickers + st.session_state.tiingo_tickers = st.text_input( + label="Tickers", value=st.session_state.tiingo_tickers + ) + with c2: + old_limit = st.session_state.selected_limit + old_end_date = st.session_state.news_end_date + st.session_state.selected_limit = st.number_input( + "Number of Stories", min_value=1, value=100 + ) + old_channel = st.session_state.selected_benzinga_channel + if st.session_state.selected_provider == "Benzinga": + st.session_state.news_end_date = st.date_input( + "End Date", value=old_end_date + ) + st.session_state.selected_benzinga_channel = st.text_input( + label="Feed Channel", value=old_channel + ) + old_term = st.session_state.selected_term + if st.session_state.selected_provider == "Biztoc": + st.session_state.selected_term = st.text_input( + label="Search Term", value=old_term + ) + if st.session_state.selected_provider == "Tiingo": + st.session_state.news_end_date = st.date_input( + "End Date", value=old_end_date + ) + + if any( + [ + old_start_date != st.session_state.news_start_date, + old_end_date != st.session_state.news_end_date, + old_limit != st.session_state.selected_limit, + old_provider != st.session_state.selected_provider, + old_tags != st.session_state.selected_tags, + old_term != st.session_state.selected_term, + old_biztoc_source != st.session_state.selected_biztoc_source, + old_channel != st.session_state.selected_benzinga_channel, + old_benzinga_tickers != st.session_state.benzinga_tickers, + old_fmp_tickers != st.session_state.fmp_tickers, + old_intrinio_tickers != st.session_state.intrinio_tickers, + old_tiingo_tickers != st.session_state.tiingo_tickers, + ] + ): + update_data() + + if st.button("Fetch Data"): + update_data() + + +def main(): + with st.session_state.news_container.container(): + st.markdown( + " " + "

Headlines and Stories

", + unsafe_allow_html=True, + ) + st.divider() + if st.session_state.news is not None: + story = -1 + expanded = False + for i in st.session_state.news.index: + story += 1 + expanded = story == 0 + text = ( + st.session_state.news.loc[i].text + if "text" in st.session_state.news.loc[i] + else st.session_state.news.loc[i].get("title") + ) + src = st.session_state.news.loc[i].url + date = str(st.session_state.news.loc[i].date) + title = st.session_state.news.loc[i].title + if text != "": + with st.expander(label=f"{date} - {title}", expanded=expanded): + st.markdown( + f""" +
+

{title}

+
+ """, + unsafe_allow_html=True, + ) + + if st.session_state.selected_provider == "Benzinga": + _tags = ( + st.session_state.news.loc[i].tags + if st.session_state.news.loc[i].get("tags") + else "" + ) + _stocks = ( + st.session_state.news.loc[i].stocks + if st.session_state.news.loc[i].get("stocks") + else "" + ) + _channels = ( + st.session_state.news.loc[i].channels + if st.session_state.news.loc[i].get("channels") + else "" + ) + _images = ( + st.session_state.news.loc[i].images + if st.session_state.news.loc[i].get("images") + else [] + ) + _url = st.session_state.news.loc[i].url + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + if _images: + img = _images[0].get("url") + if img is not None: + st.markdown( + f"


", + unsafe_allow_html=True, + ) + if text is not None: + st.markdown(text, unsafe_allow_html=True) + st.divider() + st.write(_url) + if _tags: + st.markdown( + f"##### Tags for this story: \n {_tags} \n" + ) + if _stocks: + st.markdown(f"##### Stocks mentioned:\n {_stocks} \n") + if _channels: + st.markdown( + f"##### Channels for this story: \n {_channels} \n" + ) + + if st.session_state.selected_provider == "Biztoc": + if st.session_state.news.loc[i].get("images"): + img = st.session_state.news.loc[i].images.get("s") + img = ( + st.session_state.news.loc[i].images.get("o") + if img is None + else img + ) + if img is not None: + st.markdown( + f"


", + unsafe_allow_html=True, + ) + if text: + st.markdown(text, unsafe_allow_html=True) + st.write(src) + _story_tags = st.session_state.news.loc[i].get("tags") + _story_tags = ",".join(_story_tags) if _story_tags else "" + if _story_tags: + st.divider() + st.markdown( + f"##### Tags for this story: \n {_story_tags} \n\n" + ) + + if st.session_state.selected_provider == "Intrinio": + _tags = st.session_state.news.loc[i].get("tags") + _stocks = ( + st.session_state.news.loc[i]["company"].get("ticker") + if st.session_state.news.loc[i].get("company") + else None + ) + _images = st.session_state.news.loc[i].get("images") + _url = st.session_state.news.loc[i].get("url") + st.markdown(text, unsafe_allow_html=True) + if _url: + st.write(_url) + if _stocks: + st.divider() + st.markdown(f"##### Stocks mentioned:\n {_stocks} \n") + + if st.session_state.selected_provider == "FMP": + _url = st.session_state.news.loc[i].get("url") + _images = st.session_state.news.loc[i].get("images") + img = _images[0].get("o") if _images else None + if img is not None: + st.markdown( + f""" +
+ +
+
+ """, + unsafe_allow_html=True, + ) + st.markdown(text, unsafe_allow_html=True) + if _url: + st.write(_url) + + if st.session_state.selected_provider == "Tiingo": + _url = st.session_state.news.loc[i].get("url") + _tags = st.session_state.news.loc[i].get("tags") + _stocks = st.session_state.news.loc[i].get("symbols") + if _url: + st.write(_url) + st.divider() + if _tags: + st.markdown( + f"##### Tags for this story: \n {_tags} \n" + ) + if _stocks: + st.markdown(f"##### Stocks mentioned:\n {_stocks} \n") + + st.divider() + st.write( + "Learn more about the OpenBB Platform [here](https://docs.openbb.co/platform)" + ) + + +if __name__ == "__main__": + main() diff --git a/examples/streamlit/requirements.txt b/examples/streamlit/requirements.txt new file mode 100644 index 00000000000..56144ee2bd9 --- /dev/null +++ b/examples/streamlit/requirements.txt @@ -0,0 +1,3 @@ +streamlit +openbb +openbb-biztoc \ No newline at end of file diff --git a/examples/streamlit_news.webp b/examples/streamlit_news.webp new file mode 100644 index 0000000000000000000000000000000000000000..1e9abedf2be3b29253965a8acddfb83133df46f6 GIT binary patch literal 9448 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 literal 0 HcmV?d00001