InvestAi / recommendation_evolution_module.py
Hardik-25's picture
Upload 69 files
8542a36 verified
Raw
History Blame Contribute Delete
42.8 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 #00E676;
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;
}
.badge {
border-radius: 12px;
padding: 4px 12px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
display: inline-block;
}
.badge-strong-buy {
background-color: rgba(0, 230, 118, 0.2);
color: #00E676;
border: 1px solid rgba(0, 230, 118, 0.5);
}
.badge-buy {
background-color: rgba(0, 176, 255, 0.2);
color: #00B0FF;
border: 1px solid rgba(0, 176, 255, 0.5);
}
.badge-hold {
background-color: rgba(255, 160, 0, 0.2);
color: #FFA000;
border: 1px solid rgba(255, 160, 0, 0.5);
}
.badge-reduce {
background-color: rgba(233, 30, 99, 0.2);
color: #E91E63;
border: 1px solid rgba(233, 30, 99, 0.5);
}
.badge-avoid {
background-color: rgba(244, 67, 54, 0.2);
color: #F44336;
border: 1px solid rgba(244, 67, 54, 0.5);
}
.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;
}
[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 badge class based on recommendation
def get_badge_class(rec):
rec_clean = str(rec).lower().strip()
if "strong buy" in rec_clean:
return "badge-strong-buy"
elif "buy" in rec_clean:
return "badge-buy"
elif "hold" in rec_clean:
return "badge-hold"
elif "reduce" in rec_clean:
return "badge-reduce"
else:
return "badge-avoid"
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: load_data()
# ----------------------------------------------------
@st.cache_data
def load_data():
"""Loads and processes all recommendation evolution datasets."""
summary_path = "data/processed/recommendation_summary.parquet"
evolution_path = "data/processed/recommendation_evolution.parquet"
master_path = "data/processed/investment_intelligence_master.parquet"
summary = pd.read_parquet(summary_path)
evolution = pd.read_parquet(evolution_path)
master = pd.read_parquet(master_path)
# Map 'Avoid' to 'Sell' globally
for df in [summary, evolution, master]:
for col in ["historical_recommendation", "Recommendation", "Recommendation_Final", "investment_signal"]:
if col in df.columns:
df[col] = df[col].replace("Avoid", "Sell")
# Add Sector from master to summary
sector_map = master.set_index("Symbol")["Sector"].to_dict()
summary["Sector"] = summary["Symbol"].map(sector_map).fillna("Unknown")
# Advanced Score Calculations
# 1. Scaled Metrics
min_evol = summary["evolution_score"].min()
max_evol = summary["evolution_score"].max()
if max_evol != min_evol:
summary["scaled_evolution"] = (summary["evolution_score"] - min_evol) / (max_evol - min_evol) * 100
else:
summary["scaled_evolution"] = 50.0
min_conf = summary["confidence_score"].min()
max_conf = summary["confidence_score"].max()
if max_conf != min_conf:
summary["scaled_confidence"] = (summary["confidence_score"] - min_conf) / (max_conf - min_conf) * 100
else:
summary["scaled_confidence"] = 50.0
# Trend Direction Value Mapping (Improving = 100, Stable = 50, Deteriorating = 0)
def map_trend_dir(val):
v = str(val).lower().strip()
if "improving" in v:
return 100.0
elif "deteriorating" in v or "weakening" in v:
return 0.0
else:
return 50.0
summary["trend_value"] = summary["trend_direction"].apply(map_trend_dir)
# A. Recommendation Momentum Score
summary["Recommendation_Momentum_Score"] = (
(summary["scaled_evolution"] * 0.5) +
(summary["scaled_confidence"] * 0.3) +
(summary["trend_value"] * 0.2)
).clip(0.0, 100.0)
# B. Recommendation Quality Score
# Stability Score is already a percentage (93-96), let's keep it raw or scale it to exaggerate differences.
min_stab = summary["stability_score"].min()
max_stab = summary["stability_score"].max()
if max_stab != min_stab:
summary["scaled_stability"] = (summary["stability_score"] - min_stab) / (max_stab - min_stab) * 100
else:
summary["scaled_stability"] = 50.0
summary["Recommendation_Quality_Score"] = (
(summary["scaled_stability"] * 0.4) +
(summary["scaled_confidence"] * 0.4) +
(summary["scaled_evolution"] * 0.2)
).clip(0.0, 100.0)
# C. Analyst Conviction Score
summary["Analyst_Conviction_Score"] = (
(summary["scaled_confidence"] * 0.6) +
(summary["evolution_percentile"] * 0.4)
).clip(0.0, 100.0)
return summary, evolution, master
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_evolution_overview()
# ----------------------------------------------------
def show_evolution_overview(summary, master):
"""Renders the top KPI metrics bar."""
st.markdown('<div class="section-header">๐Ÿ”„ Evolution Overview</div>', unsafe_allow_html=True)
avg_evo = summary["evolution_score"].mean()
improving_count = len(summary[summary["trend_direction"].str.lower().str.strip() == "improving"])
weakening_count = len(summary[summary["trend_direction"].str.lower().str.strip() == "deteriorating"])
avg_stab = summary["stability_score"].mean()
avg_conf = summary["confidence_score"].mean()
# Find Highest Conviction Stock based on Analyst Conviction Score
best_idx = summary["Analyst_Conviction_Score"].idxmax()
highest_conviction_stock = summary.loc[best_idx, "Symbol"]
highest_conviction_val = summary.loc[best_idx, "Analyst_Conviction_Score"]
col1, col2, col3, col4, col5, col6 = st.columns(6)
with col1:
st.metric(label="Avg Evolution Score", value=f"{avg_evo:.3f}")
with col2:
st.metric(label="Improving Stocks", value=f"{improving_count}")
with col3:
st.metric(label="Weakening Stocks", value=f"{weakening_count}")
with col4:
st.metric(label="Avg Stability Score", value=f"{avg_stab:.2f}%")
with col5:
st.metric(label="Avg Confidence", value=f"{avg_conf:.1f}")
with col6:
st.metric(label="Highest Conviction", value=f"{highest_conviction_stock}", delta=f"{highest_conviction_val:.1f} Score")
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_stock_timeline()
# ----------------------------------------------------
def show_stock_timeline(selected_symbol, summary, evolution):
"""Renders the historical timeline chart for a specific selected stock."""
# Filter evolution records for selected stock
stock_evo = evolution[evolution["Symbol"] == selected_symbol].sort_values("Date").reset_index(drop=True)
stock_sum = summary[summary["Symbol"] == selected_symbol].iloc[0]
st.markdown(f"### ๐Ÿ“ˆ Recommendation Timeline for {selected_symbol}")
col1, col2, col3, col4 = st.columns(4)
with col1:
badge = get_badge_class(stock_sum["historical_recommendation"])
st.markdown(f"""
<div class="hud-card">
<div class="hud-title">Current Recommendation</div>
<div class="hud-value"><span class="badge {badge}">{stock_sum["historical_recommendation"]}</span></div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
<div class="hud-card">
<div class="hud-title">Current Confidence</div>
<div class="hud-value" style="color: #00B0FF;">{stock_sum["confidence_score"]:.1f}</div>
</div>
""", unsafe_allow_html=True)
with col3:
trend = stock_sum["trend_direction"]
color = "#00E676" if "improving" in trend.lower() else ("#FF3D00" if "deteriorating" in trend.lower() else "#FFA000")
st.markdown(f"""
<div class="hud-card">
<div class="hud-title">Trend Direction</div>
<div class="hud-value" style="color: {color};">{trend}</div>
</div>
""", unsafe_allow_html=True)
with col4:
st.markdown(f"""
<div class="hud-card">
<div class="hud-title">Evolution Score</div>
<div class="hud-value" style="color: #FFFFFF;">{stock_sum["evolution_score"]:.4f}</div>
</div>
""", unsafe_allow_html=True)
# Plotly Timeline Chart (Ordered Recommendation Level over Time)
rec_order = ["Sell", "Reduce", "Hold", "Buy", "Strong Buy"]
fig = px.line(
stock_evo,
x="Date",
y="historical_recommendation",
markers=True,
title=f"Historical Recommendation Path of {selected_symbol}",
category_orders={"historical_recommendation": rec_order},
color_discrete_sequence=["#00E676"]
)
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)",
categoryarray=rec_order
),
height=350,
margin=dict(l=10, r=10, t=50, b=10)
)
fig.update_traces(
line=dict(shape="hv", width=3),
marker=dict(size=8, symbol="circle"),
hovertemplate="<b>Date: %{x}</b><br>Recommendation: %{y}<extra></extra>"
)
st.plotly_chart(fig, use_container_width=True)
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_confidence_analysis()
# ----------------------------------------------------
def show_confidence_analysis(selected_symbol, summary, evolution):
"""Renders confidence analytics charts and metrics."""
st.markdown('<div class="section-header">๐ŸŽฏ Confidence Intelligence</div>', unsafe_allow_html=True)
# selected stock evolution records
stock_evo = evolution[evolution["Symbol"] == selected_symbol].sort_values("Date").reset_index(drop=True)
avg_c = stock_evo["confidence_score"].mean()
max_c = stock_evo["confidence_score"].max()
min_c = stock_evo["confidence_score"].min()
c_col1, c_col2, c_col3 = st.columns(3)
with c_col1:
st.metric(label="Average Confidence (Stock)", value=f"{avg_c:.1f}")
with c_col2:
st.metric(label="Highest Confidence (Stock)", value=f"{max_c:.1f}")
with c_col3:
st.metric(label="Lowest Confidence (Stock)", value=f"{min_c:.1f}")
col_chart1, col_chart2 = st.columns(2)
with col_chart1:
# Confidence Through Time
fig1 = px.line(
stock_evo,
x="Date",
y="confidence_score",
title=f"Confidence Score Evolution: {selected_symbol}",
color_discrete_sequence=["#00B0FF"]
)
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),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig1, use_container_width=True)
with col_chart2:
# Confidence Distribution (All stocks)
fig2 = px.histogram(
summary,
x="confidence_score",
nbins=12,
title="System-Wide Confidence Score Distribution",
color_discrete_sequence=["#FFA000"]
)
fig2.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="Confidence Score"),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
bargap=0.05,
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig2, use_container_width=True)
# Confidence vs Recommendation (Scatter Plot)
fig3 = px.scatter(
summary,
x="confidence_score",
y="historical_recommendation",
color="historical_recommendation",
hover_name="Symbol",
title="Confidence Score vs Recommendation Category",
category_orders={"historical_recommendation": ["Sell", "Reduce", "Hold", "Buy", "Strong Buy"]},
color_discrete_map={
"Strong Buy": "#00E676",
"Buy": "#00B0FF",
"Hold": "#FFA000",
"Reduce": "#E91E63",
"Sell": "#F44336"
}
)
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=True, gridcolor="rgba(255,255,255,0.08)", title="Confidence Score"),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
height=320,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig3, use_container_width=True)
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_upgrade_analysis()
# ----------------------------------------------------
def show_upgrade_analysis(selected_symbol, summary, evolution):
"""Renders upgrade and downgrade timelines and comparisons."""
st.markdown('<div class="section-header">๐Ÿ“ˆ Upgrade / Downgrade Analysis</div>', unsafe_allow_html=True)
stock_sum = summary[summary["Symbol"] == selected_symbol].iloc[0]
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric(label="Upgrade Count (Stock)", value=f"{stock_sum['upgrade_count']}")
with col2:
st.metric(label="Downgrade Count (Stock)", value=f"{stock_sum['downgrade_count']}")
with col3:
st.metric(label="Net Recommendation Trend", value=f"{stock_sum['net_recommendation_trend']}", delta=int(stock_sum['net_recommendation_trend']))
with col4:
st.metric(label="Evolution Category", value=stock_sum["evolution_category"])
# Filter upgrades / downgrades for visualization
# We can aggregate changes by Month or Date from evolution where change_direction != 'No Change'
changes_df = evolution[evolution["change_direction"].isin(["Upgrade", "Downgrade"])].copy()
changes_df["Month"] = changes_df["Date"].dt.to_period("M").astype(str)
monthly_changes = changes_df.groupby(["Month", "change_direction"]).size().reset_index(name="Count")
col_chart1, col_chart2 = st.columns(2)
with col_chart1:
# Upgrade / Downgrade over time
fig1 = px.line(
monthly_changes,
x="Month",
y="Count",
color="change_direction",
title="Monthly System-Wide Upgrades & Downgrades Trend",
color_discrete_map={"Upgrade": "#00E676", "Downgrade": "#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, tickangle=-45),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig1, use_container_width=True)
with col_chart2:
# Aggregated Up vs Down comparison
overall_changes = changes_df["change_direction"].value_counts().reset_index()
overall_changes.columns = ["Change", "Count"]
fig2 = px.bar(
overall_changes,
x="Change",
y="Count",
color="Change",
title="System-Wide Upgrades vs Downgrades Comparison",
color_discrete_map={"Upgrade": "#00E676", "Downgrade": "#FF3D00"}
)
fig2.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)"),
showlegend=False,
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig2, use_container_width=True)
# ----------------------------------------------------
# Stability analysis
# ----------------------------------------------------
def show_stability_analysis(selected_symbol, summary, evolution):
st.markdown('<div class="section-header">โš–๏ธ Recommendation Stability Analysis</div>', unsafe_allow_html=True)
stock_sum = summary[summary["Symbol"] == selected_symbol].iloc[0]
stock_evo = evolution[evolution["Symbol"] == selected_symbol].sort_values("Date")
# Calculate numerical volatility: map categories to numbers
mapping = {"Sell": 1, "Reduce": 2, "Hold": 3, "Buy": 4, "Strong Buy": 5}
mapped_vals = stock_evo["historical_recommendation"].map(mapping).fillna(3)
rec_vol = mapped_vals.std()
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric(label="Stability Score (Stock)", value=f"{stock_sum['stability_score']:.2f}%")
with col2:
st.metric(label="Recommendation Changes", value=f"{stock_sum['recommendation_changes']}")
with col3:
st.metric(label="Total Records", value=f"{stock_sum['total_records']}")
with col4:
st.metric(label="Recommendation Volatility", value=f"{rec_vol:.3f}")
col_chart1, col_chart2 = st.columns(2)
with col_chart1:
# Stability Distribution
fig1 = px.histogram(
summary,
x="stability_score",
nbins=10,
title="System-Wide Stability Distribution",
color_discrete_sequence=["#00B0FF"]
)
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="Stability Score (%)"),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
bargap=0.05,
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig1, use_container_width=True)
with col_chart2:
# Changes Distribution
fig2 = px.histogram(
summary,
x="recommendation_changes",
nbins=8,
title="Recommendation Changes Distribution",
color_discrete_sequence=["#E91E63"]
)
fig2.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="Number of Changes"),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
bargap=0.05,
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig2, use_container_width=True)
# Stability Ranking (Top 15 Stable Stocks)
stable_rank = summary.sort_values(by="stability_score", ascending=False).head(15)
fig3 = px.bar(
stable_rank,
x="Symbol",
y="stability_score",
title="Top 15 Most Stable Stocks (Stability Score)",
color="stability_score",
color_continuous_scale="Tealgrn"
)
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)", range=[90, 100]),
coloraxis_showscale=False,
height=320,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig3, use_container_width=True)
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_leaderboards()
# ----------------------------------------------------
def show_leaderboards(summary):
"""Renders tabbed ranking tables of the evolution dataset."""
st.markdown('<div class="section-header">๐Ÿ† Evolution Leaderboards</div>', unsafe_allow_html=True)
tab1, tab2, tab3, tab4, tab5 = st.tabs([
"๐Ÿฅ‡ Most Improved Stocks",
"โšก Strongly Improving",
"๐Ÿ›ก๏ธ Most Stable Recommendations",
"๐ŸŽฏ Highest Confidence",
"๐Ÿ“‰ Weakening Recommendations"
])
col_config = {
"Symbol": st.column_config.TextColumn("Symbol", width="small"),
"historical_recommendation": st.column_config.TextColumn("Recommendation"),
"confidence_score": st.column_config.NumberColumn("Confidence", format="%.1f"),
"evolution_score": st.column_config.NumberColumn("Evolution Score", format="%.4f"),
"trend_direction": st.column_config.TextColumn("Trend Direction"),
"stability_score": st.column_config.NumberColumn("Stability Score", format="%.2f%%"),
"Recommendation_Momentum_Score": st.column_config.ProgressColumn("Momentum Score", format="%.1f", min_value=0.0, max_value=100.0),
"Recommendation_Quality_Score": st.column_config.ProgressColumn("Quality Score", format="%.1f", min_value=0.0, max_value=100.0),
"Analyst_Conviction_Score": st.column_config.ProgressColumn("Conviction Score", format="%.1f", min_value=0.0, max_value=100.0)
}
display_cols = ["Symbol", "historical_recommendation", "confidence_score", "evolution_score", "trend_direction", "Recommendation_Momentum_Score", "Recommendation_Quality_Score", "Analyst_Conviction_Score"]
with tab1:
# Sort Evolution Score Desc
df1 = summary.sort_values(by="evolution_score", ascending=False).copy()
st.dataframe(df1[display_cols], column_config=col_config, hide_index=True, use_container_width=True)
with tab2:
# Strongly Improving Category
df2 = summary[summary["evolution_category"].str.lower().str.contains("improving") | (summary["trend_direction"].str.lower() == "improving")].sort_values(by="evolution_score", ascending=False).copy()
st.dataframe(df2[display_cols], column_config=col_config, hide_index=True, use_container_width=True)
with tab3:
# Sort Stability Score Desc
df3 = summary.sort_values(by="stability_score", ascending=False).copy()
st.dataframe(df3[display_cols + ["stability_score"]], column_config=col_config, hide_index=True, use_container_width=True)
with tab4:
# Sort Confidence Desc
df4 = summary.sort_values(by="confidence_score", ascending=False).copy()
st.dataframe(df4[display_cols], column_config=col_config, hide_index=True, use_container_width=True)
with tab5:
# Trend Direction Deteriorating
df5 = summary[summary["trend_direction"].str.lower().str.strip() == "deteriorating"].sort_values(by="evolution_score", ascending=True).copy()
if not df5.empty:
st.dataframe(df5[display_cols], column_config=col_config, hide_index=True, use_container_width=True)
else:
st.info("No recommendations currently classed as deteriorating.")
# ----------------------------------------------------
# Recommendation Momentum Analytics
# ----------------------------------------------------
def show_momentum_analytics(summary):
st.markdown('<div class="section-header">โšก Recommendation Momentum Analytics</div>', unsafe_allow_html=True)
col_chart1, col_chart2 = st.columns(2)
with col_chart1:
# Evolution Score Distribution
fig1 = px.histogram(
summary,
x="evolution_score",
nbins=12,
title="Evolution Score Distribution",
color_discrete_sequence=["#00E676"]
)
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="Evolution Score"),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
bargap=0.05,
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig1, use_container_width=True)
with col_chart2:
# Trend Direction Distribution
trend_c = summary["trend_direction"].value_counts().reset_index()
trend_c.columns = ["Trend Direction", "Count"]
fig2 = px.bar(
trend_c,
x="Trend Direction",
y="Count",
color="Trend Direction",
title="Trend Direction Distribution",
color_discrete_map={"Improving": "#00E676", "Stable": "#FFA000", "Deteriorating": "#FF3D00"}
)
fig2.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)"),
showlegend=False,
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig2, use_container_width=True)
col_chart3, col_chart4 = st.columns(2)
with col_chart3:
# Evolution Percentile Distribution
fig3 = px.histogram(
summary,
x="evolution_percentile",
nbins=10,
title="Evolution Percentile Distribution",
color_discrete_sequence=["#00B0FF"]
)
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, title="Percentile Rank"),
yaxis=dict(showgrid=True, gridcolor="rgba(255,255,255,0.08)"),
bargap=0.05,
height=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig3, use_container_width=True)
with col_chart4:
# Evolution Score by Sector
sec_evo = summary.groupby("Sector")["evolution_score"].mean().reset_index().sort_values("evolution_score")
fig4 = px.bar(
sec_evo,
x="evolution_score",
y="Sector",
orientation="h",
title="Average Evolution Score by Sector",
color="evolution_score",
color_continuous_scale="Viridis"
)
fig4.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=300,
margin=dict(l=10, r=10, t=50, b=10)
)
st.plotly_chart(fig4, use_container_width=True)
# ----------------------------------------------------
# Evolution Heatmaps
# ----------------------------------------------------
def show_heatmaps(summary):
st.markdown('<div class="section-header">๐Ÿ—บ๏ธ Evolution Heatmaps</div>', unsafe_allow_html=True)
# We will pivot Sector against different metrics to represent beautiful matrix heatmaps.
# Group by Sector and compute averages
sec_metrics = summary.groupby("Sector").agg({
"evolution_score": "mean",
"confidence_score": "mean",
"stability_score": "mean",
"Recommendation_Momentum_Score": "mean"
}).reset_index()
# Pivot metrics to create dynamic heatmap layout
sec_metrics_melted = sec_metrics.melt(id_vars="Sector", var_name="Metric", value_name="Value")
col1, col2 = st.columns(2)
with col1:
# Heatmap 1: Evolution Score by Sector
fig1 = px.density_heatmap(
sec_metrics,
x="Sector",
y="evolution_score",
z="evolution_score",
title="Evolution Score Heatmap",
color_continuous_scale="RdYlGn"
)
fig1.update_layout(
template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
height=260, margin=dict(l=10, r=10, t=40, b=10)
)
st.plotly_chart(fig1, use_container_width=True)
# Heatmap 2: Confidence by Sector
fig2 = px.density_heatmap(
sec_metrics,
x="Sector",
y="confidence_score",
z="confidence_score",
title="Confidence Score Heatmap",
color_continuous_scale="Blues"
)
fig2.update_layout(
template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
height=260, margin=dict(l=10, r=10, t=40, b=10)
)
st.plotly_chart(fig2, use_container_width=True)
with col2:
# Heatmap 3: Stability by Sector
fig3 = px.density_heatmap(
sec_metrics,
x="Sector",
y="stability_score",
z="stability_score",
title="Stability Heatmap",
color_continuous_scale="Purples"
)
fig3.update_layout(
template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
height=260, margin=dict(l=10, r=10, t=40, b=10)
)
st.plotly_chart(fig3, use_container_width=True)
# Heatmap 4: Momentum / Trend Direction by Sector
fig4 = px.density_heatmap(
sec_metrics,
x="Sector",
y="Recommendation_Momentum_Score",
z="Recommendation_Momentum_Score",
title="Recommendation Momentum Score Heatmap",
color_continuous_scale="Viridis"
)
fig4.update_layout(
template="plotly_dark", font_family="Outfit", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
height=260, margin=dict(l=10, r=10, t=40, b=10)
)
st.plotly_chart(fig4, use_container_width=True)
# ----------------------------------------------------
# TECHNICAL REQUIREMENTS: show_ai_recommendation_report()
# ----------------------------------------------------
def show_ai_recommendation_report(summary):
"""Generates and displays AI Chief Strategist advisory report."""
st.markdown('<div class="section-header">๐Ÿง  AI Recommendation Strategist</div>', unsafe_allow_html=True)
# Compute stats for dynamic report
improving_stocks = summary[summary["trend_direction"].str.lower().str.strip() == "improving"]["Symbol"].tolist()
weakening_stocks = summary[summary["trend_direction"].str.lower().str.strip() == "deteriorating"]["Symbol"].tolist()
avg_stability = summary["stability_score"].mean()
improving_str = ", ".join(improving_stocks[:4]) if improving_stocks else "No stocks"
weakening_str = ", ".join(weakening_stocks[:4]) if weakening_stocks else "No stocks"
st.markdown(f"""
<div class="outlook-card">
<h3 style="color: #00E676; margin-top: 0; font-size: 1.25rem;">๐Ÿง  Premium Recommendation Evolution Report</h3>
<p style="font-size: 1.1rem; color: #F1F5F9; line-height: 1.6; margin-bottom: 0;">
The algorithm reports <strong>stable conviction</strong> system-wide, with average signal stability registering at <strong>{avg_stability:.2f}%</strong>. High-conviction setups continue to display strong resilience across sectors.
</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: #00E676; margin-top: 0; font-size: 1.1rem;">โšก Recommendation Summary</h4>
<p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;">
Overall rating configurations show improving patterns in selection nodes. Most stock recommendations remain stable over the medium term, with confidence clusters consolidating around blue-chip leaders.
</p>
<h4 style="color: #00E676; margin-top: 15px; font-size: 1.1rem;">๐Ÿš€ Improving Opportunities</h4>
<p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;">
<strong>{improving_str}</strong> exhibit the strongest positive rating momentum, characterized by continuous upgrades and accelerating analyst conviction.
</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: #E91E63; margin-top: 0; font-size: 1.1rem;">โš ๏ธ Weakening Opportunities</h4>
<p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;">
<strong>{weakening_str}</strong> are currently under pressure with negative evolution vectors, demanding closer risk management and dynamic hedging implementation.
</p>
<h4 style="color: #FFA000; margin-top: 15px; font-size: 1.1rem;">๐Ÿ›ก๏ธ Stability & Confidence</h4>
<p style="color: #CBD5E1; font-size: 0.88rem; line-height: 1.55;">
Stability scores remain above 95% across the main cohort, confirming reliable signals. Confidence levels remain elevated among top-ranked opportunities.
</p>
</div>
""", unsafe_allow_html=True)
# ----------------------------------------------------
# Export Features
# ----------------------------------------------------
def show_export_features(summary):
st.markdown('<div class="section-header">๐Ÿ“ฅ Export Station</div>', unsafe_allow_html=True)
col_e1, col_e2, col_e3, col_e4 = st.columns(4)
with col_e1:
sum_csv = summary[["Symbol", "historical_recommendation", "confidence_score", "stability_score", "trend_direction"]].to_csv(index=False).encode("utf-8")
st.download_button(
label="Download Recommendation Summary",
data=sum_csv,
file_name="investiq_recommendation_summary.csv",
mime="text/csv",
key="dl-rec-sum",
use_container_width=True
)
with col_e2:
rank_csv = summary.sort_values("evolution_score", ascending=False)[["Symbol", "evolution_score", "Recommendation_Momentum_Score", "Recommendation_Quality_Score", "Analyst_Conviction_Score"]].to_csv(index=False).encode("utf-8")
st.download_button(
label="Download Evolution Rankings",
data=rank_csv,
file_name="investiq_evolution_rankings.csv",
mime="text/csv",
key="dl-evo-rank",
use_container_width=True
)
with col_e3:
imp_df = summary[summary["trend_direction"].str.lower().str.strip() == "improving"]
imp_csv = imp_df[["Symbol", "historical_recommendation", "confidence_score", "evolution_score", "stability_score"]].to_csv(index=False).encode("utf-8")
st.download_button(
label="Download Improving Opportunities",
data=imp_csv,
file_name="investiq_improving_opportunities.csv",
mime="text/csv",
key="dl-imp-opp",
use_container_width=True
)
with col_e4:
weak_df = summary[summary["trend_direction"].str.lower().str.strip() == "deteriorating"]
weak_csv = weak_df[["Symbol", "historical_recommendation", "confidence_score", "evolution_score", "stability_score"]].to_csv(index=False).encode("utf-8")
st.download_button(
label="Download Weakening Opportunities",
data=weak_csv,
file_name="investiq_weakening_opportunities.csv",
mime="text/csv",
key="dl-weak-opp",
use_container_width=True
)
# ----------------------------------------------------
# MAIN CALLABLE: show_recommendation_evolution_workstation()
# ----------------------------------------------------
def show_recommendation_evolution_workstation():
"""Main Streamlit execution point for Section 7 Page."""
apply_custom_style()
st.title("๐Ÿ”„ Recommendation Evolution Workstation")
st.markdown("<p style='color: #CBD5E1; font-size: 1.1rem; margin-top: -10px;'>Track how investment recommendations have changed over time and identify improving opportunities before the market.</p>", unsafe_allow_html=True)
# Load data
try:
summary, evolution, master = load_data()
except Exception as e:
st.error(f"Error loading workstation data: {e}")
st.stop()
# 1. Evolution Overview
show_evolution_overview(summary, master)
st.markdown("---")
# 2. Stock Recommendation Timeline Selector
st.markdown('<div class="section-header">๐Ÿ•ฐ๏ธ Stock Recommendation Timeline</div>', unsafe_allow_html=True)
selected_symbol = st.selectbox("Select Asset to Audit Timeline & Performance", summary["Symbol"].unique())
show_stock_timeline(selected_symbol, summary, evolution)
st.markdown("---")
# 3. Confidence Intelligence
show_confidence_analysis(selected_symbol, summary, evolution)
st.markdown("---")
# 4. Upgrade / Downgrade Analysis
show_upgrade_analysis(selected_symbol, summary, evolution)
st.markdown("---")
# 5. Recommendation Stability Analysis
show_stability_analysis(selected_symbol, summary, evolution)
st.markdown("---")
# 6. Leaderboards
show_leaderboards(summary)
st.markdown("---")
# 7. Recommendation Momentum Analytics
show_momentum_analytics(summary)
st.markdown("---")
# 8. Evolution Heatmaps
show_heatmaps(summary)
st.markdown("---")
# 9. AI Recommendation Strategist
show_ai_recommendation_report(summary)
st.markdown("---")
# 10. Export Features
show_export_features(summary)