| 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 #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) |
|
|
| |
| 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" |
|
|
| |
| |
| |
| @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) |
| |
| |
| 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") |
| |
| |
| sector_map = master.set_index("Symbol")["Sector"].to_dict() |
| summary["Sector"] = summary["Symbol"].map(sector_map).fillna("Unknown") |
| |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| summary["Recommendation_Momentum_Score"] = ( |
| (summary["scaled_evolution"] * 0.5) + |
| (summary["scaled_confidence"] * 0.3) + |
| (summary["trend_value"] * 0.2) |
| ).clip(0.0, 100.0) |
| |
| |
| |
| 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) |
| |
| |
| summary["Analyst_Conviction_Score"] = ( |
| (summary["scaled_confidence"] * 0.6) + |
| (summary["evolution_percentile"] * 0.4) |
| ).clip(0.0, 100.0) |
| |
| return summary, evolution, master |
|
|
| |
| |
| |
| 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() |
| |
| |
| 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") |
|
|
| |
| |
| |
| def show_stock_timeline(selected_symbol, summary, evolution): |
| """Renders the historical timeline chart for a specific 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) |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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) |
| |
| |
| 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: |
| |
| 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: |
| |
| 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) |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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"]) |
| |
| |
| |
| 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: |
| |
| 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: |
| |
| 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) |
|
|
| |
| |
| |
| 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") |
| |
| |
| 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: |
| |
| 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: |
| |
| 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) |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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: |
| |
| 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: |
| |
| 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: |
| |
| 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: |
| |
| 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: |
| |
| 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.") |
|
|
| |
| |
| |
| 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: |
| |
| 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_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: |
| |
| 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: |
| |
| 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) |
|
|
| |
| |
| |
| def show_heatmaps(summary): |
| st.markdown('<div class="section-header">๐บ๏ธ Evolution Heatmaps</div>', unsafe_allow_html=True) |
| |
| |
| |
| sec_metrics = summary.groupby("Sector").agg({ |
| "evolution_score": "mean", |
| "confidence_score": "mean", |
| "stability_score": "mean", |
| "Recommendation_Momentum_Score": "mean" |
| }).reset_index() |
| |
| |
| sec_metrics_melted = sec_metrics.melt(id_vars="Sector", var_name="Metric", value_name="Value") |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| |
| 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) |
| |
| |
| 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: |
| |
| 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) |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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) |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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 |
| ) |
|
|
| |
| |
| |
| 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) |
| |
| |
| try: |
| summary, evolution, master = load_data() |
| except Exception as e: |
| st.error(f"Error loading workstation data: {e}") |
| st.stop() |
| |
| |
| show_evolution_overview(summary, master) |
| |
| st.markdown("---") |
| |
| |
| 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("---") |
| |
| |
| show_confidence_analysis(selected_symbol, summary, evolution) |
| st.markdown("---") |
| |
| |
| show_upgrade_analysis(selected_symbol, summary, evolution) |
| st.markdown("---") |
| |
| |
| show_stability_analysis(selected_symbol, summary, evolution) |
| st.markdown("---") |
| |
| |
| show_leaderboards(summary) |
| st.markdown("---") |
| |
| |
| show_momentum_analytics(summary) |
| st.markdown("---") |
| |
| |
| show_heatmaps(summary) |
| st.markdown("---") |
| |
| |
| show_ai_recommendation_report(summary) |
| st.markdown("---") |
| |
| |
| show_export_features(summary) |
|
|