"""News display components for the financial dashboard.""" import streamlit as st import pandas as pd from datetime import datetime import html as html_module def display_tradingview_news_card(news_item: dict): """Display a single news card with TradingView-inspired styling.""" # Calculate time ago time_diff = datetime.now() - news_item['timestamp'] if time_diff.seconds < 60: time_ago = f"{time_diff.seconds}s ago" elif time_diff.seconds < 3600: time_ago = f"{time_diff.seconds // 60}m ago" else: hours = time_diff.seconds // 3600 time_ago = f"{hours}h ago" if hours < 24 else f"{time_diff.days}d ago" # Impact badge colors (TradingView style) impact_colors = { 'high': '#F23645', # Red 'medium': '#FF9800', # Orange 'low': '#089981' # Green } # Sentiment colors sentiment_colors = { 'positive': '#089981', # Green 'negative': '#F23645', # Red 'neutral': '#787B86' # Gray } impact_color = impact_colors.get(news_item['impact'], '#787B86') sentiment_color = sentiment_colors.get(news_item['sentiment'], '#787B86') # Escape HTML in text summary = html_module.escape(news_item.get('summary', '').strip()) source = html_module.escape(news_item['source']) category = html_module.escape(news_item['category']) url = html_module.escape(news_item['url']) # TradingView-style card HTML card_html = f"""
{source} {news_item['impact'].upper()} {'β–²' if news_item['sentiment'] == 'positive' else 'β–Ό' if news_item['sentiment'] == 'negative' else '●'} {news_item['sentiment'].upper()} #{category}
{time_ago}
{summary}
Read Full Story β†’
""" st.markdown(card_html, unsafe_allow_html=True) def display_news_card(news_item: dict): """Wrapper to maintain compatibility - calls TradingView-style card.""" display_tradingview_news_card(news_item) def display_scrollable_news_section(df: pd.DataFrame, section_title: str, section_icon: str, section_subtitle: str, max_items: int = 20, height: str = "600px"): """Display a scrollable news section with TradingView styling.""" if df.empty: st.markdown(f"""

πŸ“­ No news available for this section

""", unsafe_allow_html=True) return # Build header HTML (no leading whitespace) header_html = f"""

{section_icon} {section_title}

{section_subtitle}

