| import streamlit as st |
| import pandas as pd |
| import numpy as np |
| import plotly.express as px |
| import plotly.graph_objects as go |
| import textwrap |
|
|
| |
| def apply_custom_style(): |
| st.markdown(""" |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap'); |
| |
| html, body, [class*="css"], .stMarkdown { |
| font-family: 'Outfit', sans-serif; |
| } |
| |
| .hud-card { |
| background: #1E293B !important; |
| border: 1px solid rgba(255, 255, 255, 0.15) !important; |
| border-radius: 12px; |
| padding: 16px 12px; |
| text-align: center; |
| box-shadow: 0 4px 25px rgba(0, 0, 0, 0.4); |
| transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); |
| margin-bottom: 15px; |
| min-height: 105px; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| color: #F1F5F9 !important; |
| } |
| .hud-card:hover { |
| border-color: rgba(0, 230, 118, 0.5); |
| transform: translateY(-2px); |
| box-shadow: 0 8px 30px rgba(0, 230, 118, 0.25); |
| } |
| .hud-title { |
| font-size: 0.75rem !important; |
| color: #CBD5E1 !important; |
| font-weight: 600 !important; |
| text-transform: uppercase; |
| letter-spacing: 0.08em; |
| margin-bottom: 6px; |
| } |
| .hud-value { |
| font-size: 1.45rem !important; |
| font-weight: 700 !important; |
| color: #FFFFFF !important; |
| line-height: 1.2; |
| word-wrap: break-word; |
| overflow-wrap: break-word; |
| } |
| |
| .outlook-card { |
| background: linear-gradient(135deg, #1E293B 0%, #0F172A 100%) !important; |
| border-left: 5px solid #FF3D00; |
| border-radius: 8px; |
| padding: 24px; |
| margin-bottom: 25px; |
| box-shadow: 0 6px 20px rgba(0,0,0,0.3); |
| border-top: 1px solid rgba(255,255,255,0.08); |
| border-right: 1px solid rgba(255,255,255,0.08); |
| border-bottom: 1px solid rgba(255,255,255,0.08); |
| color: #F1F5F9 !important; |
| } |
| |
| .section-header { |
| font-size: 1.6rem; |
| font-weight: 700; |
| border-bottom: 2px solid rgba(255,255,255,0.08); |
| padding-bottom: 10px; |
| margin-bottom: 20px; |
| color: #FFFFFF; |
| } |
| |
| /* Event Feed Card Styling */ |
| .anomaly-card { |
| background: #1E293B !important; |
| border: 1px solid rgba(255, 255, 255, 0.1) !important; |
| border-radius: 10px; |
| padding: 18px; |
| margin-bottom: 12px; |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); |
| transition: all 0.25s ease; |
| } |
| .anomaly-card:hover { |
| transform: translateX(4px); |
| } |
| |
| .badge-severity { |
| border-radius: 12px; |
| padding: 2px 10px; |
| font-size: 0.7rem; |
| font-weight: 700; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| display: inline-block; |
| } |
| .badge-critical { |
| background-color: rgba(255, 61, 0, 0.2); |
| color: #FF3D00; |
| border: 1px solid rgba(255, 61, 0, 0.5); |
| } |
| .badge-high { |
| background-color: rgba(255, 145, 0, 0.2); |
| color: #FF9100; |
| border: 1px solid rgba(255, 145, 0, 0.5); |
| } |
| .badge-medium { |
| background-color: rgba(255, 214, 0, 0.2); |
| color: #FFD600; |
| border: 1px solid rgba(255, 214, 0, 0.5); |
| } |
| .badge-low { |
| background-color: rgba(0, 176, 255, 0.2); |
| color: #00B0FF; |
| border: 1px solid rgba(0, 176, 255, 0.5); |
| } |
| |
| [data-testid="stMetric"] { |
| overflow: visible !important; |
| height: auto !important; |
| } |
| [data-testid="stMetricValue"], |
| [data-testid="stMetricValue"] > div, |
| [data-testid="stMetricValue"] * { |
| font-size: 1.35rem !important; |
| white-space: normal !important; |
| word-break: break-word !important; |
| overflow-wrap: break-word !important; |
| text-overflow: clip !important; |
| line-height: 1.25 !important; |
| } |
| [data-testid="stMetricLabel"], |
| [data-testid="stMetricLabel"] > div, |
| [data-testid="stMetricLabel"] * { |
| white-space: normal !important; |
| overflow-wrap: break-word !important; |
| } |
| |
| /* Enforce uniform size for all download buttons */ |
| div[data-testid="stDownloadButton"] button, |
| div[data-testid="stDownloadButton"] a, |
| .stDownloadButton button, |
| .stDownloadButton a, |
| div[data-testid="stDownloadButton"] { |
| width: 100% !important; |
| height: 65px !important; |
| min-height: 65px !important; |
| display: flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| text-align: center !important; |
| white-space: normal !important; |
| word-wrap: break-word !important; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| def get_severity_badge_class(cat): |
| c = str(cat).lower().strip() |
| if "critical" in c: |
| return "badge-critical" |
| elif "high" in c: |
| return "badge-high" |
| elif "medium" in c: |
| return "badge-medium" |
| else: |
| return "badge-low" |
|
|
| |
| def get_risk_level_color(risk): |
| r = str(risk).lower().strip() |
| if "high" in r: |
| return "#FF3D00" |
| elif "medium" in r or "moderate" in r: |
| return "#FFD600" |
| else: |
| return "#00B0FF" |
|
|
| |
| |
| |
| @st.cache_data |
| def load_data(): |
| """Loads and processes all anomaly intelligence datasets.""" |
| intel_path = "data/processed/anomaly_intelligence.parquet" |
| summary_path = "data/processed/anomaly_summary.parquet" |
| master_path = "data/processed/investment_intelligence_master.parquet" |
| |
| intel = pd.read_parquet(intel_path) |
| summary = pd.read_parquet(summary_path) |
| master = pd.read_parquet(master_path) |
| |
| |
| for df in [intel, summary, master]: |
| for col in ["Recommendation", "Recommendation_Final", "investment_signal", "historical_recommendation"]: |
| if col in df.columns: |
| df[col] = df[col].replace("Avoid", "Sell") |
| |
| |
| sector_map = master.set_index("Symbol")["Sector"].to_dict() |
| summary["Sector"] = summary["Symbol"].map(sector_map).fillna("Unknown") |
| intel["Sector"] = intel["Symbol"].map(sector_map).fillna("Unknown") |
| |
| |
| total_rec_map = master.set_index("Symbol")["total_records"].to_dict() |
| summary["total_records"] = summary["Symbol"].map(total_rec_map).fillna(1000) |
| |
| |
| |
| |
| |
| |
| max_count = summary["anomaly_count"].max() |
| if max_count > 0: |
| summary["norm_count"] = (summary["anomaly_count"] / max_count) * 100 |
| else: |
| summary["norm_count"] = 50.0 |
| |
| summary["Anomaly_Risk_Score"] = ( |
| (summary["norm_count"] * 0.4) + |
| (summary["avg_severity"] * 0.4) + |
| (summary["max_severity"] * 0.2) |
| ).clip(0.0, 100.0) |
| |
| |
| |
| sector_grp = summary.groupby("Sector").agg({ |
| "anomaly_count": "mean", |
| "avg_severity": "mean" |
| }).reset_index() |
| |
| max_sec_count = sector_grp["anomaly_count"].max() |
| if max_sec_count > 0: |
| sector_grp["norm_sec_count"] = (sector_grp["anomaly_count"] / max_sec_count) * 100 |
| else: |
| sector_grp["norm_sec_count"] = 50.0 |
| |
| sector_grp["Sector_Risk_Score"] = ( |
| (sector_grp["norm_sec_count"] * 0.5) + |
| (sector_grp["avg_severity"] * 0.5) |
| ).clip(0.0, 100.0) |
| |
| sector_risk_map = sector_grp.set_index("Sector")["Sector_Risk_Score"].to_dict() |
| summary["Sector_Risk_Score"] = summary["Sector"].map(sector_risk_map).fillna(50.0) |
| intel["Sector_Risk_Score"] = intel["Sector"].map(sector_risk_map).fillna(50.0) |
| |
| |
| |
| summary["raw_freq"] = summary["anomaly_count"] / summary["total_records"] |
| max_freq = summary["raw_freq"].max() |
| if max_freq > 0: |
| summary["Event_Frequency_Score"] = (summary["raw_freq"] / max_freq) * 100 |
| else: |
| summary["Event_Frequency_Score"] = 50.0 |
| summary["Event_Frequency_Score"] = summary["Event_Frequency_Score"].clip(0.0, 100.0) |
| |
| |
| min_change = intel["risk_change"].min() |
| max_change = intel["risk_change"].max() |
| if max_change != min_change: |
| intel["norm_risk_change"] = (intel["risk_change"] - min_change) / (max_change - min_change) * 100 |
| else: |
| intel["norm_risk_change"] = 50.0 |
| |
| intel["Alert_Priority_Score"] = ( |
| (intel["severity_score"] * 0.7) + |
| (intel["norm_risk_change"].fillna(0.0) * 0.3) |
| ).fillna(0.0).clip(0.0, 100.0) |
| |
| return intel, summary, master, sector_grp |
|
|
| |
| |
| |
| def show_anomaly_overview(intel, summary): |
| """Renders the dashboard metrics summary header.""" |
| st.markdown('<div class="section-header">π¨ Anomaly Overview</div>', unsafe_allow_html=True) |
| |
| total_anom = len(intel) |
| crit_events = len(intel[intel["severity_category"].str.lower().str.strip() == "critical"]) |
| high_events = len(intel[intel["severity_category"].str.lower().str.strip() == "high"]) |
| affected_stocks = intel["Symbol"].nunique() |
| avg_sev = intel["severity_score"].mean() |
| |
| most_anom_row = summary.sort_values(by="anomaly_count", ascending=False).iloc[0] |
| most_anom_stock = most_anom_row["Symbol"] |
| most_anom_count = most_anom_row["anomaly_count"] |
| |
| col1, col2, col3, col4, col5, col6 = st.columns(6) |
| with col1: |
| st.metric(label="Total Anomalies", value=f"{total_anom:,}") |
| with col2: |
| st.metric(label="Critical Events", value=f"{crit_events}") |
| with col3: |
| st.metric(label="High Severity Events", value=f"{high_events}") |
| with col4: |
| st.metric(label="Affected Stocks", value=f"{affected_stocks} / 49") |
| with col5: |
| st.metric(label="Average Severity", value=f"{avg_sev:.1f}") |
| with col6: |
| st.metric(label="Most Anomalous", value=most_anom_stock, delta=f"{most_anom_count} events") |
|
|
| |
| |
| |
| def show_event_feed(intel): |
| """Renders a real-time visual feed of latest anomalies using custom alert cards.""" |
| st.markdown('<div class="section-header">π° Real-Time Event Feed</div>', unsafe_allow_html=True) |
| |
| |
| latest_anoms = intel.sort_values(by="Date", ascending=False).head(20).reset_index(drop=True) |
| |
| |
| for idx, row in latest_anoms.head(6).iterrows(): |
| b_class = get_severity_badge_class(row["severity_category"]) |
| alert_pri = row['Alert_Priority_Score'] |
| if pd.isna(alert_pri): |
| alert_pri = 0.0 |
| |
| card_html = f""" |
| <div class="anomaly-card"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> |
| <div> |
| <span style="font-size: 1.15rem; font-weight: 700; color: #FFFFFF;">{row['Symbol']}</span> |
| <span style="color: #CBD5E1; font-size: 0.8rem; margin-left: 10px;">({row['Sector']})</span> |
| </div> |
| <div> |
| <span class="badge-severity {b_class}">{row['severity_category']}</span> |
| </div> |
| </div> |
| <div style="display: flex; gap: 15px; margin-bottom: 10px; font-size: 0.85rem; color: #CBD5E1;"> |
| <div>Event: <strong style="color: #FFFFFF;">{row['anomaly_type']}</strong></div> |
| <div>Severity Score: <strong style="color: #FFFFFF;">{row['severity_score']:.1f}</strong></div> |
| <div>Regime: <strong style="color: #FFFFFF;">{row['market_regime']}</strong></div> |
| <div>Alert Priority: <strong style="color: #00B0FF;">{alert_pri:.1f}</strong></div> |
| </div> |
| <div style="font-size: 0.9rem; line-height: 1.5; color: #F1F5F9; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 8px;"> |
| <strong>Alert:</strong> {row['anomaly_explanation']} |
| </div> |
| </div> |
| """ |
| st.markdown(card_html, unsafe_allow_html=True) |
| |
| |
| if len(latest_anoms) > 6: |
| with st.expander("βΌ Reveal Rest of the Alert Events"): |
| for idx, row in latest_anoms.iloc[6:].iterrows(): |
| b_class = get_severity_badge_class(row["severity_category"]) |
| alert_pri = row['Alert_Priority_Score'] |
| if pd.isna(alert_pri): |
| alert_pri = 0.0 |
| |
| card_html = f""" |
| <div class="anomaly-card"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> |
| <div> |
| <span style="font-size: 1.15rem; font-weight: 700; color: #FFFFFF;">{row['Symbol']}</span> |
| <span style="color: #CBD5E1; font-size: 0.8rem; margin-left: 10px;">({row['Sector']})</span> |
| </div> |
| <div> |
| <span class="badge-severity {b_class}">{row['severity_category']}</span> |
| </div> |
| </div> |
| <div style="display: flex; gap: 15px; margin-bottom: 10px; font-size: 0.85rem; color: #CBD5E1;"> |
| <div>Event: <strong style="color: #FFFFFF;">{row['anomaly_type']}</strong></div> |
| <div>Severity Score: <strong style="color: #FFFFFF;">{row['severity_score']:.1f}</strong></div> |
| <div>Regime: <strong style="color: #FFFFFF;">{row['market_regime']}</strong></div> |
| <div>Alert Priority: <strong style="color: #00B0FF;">{alert_pri:.1f}</strong></div> |
| </div> |
| <div style="font-size: 0.9rem; line-height: 1.5; color: #F1F5F9; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 8px;"> |
| <strong>Alert:</strong> {row['anomaly_explanation']} |
| </div> |
| </div> |
| """ |
| st.markdown(card_html, unsafe_allow_html=True) |
|
|
| |
| |
| |
| def show_stock_anomalies(selected_symbol, summary, intel): |
| """Renders customized metrics and complete anomaly timeline history for a selected stock.""" |
| st.markdown(f"### π Stock Anomaly Explorer: {selected_symbol}") |
| |
| stock_sum = summary[summary["Symbol"] == selected_symbol].iloc[0] |
| stock_intel = intel[intel["Symbol"] == selected_symbol].sort_values(by="Date", ascending=False) |
| |
| col1, col2, col3, col4, col5 = st.columns(5) |
| with col1: |
| st.metric(label="Total Anomalies", value=f"{stock_sum['anomaly_count']}") |
| with col2: |
| st.metric(label="Average Severity", value=f"{stock_sum['avg_severity']:.1f}") |
| with col3: |
| st.metric(label="Maximum Severity", value=f"{stock_sum['max_severity']:.1f}") |
| with col4: |
| st.markdown(f""" |
| <div class="hud-card" style="min-height: 95px; padding: 10px 5px !important; margin-bottom: 0px;"> |
| <div class="hud-title" style="font-size: 0.7rem !important; margin-bottom: 4px;">Risk Level</div> |
| <div class="hud-value" style="font-size: 1.15rem !important; color: {get_risk_level_color(stock_sum['risk_level'])};">{stock_sum['risk_level']}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| with col5: |
| st.metric(label="Anomaly Risk Score", value=f"{stock_sum['Anomaly_Risk_Score']:.1f}") |
| |
| if not stock_intel.empty: |
| latest_event = stock_intel.iloc[0] |
| latest_date = latest_event["Date"].strftime("%d-%b-%Y") |
| st.info(f"π
**Latest Event Detected ({latest_date}):** {latest_event['anomaly_explanation']} (Severity: {latest_event['severity_score']:.1f})") |
| |
| |
| st.markdown("**Complete Anomaly History**") |
| disp_cols = ["Date", "anomaly_type", "severity_score", "severity_category", "Alert_Priority_Score", "anomaly_explanation"] |
| |
| col_config = { |
| "Date": st.column_config.DateColumn("Date", format="DD-MMM-YYYY"), |
| "anomaly_type": st.column_config.TextColumn("Anomaly Event Type"), |
| "severity_score": st.column_config.NumberColumn("Severity Score", format="%.1f"), |
| "severity_category": st.column_config.TextColumn("Category"), |
| "Alert_Priority_Score": st.column_config.ProgressColumn("Priority Score", format="%.1f", min_value=0.0, max_value=100.0), |
| "anomaly_explanation": st.column_config.TextColumn("Alert Explanation", width="large") |
| } |
| |
| st.dataframe( |
| stock_intel[disp_cols], |
| column_config=col_config, |
| hide_index=True, |
| use_container_width=True, |
| height=600 |
| ) |
| else: |
| st.info("No anomalies recorded for this stock.") |
|
|
| |
| |
| |
| def show_severity_intelligence(intel): |
| st.markdown('<div class="section-header">π― Severity Intelligence</div>', unsafe_allow_html=True) |
| |
| |
| sev_counts = intel["severity_category"].value_counts().reset_index() |
| sev_counts.columns = ["Category", "Count"] |
| |
| col_chart1, col_chart2 = st.columns(2) |
| with col_chart1: |
| |
| fig1 = px.histogram( |
| intel, |
| x="severity_score", |
| nbins=15, |
| title="Severity Score Distribution (All Anomalies)", |
| color_discrete_sequence=["#FF3D00"] |
| ) |
| fig1.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", |
| xaxis=dict(showgrid=False, title="Severity Score"), |
| yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"), |
| bargap=0.05, height=280, margin=dict(l=10, r=10, t=50, b=10) |
| ) |
| st.plotly_chart(fig1, use_container_width=True) |
| |
| with col_chart2: |
| |
| fig2 = px.pie( |
| sev_counts, |
| values="Count", |
| names="Category", |
| title="Severity Category breakdown", |
| color="Category", |
| color_discrete_map={ |
| "Critical": "#FF3D00", |
| "High": "#FF9100", |
| "Medium": "#FFD600", |
| "Low": "#00B0FF" |
| } |
| ) |
| fig2.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", |
| height=280, margin=dict(l=10, r=10, t=50, b=10) |
| ) |
| st.plotly_chart(fig2, use_container_width=True) |
| |
| |
| |
| intel_time = intel.copy() |
| intel_time["Month"] = intel_time["Date"].dt.to_period("M").astype(str) |
| monthly_avg = intel_time.groupby("Month")["severity_score"].mean().reset_index() |
| |
| fig3 = px.line( |
| monthly_avg, |
| x="Month", |
| y="severity_score", |
| title="Monthly Average Severity Score Trend", |
| color_discrete_sequence=["#FF9100"] |
| ) |
| fig3.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", |
| xaxis=dict(showgrid=False), |
| yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)", title="Average Severity"), |
| height=280, margin=dict(l=10, r=10, t=50, b=10) |
| ) |
| st.plotly_chart(fig3, use_container_width=True) |
|
|
| |
| |
| |
| def show_event_analytics_tabs(intel): |
| st.markdown('<div class="section-header">π Event Analytics Workspace</div>', unsafe_allow_html=True) |
| |
| tab1, tab2, tab3, tab4 = st.tabs([ |
| "β‘ Volatility Spikes", |
| "π Drawdown Events", |
| "π Return Outliers", |
| "π‘οΈ Risk Shocks" |
| ]) |
| |
| |
| types_mapping = { |
| "Volatility Spike": "Volatility Spike", |
| "Drawdown Shock": "Drawdown Shock", |
| "Return Outlier": "Return Outlier", |
| "Risk Shock": "Risk Shock" |
| } |
| |
| def render_tab_content(anom_type): |
| df_type = intel[intel["anomaly_type"] == anom_type] |
| count = len(df_type) |
| avg_sev = df_type["severity_score"].mean() if count > 0 else 0 |
| |
| |
| affected = df_type["Symbol"].value_counts().reset_index() |
| affected.columns = ["Symbol", "Event Count"] |
| |
| col_c1, col_c2 = st.columns([1, 2]) |
| with col_c1: |
| st.metric(label="Event Count", value=f"{count}") |
| st.metric(label="Average Severity", value=f"{avg_sev:.1f}") |
| with col_c2: |
| st.markdown(f"**Most Affected Assets ({anom_type})**") |
| st.dataframe( |
| affected.head(5), |
| hide_index=True, |
| use_container_width=True |
| ) |
| |
| with tab1: |
| render_tab_content("Volatility Spike") |
| with tab2: |
| render_tab_content("Drawdown Shock") |
| with tab3: |
| render_tab_content("Return Outlier") |
| with tab4: |
| render_tab_content("Risk Shock") |
|
|
| |
| |
| |
| def show_sector_analysis(summary, sector_grp): |
| """Renders sector anomaly counts, average severity and charts.""" |
| st.markdown('<div class="section-header">πΊοΈ Sector Anomaly Analysis</div>', unsafe_allow_html=True) |
| |
| most_volatile_row = sector_grp.sort_values(by="avg_severity", ascending=False).iloc[0] |
| safest_row = sector_grp.sort_values(by="avg_severity", ascending=True).iloc[0] |
| |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| st.metric(label="Total Anomalies (Avg/Sector)", value=f"{sector_grp['anomaly_count'].mean():.1f}") |
| with col2: |
| st.metric(label="Average Sector Severity", value=f"{sector_grp['avg_severity'].mean():.1f}") |
| with col3: |
| st.metric(label="Most Volatile Sector", value=most_volatile_row["Sector"], delta=f"{most_volatile_row['avg_severity']:.1f} Avg Sev") |
| with col4: |
| st.metric(label="Safest Sector", value=safest_row["Sector"], delta=f"{safest_row['avg_severity']:.1f} Avg Sev", delta_color="inverse") |
| |
| col_chart1, col_chart2 = st.columns(2) |
| with col_chart1: |
| |
| fig1 = px.bar( |
| sector_grp.sort_values("anomaly_count", ascending=True), |
| x="anomaly_count", |
| y="Sector", |
| orientation="h", |
| title="Total Anomalies by Sector", |
| color="anomaly_count", |
| color_continuous_scale="Reds" |
| ) |
| fig1.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", |
| xaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"), |
| yaxis=dict(showgrid=False, title=None), |
| coloraxis_showscale=False, height=280, margin=dict(l=10, r=10, t=50, b=10) |
| ) |
| st.plotly_chart(fig1, use_container_width=True) |
| |
| with col_chart2: |
| |
| |
| fig2 = px.density_heatmap( |
| sector_grp, |
| x="Sector", |
| y="avg_severity", |
| z="avg_severity", |
| title="Average Severity Heatmap by Sector", |
| color_continuous_scale="Reds" |
| ) |
| fig2.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", |
| height=280, margin=dict(l=10, r=10, t=50, b=10) |
| ) |
| st.plotly_chart(fig2, use_container_width=True) |
|
|
| |
| |
| |
| def show_event_timeline(intel): |
| st.markdown('<div class="section-header">π°οΈ Event Timeline & Frequency</div>', unsafe_allow_html=True) |
| |
| |
| fig = px.scatter( |
| intel, |
| x="Date", |
| y="severity_score", |
| color="anomaly_type", |
| size="severity_score", |
| hover_name="Symbol", |
| title="Anomaly Timeline (Date vs Severity Score)", |
| color_discrete_map={ |
| "Volatility Spike": "#FF3D00", |
| "Drawdown Shock": "#00B0FF", |
| "Return Outlier": "#FF9100", |
| "Risk Shock": "#FFD600" |
| } |
| ) |
| fig.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", |
| xaxis=dict(showgrid=False), |
| yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)", title="Severity Score"), |
| height=320, margin=dict(l=10, r=10, t=50, b=10) |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
| |
| |
| intel_freq = intel.copy() |
| intel_freq["Month"] = intel_freq["Date"].dt.to_period("M").astype(str) |
| intel_freq["Year"] = intel_freq["Date"].dt.year |
| |
| monthly_cnt = intel_freq.groupby("Month").size().reset_index(name="Count") |
| yearly_cnt = intel_freq.groupby("Year").size().reset_index(name="Count") |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| fig_m = px.bar( |
| monthly_cnt, x="Month", y="Count", |
| title="Monthly Event Frequency Distribution", |
| color_discrete_sequence=["#00B0FF"] |
| ) |
| fig_m.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", |
| xaxis=dict(showgrid=False), |
| yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"), |
| height=260, margin=dict(l=10, r=10, t=40, b=10) |
| ) |
| st.plotly_chart(fig_m, use_container_width=True) |
| |
| with col2: |
| fig_y = px.bar( |
| yearly_cnt, x="Year", y="Count", |
| title="Yearly Event Frequency Distribution", |
| color_discrete_sequence=["#FFD600"] |
| ) |
| fig_y.update_layout( |
| template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", |
| xaxis=dict(showgrid=False), |
| yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"), |
| height=260, margin=dict(l=10, r=10, t=40, b=10) |
| ) |
| st.plotly_chart(fig_y, use_container_width=True) |
|
|
| |
| |
| |
| def show_critical_alerts(intel): |
| """Renders the top 20 alerts ranked by severity.""" |
| st.markdown('<div class="section-header">β οΈ Critical Alerts Center</div>', unsafe_allow_html=True) |
| |
| |
| critical_df = intel.sort_values(by="severity_score", ascending=False).head(20) |
| |
| disp_cols = ["Date", "Symbol", "severity_score", "anomaly_type", "Alert_Priority_Score", "anomaly_explanation"] |
| |
| col_config = { |
| "Date": st.column_config.DateColumn("Date", format="DD-MMM-YYYY"), |
| "Symbol": st.column_config.TextColumn("Stock"), |
| "severity_score": st.column_config.NumberColumn("Severity Score", format="%.1f"), |
| "anomaly_type": st.column_config.TextColumn("Anomaly Event"), |
| "Alert_Priority_Score": st.column_config.ProgressColumn("Priority Score", format="%.1f", min_value=0.0, max_value=100.0), |
| "anomaly_explanation": st.column_config.TextColumn("Advisory Alerts Detail", width="large") |
| } |
| |
| st.dataframe( |
| critical_df[disp_cols], |
| column_config=col_config, |
| hide_index=True, |
| use_container_width=True |
| ) |
|
|
| |
| |
| |
| def show_ai_event_report(intel, summary, sector_grp): |
| """Generates and displays AI executive strategizing briefs.""" |
| st.markdown('<div class="section-header">π§ AI Event Strategist</div>', unsafe_allow_html=True) |
| |
| total_anoms = len(intel) |
| unique_stocks = intel["Symbol"].nunique() |
| crit_cnt = len(intel[intel["severity_category"].str.lower().str.strip() == "critical"]) |
| |
| vol_cnt = len(intel[intel["anomaly_type"] == "Volatility Spike"]) |
| outlier_cnt = len(intel[intel["anomaly_type"] == "Return Outlier"]) |
| |
| most_volatile_sector = sector_grp.sort_values(by="avg_severity", ascending=False).iloc[0]["Sector"] |
| safest_sector = sector_grp.sort_values(by="avg_severity", ascending=True).iloc[0]["Sector"] |
| |
| st.markdown(f""" |
| <div class="outlook-card"> |
| <h3 style="color: #FF3D00; margin-top: 0; font-size: 1.25rem;">π§ Premium Market Surveillance & Event Assessment</h3> |
| <p style="font-size: 1.15rem; color: #F1F5F9; line-height: 1.6; margin-bottom: 0;"> |
| The surveillance engines have processed <strong>{total_anoms:,} anomalies</strong> across <strong>{unique_stocks} active assets</strong>. A total of <strong>{crit_cnt} critical alerts</strong> are currently logged, indicating localized market volatility regimes. |
| </p> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| c1, c2 = st.columns(2) |
| with c1: |
| st.markdown(f""" |
| <div style="background: #1E293B; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 20px; min-height: 200px;"> |
| <h4 style="color: #FF3D00; margin-top: 0; font-size: 1.1rem;">π Market Event Summary & Risk Commentary</h4> |
| <p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;"> |
| The bulk of identified anomalies originated from <strong>Volatility Spikes ({vol_cnt})</strong> and <strong>Return Outliers ({outlier_cnt})</strong>. This points toward dynamic shifts in volatility levels rather than structural breakdowns in downside support. |
| </p> |
| <h4 style="color: #00E676; margin-top: 15px; font-size: 1.1rem;">π‘ Opportunity Commentary</h4> |
| <p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;"> |
| Several high-severity return outliers may represent temporary price dislocations. Growth managers should track high-severity events on blue chips as entry windows rather than risk flags. |
| </p> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with c2: |
| st.markdown(f""" |
| <div style="background: #1E293B; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 20px; min-height: 200px;"> |
| <h4 style="color: #FF9100; margin-top: 0; font-size: 1.1rem;">π Sector Vulnerability Commentary</h4> |
| <p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;"> |
| Vulnerabilities are currently concentrated within the <strong>{most_volatile_sector}</strong> sector, yielding the highest average severity score. Defensive reallocations should favor the <strong>{safest_sector}</strong> cohort which remains extremely resilient. |
| </p> |
| <h4 style="color: #00B0FF; margin-top: 15px; font-size: 1.1rem;">π‘οΈ Defensive Signals</h4> |
| <p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;"> |
| Risk profiles show sideways volatility regime shifts inside consumer defense clusters, indicating robust protection flags. |
| </p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| |
| |
| def show_export_features(selected_symbol, intel): |
| st.markdown('<div class="section-header">π₯ Export Station</div>', unsafe_allow_html=True) |
| |
| col_e1, col_e2, col_e3 = st.columns(3) |
| |
| with col_e1: |
| feed_csv = intel.sort_values(by="Date", ascending=False).head(100)[["Date", "Symbol", "anomaly_type", "severity_score", "severity_category", "anomaly_explanation"]].to_csv(index=False).encode("utf-8") |
| st.download_button( |
| label="Download Anomaly Feed", |
| data=feed_csv, |
| file_name="investiq_anomaly_feed.csv", |
| mime="text/csv", |
| key="dl-anom-feed", |
| use_container_width=True |
| ) |
| |
| with col_e2: |
| crit_csv = intel.sort_values(by="severity_score", ascending=False).head(50)[["Date", "Symbol", "severity_score", "anomaly_type", "Alert_Priority_Score", "anomaly_explanation"]].to_csv(index=False).encode("utf-8") |
| st.download_button( |
| label="Download Critical Alerts", |
| data=crit_csv, |
| file_name="investiq_critical_alerts.csv", |
| mime="text/csv", |
| key="dl-crit-alerts", |
| use_container_width=True |
| ) |
| |
| with col_e3: |
| stock_csv = intel[intel["Symbol"] == selected_symbol].sort_values(by="Date", ascending=False)[["Date", "anomaly_type", "severity_score", "severity_category", "anomaly_explanation"]].to_csv(index=False).encode("utf-8") |
| st.download_button( |
| label="Download Stock Events", |
| data=stock_csv, |
| file_name=f"investiq_{selected_symbol}_anomaly_events.csv", |
| mime="text/csv", |
| key="dl-stock-events", |
| use_container_width=True |
| ) |
|
|
| |
| |
| |
| def show_anomaly_intelligence_workstation(): |
| """Main Streamlit execution point for Section 8 Page.""" |
| apply_custom_style() |
| |
| st.title("π¨ Anomaly Intelligence Workstation") |
| st.markdown("<p style='color: #CBD5E1; font-size: 1.1rem; margin-top: -10px;'>Monitor unusual market events, risk shocks, and abnormal price/volatility behaviors before they propagate.</p>", unsafe_allow_html=True) |
| |
| |
| try: |
| intel, summary, master, sector_grp = load_data() |
| except Exception as e: |
| st.error(f"Error loading anomaly datasets: {e}") |
| st.stop() |
| |
| |
| show_anomaly_overview(intel, summary) |
| st.markdown("---") |
| |
| |
| col_explorer, col_feed = st.columns([4, 6]) |
| |
| with col_explorer: |
| st.markdown('<div class="section-header">π Anomaly Explorer</div>', unsafe_allow_html=True) |
| selected_symbol = st.selectbox("Select Asset to Audit Anomalies", summary["Symbol"].unique()) |
| show_stock_anomalies(selected_symbol, summary, intel) |
| |
| with col_feed: |
| show_event_feed(intel) |
| |
| st.markdown("---") |
| |
| |
| show_severity_intelligence(intel) |
| st.markdown("---") |
| |
| |
| show_event_analytics_tabs(intel) |
| st.markdown("---") |
| |
| |
| show_sector_analysis(summary, sector_grp) |
| st.markdown("---") |
| |
| |
| show_event_timeline(intel) |
| st.markdown("---") |
| |
| |
| show_critical_alerts(intel) |
| st.markdown("---") |
| |
| |
| show_ai_event_report(intel, summary, sector_grp) |
| st.markdown("---") |
| |
| |
| show_export_features(selected_symbol, intel) |
|
|