Dmitry Beresnev
fix economic calendar
cfe2c87
"""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
# Build header HTML (no leading whitespace)
header_html = 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;">{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>"""
# 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"""<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';">
<div style="position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: {impact_color};"></div>
<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;">{sentiment_symbol} {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>
<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>
<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>
"""
# 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"""<style>
.{unique_id} {{
height: {height};
overflow-y: auto;
background: #0D0E13;
border: 1px solid #2A2E39;
border-top: none;
border-radius: 0 0 8px 8px;
padding: 16px;
}}
.{unique_id}::-webkit-scrollbar {{
width: 8px;
}}
.{unique_id}::-webkit-scrollbar-track {{
background: #1E222D;
border-radius: 4px;
}}
.{unique_id}::-webkit-scrollbar-thumb {{
background: #363A45;
border-radius: 4px;
}}
.{unique_id}::-webkit-scrollbar-thumb:hover {{
background: #434651;
}}
</style>
<div class="{unique_id}">
{news_cards_html}
</div>
"""
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"""<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;">
<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>
<div style="position: relative; z-index: 1;">
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;">
<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>
<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; flex-wrap: wrap;">
<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>
<span style="opacity: 0.8;">β€’</span>
<span style="background: {score_color}; color: white; padding: 2px 8px; border-radius: 4px; font-weight: 700; font-size: 10px; letter-spacing: 0.5px;">πŸ“Š IMPACT: {score_display}/100 ({score_label})</span>
</div>
</div>
<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>
<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)
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"""
<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;
" onmouseover="this.style.borderColor='#3861FB'; this.style.transform='translateY(-2px)';"
onmouseout="this.style.borderColor='#2A2E39'; this.style.transform='translateY(0)';">
<!-- Header -->
<div style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px;">
<span style="color: #3861FB; font-weight: 600; font-size: 13px;">{source}</span>
<span style="
background: {bar_color};
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
">{sentiment_text}</span>
</div>
<div style="color: #D1D4DC; font-size: 14px; font-weight: 500; line-height: 1.4; margin-bottom: 8px;">
{title}
</div>
</div>
<!-- Probability Visualization -->
<div style="margin-bottom: 10px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span style="color: #089981; font-size: 12px; font-weight: 600;">YES {yes_prob:.1f}%</span>
<span style="color: #F23645; font-size: 12px; font-weight: 600;">NO {no_prob:.1f}%</span>
</div>
<!-- Horizontal probability bar -->
<div style="
display: flex;
height: 8px;
border-radius: 4px;
overflow: hidden;
background: #2A2E39;
">
<div style="
width: {yes_prob}%;
background: #089981;
transition: width 0.3s ease;
"></div>
<div style="
width: {no_prob}%;
background: #F23645;
transition: width 0.3s ease;
"></div>
</div>
</div>
<!-- Footer info -->
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="color: #787B86; font-size: 11px;">
{end_date_display}{" β€’ " + volume_display if volume_display and end_date_display else volume_display}
</div>
<a href="{url}" target="_blank" style="
color: #3861FB;
font-size: 11px;
font-weight: 600;
text-decoration: none;
">View Market β†’</a>
</div>
</div>
"""
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 = '<span style="color: #089981; font-weight: 700;">[BEAT]</span>'
elif actual < forecast:
beat_miss_html = '<span style="color: #F23645; font-weight: 700;">[MISS]</span>'
# Country flag emojis
country_flags = {
'US': 'πŸ‡ΊπŸ‡Έ',
'EU': 'πŸ‡ͺπŸ‡Ί',
'UK': 'πŸ‡¬πŸ‡§',
'JP': 'πŸ‡―πŸ‡΅',
'CN': 'πŸ‡¨πŸ‡³',
'CA': 'πŸ‡¨πŸ‡¦',
'AU': 'πŸ‡¦πŸ‡Ί'
}
flag = country_flags.get(country, '🌍')
# Event 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;
" onmouseover="this.style.borderColor='#3861FB'; this.style.transform='translateY(-2px)';"
onmouseout="this.style.borderColor='#2A2E39'; this.style.transform='translateY(0)';">
<!-- Header -->
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<span style="font-size: 20px;">{flag}</span>
<span style="
background: {importance_color};
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
">{importance.upper()}</span>
</div>
<div style="color: #D1D4DC; font-size: 14px; font-weight: 500; line-height: 1.4;">
{title}
</div>
</div>
{f'<div style="color: #3861FB; font-size: 12px; font-weight: 600; white-space: nowrap; margin-left: 12px;">{time_to_event}</div>' if time_to_event else ''}
</div>
<!-- Values comparison -->
<div style="background: #0D0E13; border-radius: 6px; padding: 10px; margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: #787B86; font-size: 11px;">Forecast:</span>
<span style="color: #D1D4DC; font-size: 12px; font-weight: 600;">{forecast_display}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: #787B86; font-size: 11px;">Previous:</span>
<span style="color: #D1D4DC; font-size: 12px; font-weight: 600;">{previous_display}</span>
</div>
{f'<div style="display: flex; justify-content: space-between;"><span style="color: #787B86; font-size: 11px;">Actual:</span><span style="color: #D1D4DC; font-size: 12px; font-weight: 600;">{actual_display} {beat_miss_html}</span></div>' if actual is not None else ''}
</div>
</div>
"""
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 = """<div style="background: linear-gradient(135deg, #1E222D 0%, #131722 100%); border: 1px solid #2A2E39; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
<div style="margin-bottom: 16px;">
<h3 style="color: #D1D4DC; font-size: 18px; font-weight: 600; margin: 0;">πŸ“… Economic Calendar</h3>
<p style="color: #787B86; font-size: 13px; margin: 4px 0 0 0;">Upcoming high-impact events</p>
</div>"""
# 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"""<div style="background: #0D0E13; border-left: 3px solid {importance_color}; border-radius: 6px; padding: 12px; margin-bottom: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<div style="color: #D1D4DC; font-size: 13px; font-weight: 500; margin-bottom: 4px;">{flag} {event_name}</div>
<div style="color: #787B86; font-size: 11px;">{stars} Forecast: {forecast_display}</div>
</div>
<div style="color: #3861FB; font-size: 12px; font-weight: 600; white-space: nowrap; margin-left: 12px;">{time_to_event}</div>
</div>
</div>
"""
widget_html += event_html
widget_html += "</div>"
st.markdown(widget_html, unsafe_allow_html=True)