Dmitry Beresnev
optimize news module performance, improve news UI, etc
a8aef2e
raw
history blame
16.7 kB
"""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"""
<div style="
background: linear-gradient(135deg, #1E222D 0%, #131722 100%);
border: 1px solid #2A2E39;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
overflow: hidden;
" onmouseover="this.style.borderColor='#3861FB'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(56, 97, 251, 0.15)';"
onmouseout="this.style.borderColor='#2A2E39'; this.style.transform='translateY(0)'; this.style.boxShadow='none';">
<!-- Left colored indicator bar -->
<div style="
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: {impact_color};
"></div>
<!-- Header row -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; margin-left: 8px;">
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<span style="
color: #3861FB;
font-weight: 600;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;
">{source}</span>
<span style="
background: {impact_color};
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
">{news_item['impact'].upper()}</span>
<span style="
color: {sentiment_color};
font-size: 11px;
font-weight: 600;
padding: 2px 6px;
border: 1px solid {sentiment_color};
border-radius: 4px;
">{'▲' if news_item['sentiment'] == 'positive' else '▼' if news_item['sentiment'] == 'negative' else '●'} {news_item['sentiment'].upper()}</span>
<span style="
color: #787B86;
font-size: 11px;
background: rgba(120, 123, 134, 0.1);
padding: 2px 6px;
border-radius: 4px;
">#{category}</span>
</div>
<span style="color: #787B86; font-size: 11px; white-space: nowrap;">{time_ago}</span>
</div>
<!-- News summary -->
<div style="
color: #D1D4DC;
font-size: 14px;
line-height: 1.5;
margin-bottom: 8px;
margin-left: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;
">{summary}</div>
<!-- Read more link -->
<a href="{url}" target="_blank" style="
color: #3861FB;
font-size: 12px;
text-decoration: none;
margin-left: 8px;
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 500;
" onmouseover="this.style.color='#5880FF';" onmouseout="this.style.color='#3861FB';">
Read Full Story →
</a>
</div>
"""
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"""
<div style="
background: linear-gradient(135deg, #1E222D 0%, #131722 100%);
border: 1px solid #2A2E39;
border-radius: 8px;
padding: 20px;
text-align: center;
color: #787B86;
">
<p style="font-size: 16px; margin: 0;">📭 No news available for this section</p>
</div>
""", unsafe_allow_html=True)
return
# Section header
st.markdown(f"""
<div style="
background: linear-gradient(135deg, #2A2E39 0%, #1E222D 100%);
border: 1px solid #363A45;
border-radius: 8px 8px 0 0;
padding: 16px 20px;
margin-bottom: 0;
">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="
color: #D1D4DC;
margin: 0;
font-size: 18px;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;
">{section_icon} {section_title}</h3>
<p style="
color: #787B86;
margin: 4px 0 0 0;
font-size: 12px;
">{section_subtitle}</p>
</div>
<div style="
background: rgba(56, 97, 251, 0.15);
color: #3861FB;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
">{len(df.head(max_items))} stories</div>
</div>
</div>
""", unsafe_allow_html=True)
# Scrollable container with TradingView-style scrollbar
st.markdown(f"""
<style>
.news-scroll-container {{
height: {height};
overflow-y: auto;
background: #0D0E13;
border: 1px solid #2A2E39;
border-top: none;
border-radius: 0 0 8px 8px;
padding: 16px;
}}
/* TradingView-style scrollbar */
.news-scroll-container::-webkit-scrollbar {{
width: 8px;
}}
.news-scroll-container::-webkit-scrollbar-track {{
background: #1E222D;
border-radius: 4px;
}}
.news-scroll-container::-webkit-scrollbar-thumb {{
background: #363A45;
border-radius: 4px;
}}
.news-scroll-container::-webkit-scrollbar-thumb:hover {{
background: #434651;
}}
</style>
""", unsafe_allow_html=True)
# Start scrollable container
st.markdown('<div class="news-scroll-container">', unsafe_allow_html=True)
# Display news items
for idx, row in df.head(max_items).iterrows():
display_tradingview_news_card(row.to_dict())
# End scrollable container
st.markdown('</div>', 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."""
breaking = df[df['is_breaking'] == True] if not df.empty and 'is_breaking' in df.columns else pd.DataFrame()
if not breaking.empty:
latest = breaking.iloc[0]
# Escape HTML
summary = html_module.escape(latest.get('summary', '').strip())
source = html_module.escape(latest['source'])
url = html_module.escape(latest['url'])
# 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
banner_html = f"""
<style>
@keyframes pulse-glow {{
0%, 100% {{ box-shadow: 0 0 20px rgba(242, 54, 69, 0.6); }}
50% {{ box-shadow: 0 0 30px rgba(242, 54, 69, 0.9); }}
}}
@keyframes slide-in {{
from {{ transform: translateX(-10px); opacity: 0; }}
to {{ transform: translateX(0); opacity: 1; }}
}}
</style>
<div style="
background: linear-gradient(135deg, #F23645 0%, #C91B28 100%);
border: 2px solid #FF6B78;
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 24px;
animation: pulse-glow 2s ease-in-out infinite;
position: relative;
overflow: hidden;
">
<!-- Animated background pattern -->
<div style="
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(255, 255, 255, 0.03) 10px,
rgba(255, 255, 255, 0.03) 20px
);
pointer-events: none;
"></div>
<!-- Content -->
<div style="position: relative; z-index: 1;">
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;">
<!-- Animated icon -->
<div style="
font-size: 32px;
animation: pulse-glow 1s ease-in-out infinite;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
">🚨</div>
<!-- Header -->
<div style="flex: 1;">
<div style="
color: white;
font-size: 14px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 4px;
font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
">⚡ Breaking News</div>
<div style="
color: rgba(255, 255, 255, 0.9);
font-size: 11px;
display: flex;
align-items: center;
gap: 8px;
">
<span style="
background: rgba(255, 255, 255, 0.2);
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
">{source}</span>
<span style="opacity: 0.8;">•</span>
<span style="opacity: 0.8;">{time_ago}</span>
</div>
</div>
<!-- Read button -->
<a href="{url}" target="_blank" style="
background: white;
color: #F23645;
padding: 10px 20px;
border-radius: 6px;
font-size: 13px;
font-weight: 700;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0, 0, 0, 0.3)';"
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(0, 0, 0, 0.2)';">
READ NOW →
</a>
</div>
<!-- News summary -->
<div style="
color: white;
font-size: 16px;
font-weight: 500;
line-height: 1.5;
margin-left: 48px;
font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
animation: slide-in 0.5s ease-out;
">{summary}</div>
</div>
</div>
"""
st.markdown(banner_html, unsafe_allow_html=True)