"""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)