InvestAi / anomaly_intelligence_module.py
Hardik-25's picture
Upload 69 files
8542a36 verified
Raw
History Blame Contribute Delete
37.9 kB
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
# Custom Stylesheet Application helper (mirrors app.py custom style)
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)
# Helper function to get severity badge class
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"
# Helper function to map risk level colors
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"
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: load_data()
# ----------------------------------------------------
@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)
# Clean recommendation values globally
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")
# Add Sector from master to summary and intel
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")
# Extract total_records from master for frequency calculation
total_rec_map = master.set_index("Symbol")["total_records"].to_dict()
summary["total_records"] = summary["Symbol"].map(total_rec_map).fillna(1000)
# ----------------------------------------------------
# ADVANCED ANALYTICS SCORE CALCULATIONS
# ----------------------------------------------------
# 1. Anomaly Risk Score (0-100)
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)
# 2. Sector Risk Score (0-100)
# Computed dynamically based on Sector averages
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)
# 3. Event Frequency Score (0-100)
# Frequency = anomalies / total records, normalized
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)
# 4. Alert Priority Score (0-100) (calculated on individual events)
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
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_anomaly_overview()
# ----------------------------------------------------
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")
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_event_feed()
# ----------------------------------------------------
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)
# Sort by Date descending to get latest anomalies
latest_anoms = intel.sort_values(by="Date", ascending=False).head(20).reset_index(drop=True)
# Render first 6 cards directly to match the height of the anomaly history table (600px)
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)
# Render the remaining items inside an expander
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)
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_stock_anomalies()
# ----------------------------------------------------
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})")
# Chronological layout table
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.")
# ----------------------------------------------------
# Severity Intelligence
# ----------------------------------------------------
def show_severity_intelligence(intel):
st.markdown('<div class="section-header">🎯 Severity Intelligence</div>', unsafe_allow_html=True)
# Compute distribution counts
sev_counts = intel["severity_category"].value_counts().reset_index()
sev_counts.columns = ["Category", "Count"]
col_chart1, col_chart2 = st.columns(2)
with col_chart1:
# Severity Histogram
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:
# Severity Category Pie Chart
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)
# Severity Trend Over Time
# Aggregate average severity daily/weekly
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)
# ----------------------------------------------------
# Event Analytics Tabs
# ----------------------------------------------------
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: 'Volatility Spike' 'Drawdown Shock' 'Return Outlier' 'Risk Shock'
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
# Most Affected Stocks
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")
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_sector_analysis()
# ----------------------------------------------------
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:
# Anomalies by Sector
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:
# Severity by Sector Heatmap
# Pivot table of sector average metrics
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)
# ----------------------------------------------------
# Event Timeline
# ----------------------------------------------------
def show_event_timeline(intel):
st.markdown('<div class="section-header">πŸ•°οΈ Event Timeline & Frequency</div>', unsafe_allow_html=True)
# Interactive Timeline Scatter Chart
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)
# Frequency aggregations
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)
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_critical_alerts()
# ----------------------------------------------------
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)
# Sort by severity score desc
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
)
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_ai_event_report()
# ----------------------------------------------------
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)
# ----------------------------------------------------
# Export Features
# ----------------------------------------------------
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
)
# ----------------------------------------------------
# MAIN CALLABLE: show_anomaly_intelligence_workstation()
# ----------------------------------------------------
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)
# Load cached datasets
try:
intel, summary, master, sector_grp = load_data()
except Exception as e:
st.error(f"Error loading anomaly datasets: {e}")
st.stop()
# 1. Anomaly Overview
show_anomaly_overview(intel, summary)
st.markdown("---")
# 2. Event Feed & Explorer Split (Layout split: 40% Explorer, 60% Feed)
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("---")
# 4. Severity Intelligence
show_severity_intelligence(intel)
st.markdown("---")
# 5. Event Analytics Workspace
show_event_analytics_tabs(intel)
st.markdown("---")
# 6. Sector Anomaly Analysis
show_sector_analysis(summary, sector_grp)
st.markdown("---")
# 7. Event Timeline & Frequency
show_event_timeline(intel)
st.markdown("---")
# 8. Critical Alerts Center
show_critical_alerts(intel)
st.markdown("---")
# 9. AI Event Strategist
show_ai_event_report(intel, summary, sector_grp)
st.markdown("---")
# 10. Export Features
show_export_features(selected_symbol, intel)