Risk Factor Attribution Analysis
Fama-French factors fetched successfully:

Number of factor data points fetched: 2391
Date	Mkt-RF	SMB	HML	RF
2014-11-05 00:00:00	0.0048	-0.0056	0.0086	0
2014-11-06 00:00:00	0.0049	0.0009	-0.0046	0
2014-11-07 00:00:00	0.0009	0	0.0047	0
2014-11-10 00:00:00	0.0033	0.0021	-0.0053	0
2014-11-11 00:00:00	0.001	0.0002	-0.0026	0
Factor data range: 2014-11-05 00:00:00 to 2024-09-30 00:00:00

Could not infer returns frequency. Please set it manually.

Error calculating risk factor attribution: 'Portfolio_Returns'

Risk factor attribution analysis is unavailable.

def calculate_risk_factor_attribution(returns, factors, annualization_factor=252):
    """
    Decompose portfolio returns by risk factors using linear regression and provide
    comments on the factor contributions.

    Parameters:
    - returns (pd.Series): Portfolio returns.
    - factors (pd.DataFrame): DataFrame where each column represents a risk factor.
    - annualization_factor (int): Annualization factor (252 for daily, 12 for monthly returns).

    Returns:
    - pd.DataFrame: Factor names, their contribution percentages, and explanatory comments.
    - str: Error message if any exception occurs, else None.
    """
    try:
        # Ensure returns has a name, or assign one if missing
        if returns.name is None:
            returns = returns.rename("Portfolio_Returns")
        
        # Convert returns to a DataFrame
        returns = returns.to_frame()
        
        # Ensure both returns and factors are in decimal format
        if returns['Portfolio_Returns'].max() > 1:
            returns = returns / 100
            
        if factors.max().max() > 1:
            factors = factors / 100
        
        # Adjust returns to excess returns by subtracting RF (if it exists in factors)
        if 'RF' in factors.columns:
            returns['Portfolio_Returns'] = returns['Portfolio_Returns'] - factors['RF']
            factors = factors.drop(columns=['RF'])
        
        # Align the data using an inner join
        aligned_data = pd.merge(returns, factors, left_index=True, right_index=True, how='inner')
        
        # Debugging: Show the number of aligned data points
        st.write(f"Number of aligned data points: {len(aligned_data)}")
        
        # Check that we have data after merging
        if aligned_data.empty:
            raise ValueError("No overlapping dates between returns and factors.")
    
        # Prepare X and y for regression
        X = aligned_data[factors.columns]
        X = sm.add_constant(X)  # Add intercept
        y = aligned_data['Portfolio_Returns']
        
        # Run the regression model
        model = sm.OLS(y, X).fit()
        coefficients = model.params.drop('const', errors='ignore')
        
        # Calculate factor contributions (non-annualized)
        factor_means = X.mean().drop('const', errors='ignore')
        contributions = coefficients * factor_means
        
        # Calculate total contribution
        total_return = returns['Portfolio_Returns'].mean()
        
        # Calculate percentage contributions
        attribution_df = pd.DataFrame({
            'Factor': contributions.index,
            'Contribution (%)': (contributions / total_return * 100).round(2)
        })
        
        # Remove the constant term if it exists
        if 'const' in attribution_df['Factor'].values:
            attribution_df = attribution_df[attribution_df['Factor'] != 'const']
        
        # Use full names for factors
        factor_full_names = {
            'Mkt-RF': 'Market Risk Premium',
            'SMB': 'Small Minus Big',
            'HML': 'High Minus Low'
        }
        attribution_df['Factor'] = attribution_df['Factor'].replace(factor_full_names)
        
        # Generate detailed comments based on factor contributions
        comments = []
        for _, row in attribution_df.iterrows():
            factor_name = row['Factor']
            contribution = row['Contribution (%)']

            # Market Risk Premium (Mkt-RF) Analysis
            if factor_name == 'Market Risk Premium':
                if contribution > 75:
                    comments.append(f"{factor_name} = {contribution}%: Extremely high sensitivity to market movements.")
                elif contribution > 50:
                    comments.append(f"{factor_name} = {contribution}%: High sensitivity to overall market movements.")
                elif contribution > 30:
                    comments.append(f"{factor_name} = {contribution}%: Notable sensitivity to market risk.")
                elif contribution > 10:
                    comments.append(f"{factor_name} = {contribution}%: Moderate exposure to market risk.")
                else:
                    comments.append(f"{factor_name} = {contribution}%: Low exposure to market risk.")

            # Small Minus Big (SMB) Analysis
            elif factor_name == 'Small Minus Big':
                if contribution > 5:
                    comments.append(f"{factor_name} = {contribution}%: Strong tilt towards small-cap stocks.")
                elif contribution > 0:
                    comments.append(f"{factor_name} = {contribution}%: Mild preference for small-cap stocks.")
                elif contribution > -5:
                    comments.append(f"{factor_name} = {contribution}%: Slight tilt towards larger-cap stocks.")
                else:
                    comments.append(f"{factor_name} = {contribution}%: Clear preference for large-cap stocks.")

            # High Minus Low (HML) Analysis
            elif factor_name == 'High Minus Low':
                if contribution > 5:
                    comments.append(f"{factor_name} = {contribution}%: Strong tilt toward value stocks.")
                elif contribution > 0:
                    comments.append(f"{factor_name} = {contribution}%: Slight positive tilt toward value stocks.")
                elif contribution > -5:
                    comments.append(f"{factor_name} = {contribution}%: Minor negative tilt toward growth stocks.")
                else:
                    comments.append(f"{factor_name} = {contribution}%: Clear preference for growth stocks.")

        # Add comments to the DataFrame
        attribution_df['Comment'] = comments
    
        return attribution_df, None
    
    except Exception as e:
        error_message = f"Error calculating risk factor attribution: {e}"
        return pd.DataFrame(), error_message

