Dmitry Beresnev
fix economic calendar
cfe2c87
raw
history blame
30 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
# 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)