import streamlit as st import requests import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots import os import asyncio import aiohttp # Global parameters API_KEY = os.getenv("FMP_API_KEY") MAX_PAGES = 5 DEFAULT_TOP_N = 10 # Initialize session state run flags and inputs if not already set if "historical_run" not in st.session_state: st.session_state.historical_run = False if "trending_run" not in st.session_state: st.session_state.trending_run = False if "change_run" not in st.session_state: st.session_state.change_run = False if "historical_ticker" not in st.session_state: st.session_state.historical_ticker = "AAPL" if "trending_top_n" not in st.session_state: st.session_state.trending_top_n = DEFAULT_TOP_N if "change_top_n" not in st.session_state: st.session_state.change_top_n = DEFAULT_TOP_N ############################# # ASYNC FUNCTIONS FOR HISTORICAL SENTIMENT ############################# async def fetch_historical_page(session, ticker, page): url = ( f"https://financialmodelingprep.com/api/v4/historical/social-sentiment?" f"symbol={ticker}&page={page}&apikey={API_KEY}" ) try: async with session.get(url) as resp: if resp.status == 200: data = await resp.json() return pd.DataFrame(data) else: return pd.DataFrame() except Exception: return pd.DataFrame() async def fetch_all_historical(ticker, pages): async with aiohttp.ClientSession() as session: tasks = [fetch_historical_page(session, ticker, page) for page in range(pages)] results = [] progress_bar = st.progress(0) completed = 0 for coro in asyncio.as_completed(tasks): df = await coro results.append(df) completed += 1 progress_bar.progress(completed / pages) return results @st.cache_data(show_spinner=False) def get_historical_sentiment_async(ticker, pages=MAX_PAGES): results = asyncio.run(fetch_all_historical(ticker, pages)) if results: full_df = pd.concat(results, ignore_index=True) if "date" in full_df.columns: full_df["date"] = pd.to_datetime(full_df["date"], errors="coerce") full_df.sort_values("date", inplace=True) return full_df return pd.DataFrame() ############################# # ASYNC FUNCTION FOR TRENDING SENTIMENT ############################# async def fetch_trending_async(sentiment_type="bullish", source="stocktwits"): url = ( f"https://financialmodelingprep.com/api/v4/social-sentiments/trending?" f"type={sentiment_type}&source={source}&apikey={API_KEY}" ) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: if resp.status == 200: data = await resp.json() return pd.DataFrame(data) else: return pd.DataFrame() @st.cache_data(show_spinner=False) def get_trending_sentiment_async(sentiment_type="bullish", source="stocktwits"): return asyncio.run(fetch_trending_async(sentiment_type, source)) ############################# # ASYNC FUNCTION FOR SENTIMENT CHANGE ############################# async def fetch_change_async(sentiment_type="bullish", source="stocktwits"): url = ( f"https://financialmodelingprep.com/api/v4/social-sentiments/change?" f"type={sentiment_type}&source={source}&apikey={API_KEY}" ) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: if resp.status == 200: data = await resp.json() return pd.DataFrame(data) else: return pd.DataFrame() @st.cache_data(show_spinner=False) def get_change_sentiment_async(sentiment_type="bullish", source="stocktwits"): return asyncio.run(fetch_change_async(sentiment_type, source)) ############################# # PLOTTING FUNCTION (unchanged) ############################# def plot_dual_axes( df, x_col, y_left, y_right, title, y_left_label, y_right_label, left_color="red", right_color="blue", date_range=None ): fig = make_subplots(specs=[[{"secondary_y": True}]]) fig.add_trace( go.Scatter( x=df[x_col], y=df[y_left], mode="lines+markers", name=y_left_label, line=dict(color=left_color) ), secondary_y=False ) fig.add_trace( go.Scatter( x=df[x_col], y=df[y_right], mode="lines+markers", name=y_right_label, line=dict(color=right_color) ), secondary_y=True ) fig.update_layout( title=title, xaxis=dict(range=date_range), xaxis_title="Date", legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5 ), margin=dict(l=60, r=60, t=80, b=60), width=1600, height=800 ) fig.update_yaxes( title_text=y_left_label, secondary_y=False, tickfont_color=left_color, titlefont_color=left_color ) fig.update_yaxes( title_text=y_right_label, secondary_y=True, tickfont_color=right_color, titlefont_color=right_color ) return fig ############################# # PAGE 1: Historical Sentiment ############################# def run_historical_sentiment(ticker): st.write( "See how sentiment changes over time for a specific ticker. " "It retrieves past data on sentiment, posts, comments, likes, and impressions " "from two social platforms. This can help spot long-term shifts or trends." ) df = get_historical_sentiment_async(ticker) if df.empty: st.error("No data found for " + ticker) return common_range = [df["date"].min(), df["date"].max()] st.subheader(f"{ticker} Sentiment Scores") st.write("Compares sentiment scores from two social channels over the selected date range.") fig1 = plot_dual_axes( df, "date", "stocktwitsSentiment", "twitterSentiment", f"{ticker} Sentiment Scores", "Stocktwits Sentiment", "Twitter Sentiment", date_range=common_range ) st.plotly_chart(fig1, use_container_width=True) st.subheader(f"{ticker} Posts") st.write("Tracks the number of posts on each platform. High volumes can hint at heightened interest.") fig2 = plot_dual_axes( df, "date", "stocktwitsPosts", "twitterPosts", f"{ticker} Posts", "Stocktwits Posts", "Twitter Posts", date_range=common_range ) st.plotly_chart(fig2, use_container_width=True) st.subheader(f"{ticker} Comments") st.write("Shows how many comments occurred. This may reveal engagement depth.") fig3 = plot_dual_axes( df, "date", "stocktwitsComments", "twitterComments", f"{ticker} Comments", "Stocktwits Comments", "Twitter Comments", date_range=common_range ) st.plotly_chart(fig3, use_container_width=True) st.subheader(f"{ticker} Likes") st.write("Compares the likes on each platform. More likes can signal positive reception.") fig4 = plot_dual_axes( df, "date", "stocktwitsLikes", "twitterLikes", f"{ticker} Likes", "Stocktwits Likes", "Twitter Likes", date_range=common_range ) st.plotly_chart(fig4, use_container_width=True) st.subheader(f"{ticker} Impressions") st.write("Shows overall impressions. Higher impressions may reflect broader visibility.") fig5 = plot_dual_axes( df, "date", "stocktwitsImpressions", "twitterImpressions", f"{ticker} Impressions", "Stocktwits Impressions", "Twitter Impressions", date_range=common_range ) st.plotly_chart(fig5, use_container_width=True) ############################# # PAGE 2: Trending Sentiment ############################# def run_trending_sentiment(top_n): st.write( "See which symbols rank highest in bullish or bearish sentiment right now. " "This helps pinpoint active tickers with strong positive or negative views." ) st.write("## Bullish Trending") bullish_df = get_trending_sentiment_async(sentiment_type="bullish", source="stocktwits") if bullish_df.empty: st.error("No bullish trending data available.") else: bullish_df = bullish_df.sort_values("rank").reset_index(drop=True) df_bull = bullish_df.head(top_n) fig = go.Figure() fig.add_trace( go.Bar( x=df_bull["symbol"], y=df_bull["sentiment"], name="Sentiment", text=df_bull["sentiment"], textposition="auto", marker_color="red" ) ) fig.add_trace( go.Bar( x=df_bull["symbol"], y=df_bull["lastSentiment"], name="Last Sentiment", text=df_bull["lastSentiment"], textposition="auto", marker_color="blue", yaxis="y2" ) ) fig.update_layout( title="Bullish Trending", barmode="group", xaxis=dict(title="Ticker", type="category", categoryorder="array", categoryarray=df_bull["symbol"].tolist()), yaxis=dict(title="Sentiment", titlefont=dict(color="red"), tickfont=dict(color="red")), yaxis2=dict(title="Last Sentiment", titlefont=dict(color="blue"), tickfont=dict(color="blue"), anchor="x", overlaying="y", side="right"), width=1000 + 100 * len(df_bull), height=600 ) st.plotly_chart(fig, use_container_width=True) st.write("## Bearish Trending") bearish_df = get_trending_sentiment_async(sentiment_type="bearish", source="stocktwits") if bearish_df.empty: st.error("No bearish trending data available.") else: bearish_df = bearish_df.sort_values("rank").reset_index(drop=True) df_bear = bearish_df.head(top_n) fig = go.Figure() fig.add_trace( go.Bar( x=df_bear["symbol"], y=df_bear["sentiment"], name="Sentiment", text=df_bear["sentiment"], textposition="auto", marker_color="red" ) ) fig.add_trace( go.Bar( x=df_bear["symbol"], y=df_bear["lastSentiment"], name="Last Sentiment", text=df_bear["lastSentiment"], textposition="auto", marker_color="blue", yaxis="y2" ) ) fig.update_layout( title="Bearish Trending", barmode="group", xaxis=dict(title="Ticker", type="category", categoryorder="array", categoryarray=df_bear["symbol"].tolist()), yaxis=dict(title="Sentiment", titlefont=dict(color="red"), tickfont=dict(color="red")), yaxis2=dict(title="Last Sentiment", titlefont=dict(color="blue"), tickfont=dict(color="blue"), anchor="x", overlaying="y", side="right"), width=1000 + 100 * len(df_bear), height=600 ) st.plotly_chart(fig, use_container_width=True) ############################# # PAGE 3: Sentiment Change ############################# def run_change_sentiment(top_n): st.write("**Sentiment Change**") st.write( "See if sentiment is trending up or down for each ticker. " "Compare the current sentiment to how much it has shifted. This can reveal rapid changes." ) st.write("## Bullish Sentiment Change") bullish_df = get_change_sentiment_async(sentiment_type="bullish", source="stocktwits") if bullish_df.empty: st.error("No bullish sentiment change data available.") else: bullish_df = bullish_df.sort_values("rank").reset_index(drop=True) df_bull = bullish_df.head(top_n) fig = go.Figure() fig.add_trace( go.Bar( x=df_bull["symbol"], y=df_bull["sentiment"], name="Sentiment", text=df_bull["sentiment"], textposition="auto", marker_color="red" ) ) fig.add_trace( go.Bar( x=df_bull["symbol"], y=df_bull["sentimentChange"], name="Sentiment Change", text=df_bull["sentimentChange"], textposition="auto", marker_color="blue", yaxis="y2" ) ) fig.update_layout( title="Bullish Sentiment Change", barmode="group", xaxis=dict(title="Ticker", type="category", categoryorder="array", categoryarray=df_bull["symbol"].tolist()), yaxis=dict(title="Sentiment", titlefont=dict(color="red"), tickfont=dict(color="red")), yaxis2=dict(title="Sentiment Change", titlefont=dict(color="blue"), tickfont=dict(color="blue"), anchor="x", overlaying="y", side="right"), width=1000 + 100 * len(df_bull), height=600 ) st.plotly_chart(fig, use_container_width=True) st.write("## Bearish Sentiment Change") bearish_df = get_change_sentiment_async(sentiment_type="bearish", source="stocktwits") if bearish_df.empty: st.error("No bearish sentiment change data available.") else: bearish_df = bearish_df.sort_values("rank").reset_index(drop=True) df_bear = bearish_df.head(top_n) fig = go.Figure() fig.add_trace( go.Bar( x=df_bear["symbol"], y=df_bear["sentiment"], name="Sentiment", text=df_bear["sentiment"], textposition="auto", marker_color="red" ) ) fig.add_trace( go.Bar( x=df_bear["symbol"], y=df_bear["sentimentChange"], name="Sentiment Change", text=df_bear["sentimentChange"], textposition="auto", marker_color="blue", yaxis="y2" ) ) fig.update_layout( title="Bearish Sentiment Change", barmode="group", xaxis=dict(title="Ticker", type="category", categoryorder="array", categoryarray=df_bear["symbol"].tolist()), yaxis=dict(title="Sentiment", titlefont=dict(color="red"), tickfont=dict(color="red")), yaxis2=dict(title="Sentiment Change", titlefont=dict(color="blue"), tickfont=dict(color="blue"), anchor="x", overlaying="y", side="right"), width=1000 + 100 * len(df_bear), height=600 ) st.plotly_chart(fig, use_container_width=True) ############################# # MAIN APP ############################# def main(): st.set_page_config(page_title="Social Sentiment Analysis", layout="wide") st.title("Social Sentiment Analysis") st.write( "Track social sentiment in three ways: " "**Historical Sentiment** shows how sentiment evolved over time for a specific stock. " "**Trending Sentiment** highlights the most talked-about stocks right now. " "**Sentiment Change** shows which stocks have had the biggest sentiment shifts recently. " ) with st.sidebar.expander("Navigation and Options", expanded=True): page = st.radio( "Select Analysis Page", ("Historical Sentiment", "Trending Sentiment", "Sentiment Change"), help="Choose which sentiment analysis you want." ) if page == "Historical Sentiment": ticker = st.text_input( "Ticker Symbol", value=st.session_state.historical_ticker, help="Enter a ticker symbol for historical data." ) if st.button("Run Analysis"): st.session_state.historical_run = True st.session_state.historical_ticker = ticker elif page == "Trending Sentiment": top_n = st.number_input( "Select Top N Stocks", min_value=1, max_value=100, value=st.session_state.trending_top_n, help="Number of top stocks to show based on rank." ) if st.button("Run Analysis"): st.session_state.trending_run = True st.session_state.trending_top_n = top_n elif page == "Sentiment Change": top_n = st.number_input( "Select Top N Stocks", min_value=1, max_value=100, value=st.session_state.change_top_n, help="Number of top stocks to show based on rank." ) if st.button("Run Analysis"): st.session_state.change_run = True st.session_state.change_top_n = top_n if page == "Historical Sentiment": st.header("Historical Social Sentiment") if st.session_state.historical_run: run_historical_sentiment(st.session_state.historical_ticker) else: st.info("Enter a ticker and click the button.") elif page == "Trending Sentiment": st.header("Trending Social Sentiment") if st.session_state.trending_run: run_trending_sentiment(st.session_state.trending_top_n) else: st.info("Pick a top N and click the button.") elif page == "Sentiment Change": st.header("Social Sentiment Change") if st.session_state.change_run: run_change_sentiment(st.session_state.change_top_n) else: st.info("Pick a top N and click the button.") if __name__ == "__main__": main() hide_streamlit_style = """ """ st.markdown(hide_streamlit_style, unsafe_allow_html=True)