import asyncio import re from datetime import datetime, timedelta from unittest.mock import patch import pandas as pd import streamlit as st import yfinance as yf from openbb_terminal.core.plots.plotly_helper import OpenBBFigure from openbb_terminal.core.session.current_system import set_system_variable from openbb_terminal.dashboards.stream import ( common_vars, streamlit_helpers as st_helpers, ) from openbb_terminal.rich_config import console pd.options.plotting.backend = "plotly" # Suppressing sdk logs set_system_variable("LOGGING_SUPPRESS", True) st.set_page_config( layout="wide", page_title="Stocks", initial_sidebar_state="expanded", ) st_helpers.set_current_page("Stocks") logger = st.empty() page_title = st.empty() main_chart, table_container = st.columns([4, 1.5]) volume_chart = st.empty() MAIN_LOOP: asyncio.AbstractEventLoop = None # type: ignore def special_st(text: str): rich_color = re.search(r"\[([a-z ]+)\]", text) text = re.sub(r"(\[\/{0,1}[a-z ]+\])|(\[\/\])", "", text) async def _special_st(): with logger.container(): if rich_color: color = rich_color.group(1) if "green" in color: st.success(text, icon="🎉") elif "red" in color: st.error(text, icon="🔥") await asyncio.sleep(5) elif "yellow" in color: st.warning(text, icon="⚠️") else: st.write(text) await asyncio.sleep(4) MAIN_LOOP.create_task(_special_st()) st_helpers.set_css() st.sidebar.markdown( "

Stock Analysis Dashboard

