| | """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.""" |
| |
|
| | |
| | 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_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') |
| |
|
| | |
| | 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']) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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>""" |
| |
|
| | |
| | st.markdown(header_html, unsafe_allow_html=True) |
| |
|
| | |
| | news_cards_html = "" |
| | for idx, row in df.head(max_items).iterrows(): |
| | news_item = row.to_dict() |
| |
|
| | |
| | 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_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') |
| |
|
| | |
| | title = html_module.escape(str(news_item.get('title', '')).strip()) |
| | summary = html_module.escape(str(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 'β' |
| |
|
| | |
| | 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: #E0E3EB; font-size: 14px; font-weight: 600; margin-bottom: 6px; margin-left: 8px; font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;">{title if title else summary}</div> |
| | <div style="color: #D1D4DC; font-size: 13px; line-height: 1.5; margin-bottom: 8px; margin-left: 8px; font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif;">{summary if title else ''}</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> |
| | """ |
| |
|
| | |
| | import random |
| | unique_id = f"news-scroll-{random.randint(10000, 99999)}" |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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.""" |
| |
|
| | |
| | |
| | |
| | if not df.empty: |
| | latest = df.iloc[0] |
| |
|
| | |
| | summary = html_module.escape(latest.get('summary', '').strip()) |
| | source = html_module.escape(latest['source']) |
| | url = html_module.escape(latest['url']) |
| |
|
| | |
| | impact_score = latest.get('breaking_score', 0) |
| | score_display = f"{impact_score:.1f}" if impact_score > 0 else "N/A" |
| |
|
| | |
| | if impact_score >= 80: |
| | score_color = "#FF3B30" |
| | score_label = "CRITICAL" |
| | elif impact_score >= 60: |
| | score_color = "#FF9500" |
| | score_label = "HIGH" |
| | elif impact_score >= 40: |
| | score_color = "#FFCC00" |
| | score_label = "MEDIUM" |
| | else: |
| | score_color = "#34C759" |
| | score_label = "LOW" |
| |
|
| | |
| | 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" |
| |
|
| | |
| | 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.""" |
| |
|
| | |
| | title = html_module.escape(prediction_item.get('title', '').strip()) |
| | source = html_module.escape(prediction_item['source']) |
| | url = html_module.escape(prediction_item['url']) |
| |
|
| | |
| | yes_prob = prediction_item.get('yes_probability', 50.0) |
| | no_prob = prediction_item.get('no_probability', 50.0) |
| |
|
| | |
| | if yes_prob > 60: |
| | bar_color = '#089981' |
| | sentiment_text = 'YES LIKELY' |
| | elif no_prob > 60: |
| | bar_color = '#F23645' |
| | sentiment_text = 'NO LIKELY' |
| | else: |
| | bar_color = '#FF9800' |
| | sentiment_text = 'BALANCED' |
| |
|
| | |
| | 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 = 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 = "" |
| |
|
| | |
| | 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.""" |
| |
|
| | |
| | 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', '')) |
| |
|
| | |
| | forecast = event_item.get('forecast') |
| | previous = event_item.get('previous') |
| | actual = event_item.get('actual') |
| | importance = event_item.get('importance', 'medium') |
| |
|
| | |
| | importance_colors = { |
| | 'high': '#F23645', |
| | 'medium': '#FF9800', |
| | 'low': '#787B86' |
| | } |
| | importance_color = importance_colors.get(importance, '#787B86') |
| |
|
| | |
| | time_to_event = event_item.get('time_to_event', '') |
| |
|
| | |
| | def format_value(val): |
| | if val is None: |
| | return '-' |
| | if isinstance(val, (int, float)): |
| | |
| | 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) |
| |
|
| | |
| | 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_flags = { |
| | 'US': 'πΊπΈ', |
| | 'EU': 'πͺπΊ', |
| | 'UK': 'π¬π§', |
| | 'JP': 'π―π΅', |
| | 'CN': 'π¨π³', |
| | 'CA': 'π¨π¦', |
| | 'AU': 'π¦πΊ' |
| | } |
| | flag = country_flags.get(country, 'π') |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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>""" |
| |
|
| | |
| | for idx, event in events_df.head(10).iterrows(): |
| | |
| | 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 = { |
| | 'US': 'πΊπΈ', |
| | 'EU': 'πͺπΊ', |
| | 'UK': 'π¬π§', |
| | 'JP': 'π―π΅', |
| | 'CN': 'π¨π³' |
| | } |
| | flag = country_flags.get(country, 'π') |
| |
|
| | |
| | stars = 'β' * ({'high': 3, 'medium': 2, 'low': 1}.get(importance, 1)) |
| |
|
| | |
| | forecast_display = f"{forecast:.1f}" if forecast is not None else "N/A" |
| |
|
| | |
| | importance_color = '#F23645' if importance == 'high' else '#FF9800' if importance == 'medium' else '#787B86' |
| |
|
| | |
| | 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) |
| |
|