Spaces:
Sleeping
Sleeping
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(""" | |
<style> | |
.stAlert { | |
padding: 10px; | |
margin-bottom: 20px; | |
} | |
.reportview-container { | |
background: #f0f2f6 | |
} | |
.main { | |
padding: 2rem; | |
} | |
h1, h2, h3 { | |
color: #1f77b4; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Initialize FinBERT-tone model and tokenizer | |
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'<a href="{row["link"]}" target="_blank">{"π" if row["is_valid"] else "β"}</a>', | |
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() |