", unsafe_allow_html=True, ) r2c1, r2c2 = st.sidebar.columns([1, 1]) r2c3 = st.sidebar.container() def format_plotly( fig: OpenBBFigure, data: str, start: datetime, end: datetime, calc: pd.DataFrame = None, ): fig.update_yaxes(title=None) fig.update_xaxes(title=None) start_t = start.strftime("%Y/%m/%d") end_t = end.strftime("%Y/%m/%d") if calc: if len(calc) == 1: fig_title = f"{calc[0]} of {data} from {start_t} to {end_t}" else: fig_title = f"{', '.join(calc)} of {data} from {start_t} to {end_t}" height = 400 else: fig_title = "Volume" height = 250 fig.update_layout( autosize=False, height=height, title=dict( text=fig_title, y=1, x=0.5, xanchor="center", yanchor="top", ), legend=dict( bgcolor="rgba(0,0,0,0.5)", bordercolor="#F5EFF3", borderwidth=1, x=0.99, xanchor="right", ), ) def create_line( visual: str, x: pd.DataFrame, y: pd.DataFrame, name: str, data: str, fig: OpenBBFigure, ): if visual == "line": fig.add_scatter( x=x, y=y[data], mode="lines", name=name, connectgaps=True, hovertemplate="%{y}", ) if visual == "scatter": fig.add_scatter(x=x, y=y[data], mode="markers", name=name, hovertemplate="%{y}") if visual == "candle": fig.add_candlestick( x=x, open=y["Open"], close=y["Close"], high=y["High"], low=y["Low"], name=name, ) def show_fig(fig: OpenBBFigure, margin: bool = True, volume: bool = False): fig.show(external=True, margin=margin) with_container = main_chart if not volume else volume_chart with with_container.container(): st.plotly_chart( fig, use_container_width=True, config=dict( scrollZoom=True, displaylogo=False, ), ) def table_data(infos: dict): cols = ["Ticker"] + list(infos) data = pd.DataFrame(columns=cols) data["Ticker"] = [common_vars.STOCKS_CLEAN_ROW[x] for x in common_vars.STOCKS_ROWS] for ticker in list(infos): data[ticker] = [ st_helpers.STOCKS_CLEAN_DATA[x](infos[ticker].get(x, None)) if infos[ticker].get(x, None) else st_helpers.STOCKS_CLEAN_DATA[x]( getattr(yf.Ticker(ticker).fast_info, x, None) ) for x in common_vars.STOCKS_ROWS ] return data def stock_data( tickers: list, interval: str, start: datetime, end: datetime ) -> pd.DataFrame: with patch.object(console, "print", special_st): try: kwargs = {} if interval in ["1d", "5d", "1wk", "1mo", "3mo"]: kwargs.update({"period": "max"}) else: s_start_dt = datetime.utcnow() - timedelta(days=59) s_date_start = s_start_dt.strftime("%Y-%m-%d") start = s_date_start if s_start_dt.date() > start else start # type: ignore kwargs.update({"start": start, "end": end + timedelta(days=1)}) # type: ignore df: pd.DataFrame = yf.download( tickers, interval=interval, progress=False, **kwargs, ) if not df.empty: df.index = pd.to_datetime(df.index).tz_localize(None) return df except Exception: # noqa: S110 pass return pd.DataFrame() def volume_data(infos: dict, start: datetime, end: datetime, interval: str) -> dict: result = {} for ticker in infos: try: df: pd.DataFrame = yf.download( ticker, start=start, end=end, interval=interval ) df = df[["Volume"]] df = df.rename(columns={"Volume": ticker}) result[ticker] = df except Exception: # noqa: S110 pass return result class Chart: def __init__(self, loop: asyncio.AbstractEventLoop): self.df: pd.DataFrame = st_helpers.load_state("df", pd.DataFrame()) self.tickers: str = st_helpers.load_state("tickers", "TSLA") self.last_tickers: str = st_helpers.load_state("last_tickers", "") self.last_interval: str = st_helpers.load_state("last_interval", "") self.infos: dict = st_helpers.load_state("infos", {}) self.start: datetime = st_helpers.load_state( "start_date", (datetime.today() - timedelta(days=365)) ) self.end: datetime = st_helpers.load_state("end_date", datetime.now()) self.calculation: list = st_helpers.load_state("calculation", ["Raw Data"]) self.data: str = st_helpers.load_state("data", "Close") self.rolling: int = st_helpers.load_state("rolling", 60) self.chart: str = st_helpers.load_state("chart", "line") self.loop = loop global MAIN_LOOP # noqa MAIN_LOOP = loop def create_stock(self): self.tickers = self.tickers.split(",") if not self.tickers: return st.error("Missing Tickers") if self.tickers: if self.tickers != self.last_tickers or self.interval != self.last_interval: # get stock data self.df = stock_data(self.tickers, self.interval, self.start, self.end) self.last_tickers = self.tickers self.last_interval = self.interval fig = OpenBBFigure() for item in self.calculation: calcs = st_helpers.get_calc(item, self.df, self.rolling) if self.interval in ["1d", "5d", "1wk", "1mo", "3mo"]: result = calcs.loc[ (calcs.index >= self.start) & (calcs.index <= self.end) ] else: result = calcs if len(result.columns) == 6: name = f"{self.tickers[0]} {item}" create_line(self.chart, result.index, result, name, self.data, fig) else: for val in result.columns.levels[1]: vals = result.xs(val, axis=1, level=1, drop_level=True) name = f"{val.upper()} {item}" create_line( self.chart, result.index, vals, name, self.data, fig ) format_plotly(fig, self.data, self.start, self.end, self.calculation) fig.update_layout(margin=dict(l=30, r=0, t=25, b=0)) show_fig(fig, False) def create_volume(self): result = self.df.loc[ (self.df.index >= self.start) & (self.df.index <= self.end) ] fig = OpenBBFigure.create_subplots( 1, 1, horizontal_spacing=0.001, vertical_spacing=0.001 ) if len(result.columns) == 6: name = f"{self.tickers[0]}" create_line("line", result.index, result, name, "Volume", fig) else: for val in result.columns.levels[1]: vals = result.xs(val, axis=1, level=1, drop_level=True) name = f"{val.upper()}" create_line("line", result.index, vals, name, "Volume", fig) format_plotly(fig, "Volume", self.start, self.end) fig.update_layout(margin=dict(l=30, r=0, t=25, b=0)) show_fig(fig, False) def create_table(self): if not self.tickers: return for ticker in self.tickers: if ticker not in self.infos: try: self.infos[ticker] = yf.Ticker(ticker).info except Exception: self.infos[ticker] = {} pass delete = [ticker for ticker in self.infos if ticker not in self.tickers] for ticker in delete: self.infos.pop(ticker) result = table_data(self.infos) result.index = result.set_index("Ticker").index result = result.drop("Ticker", axis=1) with table_container: st.table(result) async def async_on_ticker_change(self): new_data = False tickers = list(set(st.session_state.tickers.split(","))) # type: ignore tickers = [ticker.upper().strip() for ticker in tickers if ticker.strip()] # type: ignore try: interval = st.session_state.interval except AttributeError: interval = "1d" check_last = [ ("last_tickers", ",".join(tickers)), ("last_interval", interval), ] new_params = any( [st.session_state.get(key, None) != value for key, value in check_last] ) if new_params: for key, value in check_last: st.session_state[key] = value new_data = True if new_data: st.session_state["tickers"] = ",".join(tickers) def on_ticker_change(self): self.loop.run_until_complete(self.async_on_ticker_change()) async def run(self): tickers = st.sidebar.text_input( "Ticker(s) (comma separated)", "", key="tickers", on_change=self.on_ticker_change, help="Enter a comma separated list of tickers", ) date_opts = st.sidebar.expander("Date options") data_opts = st.sidebar.expander("Data options") ( r1c2, r1c3, ) = date_opts.columns([1, 1]) r1c4, r1c5 = data_opts.columns([1, 1]) with date_opts.container(): with r1c2: self.start = st.date_input( "Start", (datetime.today() - timedelta(days=365)), key="start_date" ) with r1c3: self.end = st.date_input("End", datetime.today(), key="end_date") self.interval = st.selectbox( "Interval", common_vars.INTERVAL_OPTS[1:], index=2, key="interval" ) with data_opts.container(): with r1c4: self.data = st.selectbox( "Target", ["Open", "Close", "High", "Low"], index=1, key="data" ) with r1c5: self.chart = st.selectbox( "Chart", ["line", "scatter", "candle"], index=0, key="chart" ) self.calculation = st.multiselect( "Calculations", list(common_vars.STOCKS_VIEWS.keys()), ["Raw Data"], key="calculation", ) self.rolling = st.number_input("Rolling", 3, 100, 60, key="rolling") st.sidebar.markdown("""---""") submit_button = st.sidebar.button("Display") if submit_button and tickers: self.start = datetime.combine(self.start, datetime.min.time()) self.end = datetime.combine(self.end, datetime.max.time()) self.create_stock() self.create_volume() self.create_table() if __name__ == "__main__": try: loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(Chart(loop).run())