import streamlit as st from urllib.request import urlopen, Request from bs4 import BeautifulSoup import pandas as pd import plotly.express as px from dateutil import parser import datetime import requests from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline import torch # Page config st.set_page_config( page_title="Stock News Sentiment Analyzer", page_icon="📈", layout="wide", initial_sidebar_state="expanded" ) # Custom CSS for styling st.markdown(""" """, unsafe_allow_html=True) # Initialize FinBERT-tone model and tokenizer @st.cache_resource def load_finbert_model(): try: model = AutoModelForSequenceClassification.from_pretrained("yiyanghkust/finbert-tone") tokenizer = AutoTokenizer.from_pretrained("yiyanghkust/finbert-tone") return pipeline("text-classification", model=model, tokenizer=tokenizer) except Exception as e: st.error(f"Error loading model: {str(e)}") return None # Load the model finbert = load_finbert_model() # Web scraping functions def verify_link(url, timeout=10, retries=3): """Verify if a URL is accessible.""" for _ in range(retries): try: response = requests.head(url, timeout=timeout, allow_redirects=True) return 200 <= response.status_code < 300 except requests.RequestException: continue return False def get_news(ticker): """Scrape news from FinViz for a given stock ticker.""" try: finviz_url = f'https://finviz.com/quote.ashx?t={ticker}' req = Request(url=finviz_url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }) response = urlopen(req) html = BeautifulSoup(response, 'html.parser') news_table = html.find(id='news-table') if news_table is None: raise ValueError("No news table found - invalid ticker or website structure changed") return news_table except Exception as e: raise Exception(f"Error fetching news for {ticker}: {str(e)}") def parse_news(news_table): """Parse the news table and return a DataFrame.""" parsed_news = [] try: for row in news_table.findAll('tr'): title = row.a.get_text() link = row.a['href'] date_data = row.td.text.strip().split() if len(date_data) == 1: time = date_data[0] date = datetime.datetime.today().strftime('%Y-%m-%d') else: date = date_data[0] time = date_data[1] parsed_date = parser.parse(f"{date} {time}") is_valid = verify_link(link) parsed_news.append([parsed_date, title, link, is_valid]) return pd.DataFrame(parsed_news, columns=['datetime', 'headline', 'link', 'is_valid']) except Exception as e: raise Exception(f"Error parsing news: {str(e)}") def analyze_sentiment(text): """Analyze sentiment of a single piece of text using FinBERT-tone.""" try: result = finbert(text)[0] label = result['label'] score = result['score'] sentiment_score = { 'Positive': score, 'Negative': -score, 'Neutral': 0 }.get(label, 0) return { 'sentiment_score': sentiment_score, 'tone': label, 'confidence': score } except Exception as e: st.error(f"Error analyzing sentiment: {str(e)}") return {'sentiment_score': 0, 'tone': 'Error', 'confidence': 0} def process_news_sentiment(parsed_news_df): """Process sentiment for all news headlines.""" try: # Analyze sentiment for each headline sentiment_data = [analyze_sentiment(headline) for headline in parsed_news_df['headline']] # Convert to DataFrame sentiment_df = pd.DataFrame(sentiment_data) # Join with original news DataFrame result_df = parsed_news_df.join(sentiment_df) return result_df.set_index('datetime') except Exception as e: raise Exception(f"Error processing sentiments: {str(e)}") # Visualization functions def plot_sentiment_timeline(df, ticker): """Create an hourly sentiment timeline plot.""" try: hourly_sentiment = df['sentiment_score'].resample('H').mean() fig = px.bar( hourly_sentiment, title=f"{ticker} Hourly Sentiment Trend", color=hourly_sentiment.values, color_continuous_scale=['red', 'yellow', 'green'], range_color=[-1, 1] ) fig.update_layout( xaxis_title="Time", yaxis_title="Sentiment Score", coloraxis_colorbar_title="Sentiment", showlegend=False, height=400 ) return fig except Exception as e: st.error(f"Error creating timeline plot: {str(e)}") return None def plot_sentiment_distribution(df, ticker): """Create a pie chart of sentiment distribution.""" try: tone_counts = df['tone'].value_counts() fig = px.pie( values=tone_counts.values, names=tone_counts.index, title=f"{ticker} Sentiment Distribution", color=tone_counts.index, color_discrete_map={ 'Positive': 'green', 'Neutral': 'yellow', 'Negative': 'red' } ) fig.update_layout(height=400) return fig except Exception as e: st.error(f"Error creating distribution plot: {str(e)}") return None def generate_recommendation(df): """Generate trading recommendation based on sentiment analysis.""" try: avg_sentiment = df['sentiment_score'].mean() tone_counts = df['tone'].value_counts() total_articles = len(df) positive_pct = tone_counts.get('Positive', 0) / total_articles * 100 negative_pct = tone_counts.get('Negative', 0) / total_articles * 100 if avg_sentiment >= 0.3 and positive_pct >= 50: return "🟢 STRONG BUY", f"Strong positive sentiment (Score: {avg_sentiment:.2f}, {positive_pct:.1f}% positive news). The recent news suggests a very favorable outlook." elif avg_sentiment >= 0.1: return "🟡 MODERATE BUY", f"Moderately positive sentiment (Score: {avg_sentiment:.2f}, {positive_pct:.1f}% positive news). The recent news leans positive." elif avg_sentiment <= -0.3 and negative_pct >= 50: return "🔴 STRONG SELL", f"Strong negative sentiment (Score: {avg_sentiment:.2f}, {negative_pct:.1f}% negative news). The recent news suggests significant caution." elif avg_sentiment <= -0.1: return "🟡 MODERATE SELL", f"Moderately negative sentiment (Score: {avg_sentiment:.2f}, {negative_pct:.1f}% negative news). The recent news leans negative." else: return "⚪ NEUTRAL", f"Neutral sentiment (Score: {avg_sentiment:.2f}). The recent news shows mixed or neutral signals." except Exception as e: st.error(f"Error generating recommendation: {str(e)}") return "⚠️ ERROR", "Unable to generate recommendation due to an error." # Main application def main(): st.title("📈 Stock News Sentiment Analyzer") st.markdown(""" This application analyzes the sentiment of recent news articles for any given stock ticker using the FinBERT-tone model, which is specifically trained for financial text analysis. """) # User input ticker = st.text_input('Enter Stock Ticker (e.g., AAPL, GOOGL)', '').upper() if ticker: try: with st.spinner('Fetching and analyzing news...'): # Get and process news news_table = get_news(ticker) parsed_news_df = parse_news(news_table) analyzed_news = process_news_sentiment(parsed_news_df) # Generate recommendation signal, explanation = generate_recommendation(analyzed_news) # Display recommendation st.header(f"Analysis Results for {ticker}") st.subheader(f"Signal: {signal}") st.write(explanation) # Display charts col1, col2 = st.columns(2) with col1: timeline_fig = plot_sentiment_timeline(analyzed_news, ticker) if timeline_fig: st.plotly_chart(timeline_fig, use_container_width=True) with col2: distribution_fig = plot_sentiment_distribution(analyzed_news, ticker) if distribution_fig: st.plotly_chart(distribution_fig, use_container_width=True) # Display news table st.subheader("Recent News Analysis") # Prepare display DataFrame display_df = analyzed_news.copy() display_df['link'] = display_df.apply( lambda row: f'{"🔗" if row["is_valid"] else "❌"}', axis=1 ) # Format and display table display_df = display_df[['headline', 'tone', 'confidence', 'sentiment_score', 'link']] display_df = display_df.rename(columns={ 'headline': 'Headline', 'tone': 'Sentiment', 'confidence': 'Confidence', 'sentiment_score': 'Score', 'link': 'Link' }) st.write(display_df.to_html(escape=False), unsafe_allow_html=True) # Disclaimer st.markdown(""" --- **Disclaimer:** This analysis is based on news sentiment only and should not be considered as financial advice. Always conduct thorough research and consult with financial professionals before making investment decisions. """) except Exception as e: st.error(f"Error processing {ticker}: {str(e)}") st.write("Please check the ticker symbol and try again.") # Run the application if __name__ == "__main__": main()