{len(df.head(max_items))} stories
""" # Render header st.markdown(header_html, unsafe_allow_html=True) # Build all news cards HTML news_cards_html = "" for idx, row in df.head(max_items).iterrows(): news_item = row.to_dict() # Calculate time ago time_diff = datetime.now() - news_item['timestamp'] if time_diff.seconds < 60: time_ago = f"{time_diff.seconds}s ago" elif time_diff.seconds < 3600: time_ago = f"{time_diff.seconds // 60}m ago" else: hours = time_diff.seconds // 3600 time_ago = f"{hours}h ago" if hours < 24 else f"{time_diff.days}d ago" # Impact and sentiment colors impact_colors = {'high': '#F23645', 'medium': '#FF9800', 'low': '#089981'} sentiment_colors = {'positive': '#089981', 'negative': '#F23645', 'neutral': '#787B86'} impact_color = impact_colors.get(news_item['impact'], '#787B86') sentiment_color = sentiment_colors.get(news_item['sentiment'], '#787B86') # Escape HTML summary = html_module.escape(news_item.get('summary', '').strip()) source = html_module.escape(news_item['source']) category = html_module.escape(news_item['category']) url = html_module.escape(news_item['url']) sentiment_symbol = 'β–²' if news_item['sentiment'] == 'positive' else 'β–Ό' if news_item['sentiment'] == 'negative' else '●' # Build card HTML (no leading whitespace) news_cards_html += f"""
{source} {news_item['impact'].upper()} {sentiment_symbol} {news_item['sentiment'].upper()} #{category}
{time_ago}
{summary}
Read Full Story β†’
""" # Generate unique class name to avoid conflicts import random unique_id = f"news-scroll-{random.randint(10000, 99999)}" # Render scrollable container with all news cards using st.markdown (no leading whitespace) scrollable_html = f"""
{news_cards_html}
""" st.markdown(scrollable_html, unsafe_allow_html=True) def display_news_feed(df: pd.DataFrame, max_items: int = 20): """Display a feed of news items (legacy compatibility).""" if df.empty: st.info("πŸ“­ No news available. Adjust your filters or refresh the feed.") return # Display news items for idx, row in df.head(max_items).iterrows(): display_tradingview_news_card(row.to_dict()) def display_news_statistics(stats: dict): """Display news feed statistics in metric cards.""" col1, col2, col3, col4 = st.columns(4) with col1: st.metric( "Total Stories", f"{stats['total']}", help="Total news items in feed" ) with col2: st.metric( "High Impact", f"{stats['high_impact']}", delta=f"{(stats['high_impact']/max(stats['total'], 1)*100):.0f}%", help="High-impact market-moving news" ) with col3: st.metric( "Breaking News", f"{stats['breaking']}", delta="LIVE" if stats['breaking'] > 0 else None, help="Breaking news alerts" ) with col4: st.metric( "Last Update", stats['last_update'], help="Time of last news fetch" ) def display_category_breakdown(stats: dict): """Display news breakdown by category using Streamlit components.""" if 'by_category' not in stats: return st.markdown("### πŸ“Š News by Category") categories = stats['by_category'] total = sum(categories.values()) if total == 0: st.info("No categorized news available") return col1, col2, col3 = st.columns(3) with col1: macro_count = categories.get('macro', 0) macro_pct = (macro_count / total) * 100 with st.container(): st.markdown("**:blue[πŸ“ˆ MACRO]**") st.markdown(f"# {macro_count}") st.caption(f"{macro_pct:.1f}% of total") with col2: geo_count = categories.get('geopolitical', 0) geo_pct = (geo_count / total) * 100 with st.container(): st.markdown("**:orange[🌍 GEOPOLITICAL]**") st.markdown(f"# {geo_count}") st.caption(f"{geo_pct:.1f}% of total") with col3: markets_count = categories.get('markets', 0) markets_pct = (markets_count / total) * 100 with st.container(): st.markdown("**:green[πŸ’Ή MARKETS]**") st.markdown(f"# {markets_count}") st.caption(f"{markets_pct:.1f}% of total") def display_breaking_news_banner(df: pd.DataFrame): """Display breaking news banner at the top with TradingView styling and ML-based impact score.""" # With ML-based scoring, we trust that the passed DataFrame already contains # the highest-impact news, so no need to filter by is_breaking # (The scorer already selected the most impactful news) if not df.empty: latest = df.iloc[0] # Escape HTML summary = html_module.escape(latest.get('summary', '').strip()) source = html_module.escape(latest['source']) url = html_module.escape(latest['url']) # Get impact score if available impact_score = latest.get('breaking_score', 0) score_display = f"{impact_score:.1f}" if impact_score > 0 else "N/A" # Determine score color and label if impact_score >= 80: score_color = "#FF3B30" # Critical red score_label = "CRITICAL" elif impact_score >= 60: score_color = "#FF9500" # High orange score_label = "HIGH" elif impact_score >= 40: score_color = "#FFCC00" # Medium yellow score_label = "MEDIUM" else: score_color = "#34C759" # Low green score_label = "LOW" # Calculate time ago time_diff = datetime.now() - latest['timestamp'] if time_diff.seconds < 60: time_ago = f"{time_diff.seconds}s ago" elif time_diff.seconds < 3600: time_ago = f"{time_diff.seconds // 60}m ago" else: hours = time_diff.seconds // 3600 time_ago = f"{hours}h ago" if hours < 24 else f"{time_diff.days}d ago" # TradingView-style breaking news banner with impact score (no leading whitespace) banner_html = f"""
🚨
⚑ Breaking News
{source} β€’ {time_ago} β€’ πŸ“Š IMPACT: {score_display}/100 ({score_label})
READ NOW β†’
{summary}
""" st.markdown(banner_html, unsafe_allow_html=True) def display_prediction_card(prediction_item: dict): """Display a single prediction market card with probability visualization.""" # Escape HTML in text title = html_module.escape(prediction_item.get('title', '').strip()) source = html_module.escape(prediction_item['source']) url = html_module.escape(prediction_item['url']) # Get probabilities yes_prob = prediction_item.get('yes_probability', 50.0) no_prob = prediction_item.get('no_probability', 50.0) # Determine bar color based on probabilities if yes_prob > 60: bar_color = '#089981' # Green - likely YES sentiment_text = 'YES LIKELY' elif no_prob > 60: bar_color = '#F23645' # Red - likely NO sentiment_text = 'NO LIKELY' else: bar_color = '#FF9800' # Orange - balanced sentiment_text = 'BALANCED' # Format end date if available end_date = prediction_item.get('end_date') if end_date: if isinstance(end_date, str): end_date_display = end_date else: days_until = (end_date - datetime.now()).days end_date_display = f"Closes in {days_until}d" if days_until > 0 else "Closed" else: end_date_display = "" # Volume display volume = prediction_item.get('volume', 0) if volume > 1000000: volume_display = f"${volume/1000000:.1f}M volume" elif volume > 1000: volume_display = f"${volume/1000:.1f}K volume" elif volume > 0: volume_display = f"${volume:.0f} volume" else: volume_display = "" # Prediction card HTML card_html = f"""
{source} {sentiment_text}
{title}
YES {yes_prob:.1f}% NO {no_prob:.1f}%
{end_date_display}{" β€’ " + volume_display if volume_display and end_date_display else volume_display}
View Market β†’
""" st.markdown(card_html, unsafe_allow_html=True) def display_economic_event_card(event_item: dict): """Display a single economic event card with forecast/actual comparison.""" # Escape HTML title = html_module.escape(event_item.get('event_name', event_item.get('title', '')).strip()) country = html_module.escape(event_item.get('country', 'US')) url = html_module.escape(event_item.get('url', '')) # Get values forecast = event_item.get('forecast') previous = event_item.get('previous') actual = event_item.get('actual') importance = event_item.get('importance', 'medium') # Importance badge color importance_colors = { 'high': '#F23645', 'medium': '#FF9800', 'low': '#787B86' } importance_color = importance_colors.get(importance, '#787B86') # Time to event time_to_event = event_item.get('time_to_event', '') # Format values with unit detection def format_value(val): if val is None: return '-' if isinstance(val, (int, float)): # Check if it looks like a percentage if abs(val) < 100: return f"{val:.1f}%" else: return f"{val:.1f}" return str(val) forecast_display = format_value(forecast) previous_display = format_value(previous) actual_display = format_value(actual) # Determine if beat/miss beat_miss_html = "" if actual is not None and forecast is not None: if actual > forecast: beat_miss_html = '[BEAT]' elif actual < forecast: beat_miss_html = '[MISS]' # Country flag emojis country_flags = { 'US': 'πŸ‡ΊπŸ‡Έ', 'EU': 'πŸ‡ͺπŸ‡Ί', 'UK': 'πŸ‡¬πŸ‡§', 'JP': 'πŸ‡―πŸ‡΅', 'CN': 'πŸ‡¨πŸ‡³', 'CA': 'πŸ‡¨πŸ‡¦', 'AU': 'πŸ‡¦πŸ‡Ί' } flag = country_flags.get(country, '🌍') # Event card HTML card_html = f"""
{flag} {importance.upper()}
{title}
{f'
{time_to_event}
' if time_to_event else ''}
Forecast: {forecast_display}
Previous: {previous_display}
{f'
Actual:{actual_display} {beat_miss_html}
' if actual is not None else ''}
""" st.markdown(card_html, unsafe_allow_html=True) def display_economic_calendar_widget(events_df: pd.DataFrame): """Display economic calendar widget showing upcoming events.""" if events_df.empty: st.info("πŸ“… No upcoming economic events in the next 7 days") return # Build widget HTML with single-line styles (no leading whitespace) widget_html = """

πŸ“… Economic Calendar

Upcoming high-impact events

""" # Show top 10 events for idx, event in events_df.head(10).iterrows(): # Get event details event_name = html_module.escape(event.get('event_name', event.get('title', ''))) country = html_module.escape(event.get('country', 'US')) importance = event.get('importance', 'medium') time_to_event = event.get('time_to_event', '') forecast = event.get('forecast') # Country flags country_flags = { 'US': 'πŸ‡ΊπŸ‡Έ', 'EU': 'πŸ‡ͺπŸ‡Ί', 'UK': 'πŸ‡¬πŸ‡§', 'JP': 'πŸ‡―πŸ‡΅', 'CN': 'πŸ‡¨πŸ‡³' } flag = country_flags.get(country, '🌍') # Importance stars stars = '⭐' * ({'high': 3, 'medium': 2, 'low': 1}.get(importance, 1)) # Format forecast forecast_display = f"{forecast:.1f}" if forecast is not None else "N/A" # Importance color importance_color = '#F23645' if importance == 'high' else '#FF9800' if importance == 'medium' else '#787B86' # Build event HTML (no leading whitespace, single-line styles) event_html = f"""
{flag} {event_name}
{stars} Forecast: {forecast_display}
{time_to_event}
""" widget_html += event_html widget_html += "
" st.markdown(widget_html, unsafe_allow_html=True)