import logging import os import tempfile from datetime import datetime import gradio as gr import pandas as pd import plotly.express as px import torch import yfinance as yf from GoogleNews import GoogleNews from transformers import pipeline logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) SENTIMENT_ANALYSIS_MODEL = ( "mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis" ) DEVICE = 0 if torch.cuda.is_available() else -1 logging.info(f"Using device: {'cuda' if DEVICE == 0 else 'cpu'}") sentiment_analyzer = pipeline( "sentiment-analysis", model=SENTIMENT_ANALYSIS_MODEL, device=DEVICE ) def fetch_articles(query, max_articles=20): try: googlenews = GoogleNews(lang="en") googlenews.search(query) articles = googlenews.result() if not articles: return [] df = pd.DataFrame(articles) required_cols = ["title", "desc", "link", "date", "media"] for col in required_cols: if col not in df.columns: df[col] = "" df = df.drop_duplicates(subset=["title", "link"]) df = df.head(max_articles) return df.to_dict("records") except Exception as e: logging.error(f"News fetch failed for {query}: {e}") raise gr.Error("Unable to fetch news. Try again later.") def analyze_sentiments_batch(articles): if not articles: return [] texts = [ f"{article.get('title', '')}. {article.get('desc', '')}" for article in articles ] sentiments = sentiment_analyzer( texts, batch_size=8, truncation=True, max_length=512 ) for article, sentiment in zip(articles, sentiments): article["sentiment_label"] = sentiment["label"].lower() article["sentiment_score"] = round(float(sentiment["score"]), 4) return articles def get_price_context(asset): try: ticker = yf.Ticker(asset) hist = ticker.history(period="1mo") if hist.empty: return "No price data found. Try using ticker symbols like AAPL, TSLA, BTC-USD, ETH-USD." latest_price = hist["Close"].iloc[-1] prev_price = hist["Close"].iloc[-2] if len(hist) > 1 else latest_price first_price = hist["Close"].iloc[0] one_day_return = ((latest_price - prev_price) / prev_price) * 100 one_month_return = ((latest_price - first_price) / first_price) * 100 volatility = hist["Close"].pct_change().std() * 100 return ( f"### Price Context\n" f"- Latest close: **{latest_price:.2f}**\n" f"- 1D return: **{one_day_return:.2f}%**\n" f"- 1M return: **{one_month_return:.2f}%**\n" f"- 1M daily volatility: **{volatility:.2f}%**" ) except Exception as e: logging.error(f"Price fetch failed for {asset}: {e}") return "Price data unavailable for this asset." def sentiment_badge(label): colors = { "negative": "#dc2626", "neutral": "#6b7280", "positive": "#16a34a", } color = colors.get(label.lower(), "#6b7280") return ( f'' f'{label.upper()}' ) def build_article_table(articles): if not articles: return pd.DataFrame() df = pd.DataFrame(articles) df["Sentiment"] = df["sentiment_label"].apply(sentiment_badge) df["Confidence"] = df["sentiment_score"] df["Title"] = df.apply( lambda row: f'{row["title"]}', axis=1 ) df["Description"] = df["desc"] df["Source"] = df["media"] df["Date"] = df["date"] return df[ [ "Sentiment", "Confidence", "Title", "Description", "Source", "Date", ] ] def create_sentiment_summary(articles): if not articles: return "No articles found." df = pd.DataFrame(articles) counts = df["sentiment_label"].value_counts(normalize=True) * 100 avg_confidence = df["sentiment_score"].mean() positive = counts.get("positive", 0) negative = counts.get("negative", 0) neutral = counts.get("neutral", 0) bullish_score = positive - negative if bullish_score > 20: verdict = "Bullish" elif bullish_score < -20: verdict = "Bearish" else: verdict = "Neutral / Mixed" return ( f"### Sentiment Summary\n" f"- Overall view: **{verdict}**\n" f"- Positive: **{positive:.1f}%**\n" f"- Neutral: **{neutral:.1f}%**\n" f"- Negative: **{negative:.1f}%**\n" f"- Average model confidence: **{avg_confidence:.2f}**\n" f"- Bullish score: **{bullish_score:.1f}**" ) def generate_llm_style_summary(asset, articles): if not articles: return "No summary available." df = pd.DataFrame(articles) top_sentiment = df["sentiment_label"].value_counts().idxmax() top_articles = df.head(5) headlines = "\n".join( [f"- {row['title']}" for _, row in top_articles.iterrows()] ) return ( f"### AI-Style Market Brief\n" f"The current news sentiment for **{asset}** appears mostly " f"**{top_sentiment.upper()}** based on recent headlines.\n\n" f"Key headlines influencing this reading:\n" f"{headlines}\n\n" f"This should not be treated as a trading signal by itself. " f"Use it alongside price action, volume, volatility, macro events, " f"and risk management." ) def create_sentiment_chart(articles): if not articles: return None df = pd.DataFrame(articles) sentiment_counts = df["sentiment_label"].value_counts().reset_index() sentiment_counts.columns = ["Sentiment", "Count"] fig = px.bar( sentiment_counts, x="Sentiment", y="Count", title="Sentiment Distribution" ) return fig def create_source_chart(articles): if not articles: return None df = pd.DataFrame(articles) if "media" not in df.columns: return None source_counts = df["media"].replace("", "Unknown").value_counts().head(10) source_df = source_counts.reset_index() source_df.columns = ["Source", "Count"] fig = px.bar( source_df, x="Source", y="Count", title="Top News Sources" ) return fig def export_csv(articles): if not articles: return None df = pd.DataFrame(articles) filename = f"sentiment_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" filepath = os.path.join(tempfile.gettempdir(), filename) df.to_csv(filepath, index=False) return filepath def analyze_single_asset(asset_name, max_articles): if not asset_name.strip(): raise gr.Error("Please enter an asset name or ticker.") articles = fetch_articles(asset_name, max_articles) analyzed_articles = analyze_sentiments_batch(articles) article_table = build_article_table(analyzed_articles) sentiment_summary = create_sentiment_summary(analyzed_articles) price_context = get_price_context(asset_name) ai_summary = generate_llm_style_summary(asset_name, analyzed_articles) sentiment_chart = create_sentiment_chart(analyzed_articles) source_chart = create_source_chart(analyzed_articles) csv_file = export_csv(analyzed_articles) return ( sentiment_summary, price_context, ai_summary, article_table, sentiment_chart, source_chart, csv_file, ) def analyze_multiple_assets(asset_list, max_articles): assets = [ asset.strip() for asset in asset_list.split(",") if asset.strip() ] if not assets: raise gr.Error("Please enter at least one asset.") rows = [] for asset in assets: articles = fetch_articles(asset, max_articles) analyzed_articles = analyze_sentiments_batch(articles) if not analyzed_articles: rows.append( { "Asset": asset, "Overall Sentiment": "No data", "Positive %": 0, "Neutral %": 0, "Negative %": 0, "Bullish Score": 0, "Articles": 0, } ) continue df = pd.DataFrame(analyzed_articles) counts = df["sentiment_label"].value_counts(normalize=True) * 100 positive = counts.get("positive", 0) neutral = counts.get("neutral", 0) negative = counts.get("negative", 0) bullish_score = positive - negative if bullish_score > 20: overall = "Bullish" elif bullish_score < -20: overall = "Bearish" else: overall = "Neutral / Mixed" rows.append( { "Asset": asset, "Overall Sentiment": overall, "Positive %": round(positive, 2), "Neutral %": round(neutral, 2), "Negative %": round(negative, 2), "Bullish Score": round(bullish_score, 2), "Articles": len(analyzed_articles), } ) result_df = pd.DataFrame(rows) fig = px.bar( result_df, x="Asset", y="Bullish Score", title="Bullish Score Comparison" ) return result_df, fig with gr.Blocks(title="FinSentinel") as app: gr.Markdown("# FinSentinel") gr.Markdown( "Financial news sentiment dashboard for stocks, crypto, ETFs, commodities, and market themes." ) with gr.Tabs(): with gr.Tab("Single Asset Analysis"): with gr.Row(): asset_input = gr.Textbox( label="Asset Name or Ticker", placeholder="Example: AAPL, TSLA, Bitcoin, BTC-USD, Nvidia", ) max_articles = gr.Slider( minimum=5, maximum=50, value=20, step=5, label="Number of Articles", ) analyze_button = gr.Button("Analyze Asset", variant="primary") gr.Examples( examples=[ "AAPL", "TSLA", "NVDA", "BTC-USD", "Bitcoin", "Gold", ], inputs=asset_input, ) with gr.Row(): sentiment_summary_output = gr.Markdown() price_context_output = gr.Markdown() ai_summary_output = gr.Markdown() with gr.Row(): sentiment_chart_output = gr.Plot() source_chart_output = gr.Plot() articles_output = gr.Dataframe( label="Articles and Sentiment Analysis", headers=[ "Sentiment", "Confidence", "Title", "Description", "Source", "Date", ], datatype=[ "html", "number", "html", "str", "str", "str", ], wrap=True, interactive=False, ) csv_output = gr.File(label="Download CSV") analyze_button.click( analyze_single_asset, inputs=[asset_input, max_articles], outputs=[ sentiment_summary_output, price_context_output, ai_summary_output, articles_output, sentiment_chart_output, source_chart_output, csv_output, ], ) with gr.Tab("Portfolio Comparison"): portfolio_input = gr.Textbox( label="Assets", placeholder="Example: AAPL, TSLA, NVDA, BTC-USD, ETH-USD", lines=2, ) portfolio_articles = gr.Slider( minimum=5, maximum=30, value=10, step=5, label="Articles Per Asset", ) portfolio_button = gr.Button("Compare Assets", variant="primary") portfolio_table = gr.Dataframe( label="Portfolio Sentiment Comparison", interactive=False, ) portfolio_chart = gr.Plot() portfolio_button.click( analyze_multiple_assets, inputs=[portfolio_input, portfolio_articles], outputs=[portfolio_table, portfolio_chart], ) if __name__ == "__main__": app.queue().launch(server_name="0.0.0.0", server_port=7860)