# Fetch actual factor data from Fama-French or another data source
def fetch_fama_french_factors(start_date, end_date):
    try:
        # Fetch daily Fama-French factors
        ff_data = pdr.get_data_famafrench('F-F_Research_Data_Factors_daily', start=start_date, end=end_date)
        if not ff_data:
            st.error("Fama-French data not found.")
            return pd.DataFrame()
        
        factors = ff_data[0]
        factors = factors.rename(columns=lambda x: x.strip())  # Remove any leading/trailing spaces
        factors /= 100  # Convert percentage returns to decimal format
        
        # Check for necessary columns
        required_columns = {'Mkt-RF', 'SMB', 'HML', 'RF'}
        if not required_columns.issubset(factors.columns):
            missing = required_columns - set(factors.columns)
            st.error(f"Missing factor columns: {', '.join(missing)}")
            return pd.DataFrame()
        
        st.write("Fama-French factors fetched successfully:")
        st.dataframe(factors.head())
        
        return factors
    except Exception as e:
        st.error(f"Error fetching factor data: {e}")
        return pd.DataFrame()

with tabs[2]:
                                st.header("📋 Detail Comparisons")
                                
                                with st.container():
                                    st.subheader("🧮 Advanced Metrics")
                                    performance_data_advanced = {
                                        'Metric': list(portfolio_metrics.keys()),
                                        'Portfolio': list(portfolio_metrics.values()),
                                        'Benchmark': list(benchmark_metrics.values())
                                    }
                                    performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')
                                    st.table(performance_df_advanced)
                                
                                st.subheader("🔍 Risk Factor Attribution Analysis")
                                
                                # Define date range based on portfolio returns index
                                start_date = returns.index.min().strftime('%Y-%m-%d')
                                end_date = returns.index.max().strftime('%Y-%m-%d')
                                
                                # Fetch actual factor data
                                factors = fetch_fama_french_factors(start_date, end_date)
                                
                                # Ensure factors are fetched
                                if factors.empty:
                                    st.write("Failed to retrieve factor data.")
                                else:
                                    st.write(f"Number of factor data points fetched: {len(factors)}")
                                    st.write(f"Factor data range: {factors.index.min()} to {factors.index.max()}")
                                    
                                    # Check and set frequency for returns
                                    if returns.index.freq is None:
                                        # Attempt to infer frequency
                                        inferred_freq = pd.infer_freq(returns.index)
                                        if inferred_freq:
                                            returns = returns.asfreq(inferred_freq)
                                            st.write(f"Inferred and set returns frequency to: {inferred_freq}")
                                        else:
                                            st.warning("Could not infer returns frequency. Please set it manually.")
                                    
                                    # Align the frequency of factors to returns
                                    if returns.index.freq != factors.index.freq:
                                        st.write(f"Aligning factor frequency from {factors.index.freq} to {returns.index.freq}")
                                        try:
                                            factors = factors.asfreq(returns.index.freq, method='ffill')
                                            st.write("Frequency alignment successful.")
                                        except Exception as e:
                                            st.error(f"Error aligning factor frequency: {e}")
                                    
                                    # Calculate Attribution with aligned dates
                                    attribution,error = calculate_risk_factor_attribution(returns, factors)
                                    
                                    if error:
                                        st.error(error)
                                        st.write("Risk factor attribution analysis is unavailable.")
                                    elif attribution.empty:
                                        st.write("Risk factor attribution analysis is unavailable.")
                                    else:
                                        st.write("Risk Factor Attribution:")
                                        st.table(attribution)
                                        # Optional: Visualize Attribution
                                        fig_attribution = px.bar(
                                            attribution,
                                            x='Factor',
                                            y='Contribution (%)',
                                            title='Risk Factor Attribution',
                                            labels={'Contribution (%)': 'Contribution (%)'},
                                            template='plotly_dark'
                                        )
                                        st.plotly_chart(fig_attribution, use_container_width=True)

fix my code