refactor: remove duplicate ETF display from sidebar for cleaner UI

This commit is contained in:
Pascal BIBEHE 2025-05-27 20:31:05 +02:00
parent e0dc6e57eb
commit 3929f2d4f0

View File

@ -19,6 +19,7 @@ import sys
import logging
import traceback
from dotenv import load_dotenv
import re
# Load environment variables
load_dotenv(override=True) # Force reload of environment variables
@ -340,11 +341,123 @@ def calculate_correlation_matrix(price_data_dict: Dict[str, pd.DataFrame]) -> pd
logger.error(traceback.format_exc())
return pd.DataFrame()
def optimize_portfolio_allocation(
etf_metrics: List[Dict[str, Any]],
risk_tolerance: str,
correlation_matrix: pd.DataFrame
) -> List[Dict[str, Any]]:
def calculate_etf_risk_score(etf: Dict[str, Any]) -> float:
"""
Calculate a comprehensive risk score for an ETF based on multiple metrics.
Args:
etf: Dictionary containing ETF metrics
Returns:
float: Risk score (0-100, higher means higher risk)
"""
try:
score = 0
metrics_used = 0
# Primary Metrics (60% of total score)
# 1. Volatility (20%)
if 'volatility' in etf:
volatility = etf['volatility']
if volatility < 10:
score += 20
elif volatility < 15:
score += 15
elif volatility < 20:
score += 10
else:
score += 5
metrics_used += 1
# 2. Yield (20%)
if 'yield' in etf:
yield_value = etf['yield']
if yield_value < 3:
score += 5
elif yield_value < 6:
score += 10
elif yield_value < 10:
score += 15
else:
score += 20
metrics_used += 1
# 3. Sharpe/Sortino Ratio (20%)
if 'sharpe_ratio' in etf:
sharpe = etf['sharpe_ratio']
if sharpe > 1.5:
score += 5
elif sharpe > 1.0:
score += 10
elif sharpe > 0.8:
score += 15
else:
score += 20
metrics_used += 1
# Secondary Metrics (40% of total score)
# 1. Dividend Growth (10%)
if 'dividend_growth' in etf:
growth = etf['dividend_growth']
if growth > 10:
score += 5
elif growth > 5:
score += 7
elif growth > 0:
score += 10
else:
score += 15
metrics_used += 1
# 2. Payout Ratio (10%)
if 'payout_ratio' in etf:
ratio = etf['payout_ratio']
if ratio < 40:
score += 5
elif ratio < 60:
score += 7
elif ratio < 80:
score += 10
else:
score += 15
metrics_used += 1
# 3. Expense Ratio (10%)
if 'expense_ratio' in etf:
ratio = etf['expense_ratio']
if ratio < 0.2:
score += 5
elif ratio < 0.4:
score += 7
elif ratio < 0.6:
score += 10
else:
score += 15
metrics_used += 1
# 4. AUM/Volume (10%)
if 'aum' in etf:
aum = etf['aum']
if aum > 5e9: # > $5B
score += 5
elif aum > 1e9: # > $1B
score += 7
elif aum > 500e6: # > $500M
score += 10
else:
score += 15
metrics_used += 1
# Normalize score based on available metrics
if metrics_used > 0:
return score / metrics_used
return 50 # Default middle score if no metrics available
except Exception as e:
logger.error(f"Error calculating ETF risk score: {str(e)}")
return 50
def optimize_portfolio_allocation(etf_metrics: List[Dict[str, Any]], risk_tolerance: str, correlation_matrix: pd.DataFrame) -> List[Dict[str, Any]]:
"""
Optimize portfolio allocation based on risk tolerance and ETF metrics.
@ -360,93 +473,61 @@ def optimize_portfolio_allocation(
logger.info(f"Optimizing portfolio allocation for {risk_tolerance} risk tolerance")
logger.info(f"ETF metrics: {etf_metrics}")
# Group ETFs by risk category
low_risk = [etf for etf in etf_metrics if etf.get("Risk Level", "Unknown") == "Low"]
medium_risk = [etf for etf in etf_metrics if etf.get("Risk Level", "Unknown") == "Medium"]
high_risk = [etf for etf in etf_metrics if etf.get("Risk Level", "Unknown") == "High"]
# Calculate risk scores for each ETF
for etf in etf_metrics:
etf['risk_score'] = calculate_etf_risk_score(etf)
logger.info(f"Risk score for {etf['Ticker']}: {etf['risk_score']:.2f}")
logger.info(f"Risk groups - Low: {len(low_risk)}, Medium: {len(medium_risk)}, High: {len(high_risk)}")
# Sort ETFs by risk score based on risk tolerance
if risk_tolerance == "Conservative":
# For conservative, prefer lower risk
sorted_etfs = sorted(etf_metrics, key=lambda x: x['risk_score'])
elif risk_tolerance == "Aggressive":
# For aggressive, prefer higher risk
sorted_etfs = sorted(etf_metrics, key=lambda x: x['risk_score'], reverse=True)
else: # Moderate
# For moderate, sort by Sharpe ratio first, then risk score
sorted_etfs = sorted(etf_metrics,
key=lambda x: (x.get('sharpe_ratio', 0), -x['risk_score']),
reverse=True)
# Sort ETFs by score within each risk category
low_risk.sort(key=lambda x: x.get("score", 0), reverse=True)
medium_risk.sort(key=lambda x: x.get("score", 0), reverse=True)
high_risk.sort(key=lambda x: x.get("score", 0), reverse=True)
logger.info(f"Sorted ETFs: {[etf['Ticker'] for etf in sorted_etfs]}")
# Initialize allocations
allocations = []
# Calculate base allocations based on risk tolerance
num_etfs = len(sorted_etfs)
if num_etfs == 0:
return []
if risk_tolerance == "Conservative":
# Conservative allocation
if low_risk:
# Allocate 50% to low-risk ETFs
low_risk_alloc = 50.0 / len(low_risk)
for etf in low_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": low_risk_alloc})
if medium_risk:
# Allocate 30% to medium-risk ETFs
medium_risk_alloc = 30.0 / len(medium_risk)
for etf in medium_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": medium_risk_alloc})
if high_risk:
# Allocate 20% to high-risk ETFs
high_risk_alloc = 20.0 / len(high_risk)
for etf in high_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": high_risk_alloc})
# 50% to low risk, 30% to medium risk, 20% to high risk
base_allocations = [0.5] + [0.3] + [0.2] + [0.0] * (num_etfs - 3)
elif risk_tolerance == "Moderate":
# Moderate allocation
if low_risk:
# Allocate 30% to low-risk ETFs
low_risk_alloc = 30.0 / len(low_risk)
for etf in low_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": low_risk_alloc})
if medium_risk:
# Allocate 40% to medium-risk ETFs
medium_risk_alloc = 40.0 / len(medium_risk)
for etf in medium_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": medium_risk_alloc})
if high_risk:
# Allocate 30% to high-risk ETFs
high_risk_alloc = 30.0 / len(high_risk)
for etf in high_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": high_risk_alloc})
# 40% to medium risk, 30% to low risk, 30% to high risk
base_allocations = [0.4] + [0.3] + [0.3] + [0.0] * (num_etfs - 3)
else: # Aggressive
# Aggressive allocation
if low_risk:
# Allocate 20% to low-risk ETFs
low_risk_alloc = 20.0 / len(low_risk)
for etf in low_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": low_risk_alloc})
# 40% to high risk, 40% to medium risk, 20% to low risk
base_allocations = [0.4] + [0.4] + [0.2] + [0.0] * (num_etfs - 3)
if medium_risk:
# Allocate 40% to medium-risk ETFs
medium_risk_alloc = 40.0 / len(medium_risk)
for etf in medium_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": medium_risk_alloc})
# Adjust allocations based on number of ETFs
if num_etfs < len(base_allocations):
base_allocations = base_allocations[:num_etfs]
# Normalize to ensure sum is 1
total = sum(base_allocations)
base_allocations = [alloc/total for alloc in base_allocations]
if high_risk:
# Allocate 40% to high-risk ETFs
high_risk_alloc = 40.0 / len(high_risk)
for etf in high_risk:
allocations.append({"ticker": etf["Ticker"], "allocation": high_risk_alloc})
# Create final allocation list
final_allocations = []
for etf, allocation in zip(sorted_etfs, base_allocations):
final_allocations.append({
"ticker": etf["Ticker"],
"allocation": allocation * 100 # Convert to percentage
})
# If no allocations were made, use equal weighting
if not allocations:
logger.warning("No risk-based allocations made, using equal weighting")
total_etfs = len(etf_metrics)
equal_alloc = 100.0 / total_etfs
allocations = [{"ticker": etf["Ticker"], "allocation": equal_alloc} for etf in etf_metrics]
logger.info(f"Final allocations: {allocations}")
return allocations
logger.info(f"Final allocations: {final_allocations}")
return final_allocations
except Exception as e:
logger.error(f"Error optimizing portfolio allocation: {str(e)}")
logger.error(f"Error in optimize_portfolio_allocation: {str(e)}")
logger.error(traceback.format_exc())
return []
@ -1344,37 +1425,263 @@ st.set_page_config(
# Initialize session state variables
if 'simulation_run' not in st.session_state:
st.session_state.simulation_run = False
logger.info("Initialized simulation_run in session state")
if 'df_data' not in st.session_state:
st.session_state.df_data = None
logger.info("Initialized df_data in session state")
if 'final_alloc' not in st.session_state:
st.session_state.final_alloc = None
logger.info("Initialized final_alloc in session state")
if 'mode' not in st.session_state:
st.session_state.mode = 'Capital Target'
logger.info("Initialized mode in session state")
if 'target' not in st.session_state:
st.session_state.target = 0
logger.info("Initialized target in session state")
if 'initial_capital' not in st.session_state:
st.session_state.initial_capital = 0
logger.info("Initialized initial_capital in session state")
if 'enable_drip' not in st.session_state:
st.session_state.enable_drip = False
logger.info("Initialized enable_drip in session state")
if 'enable_erosion' not in st.session_state:
st.session_state.enable_erosion = False
logger.info("Initialized enable_erosion in session state")
if 'api_calls' not in st.session_state:
st.session_state.api_calls = 0
logger.info("Initialized api_calls in session state")
if 'force_refresh_data' not in st.session_state:
st.session_state.force_refresh_data = False
logger.info("Initialized force_refresh_data in session state")
if 'etf_allocations' not in st.session_state:
st.session_state.etf_allocations = []
logger.info("Initialized empty etf_allocations in session state")
if 'risk_tolerance' not in st.session_state:
st.session_state.risk_tolerance = "Moderate"
logger.info("Initialized risk_tolerance in session state")
# Main title
st.title("📈 ETF Portfolio Builder")
# Sidebar for simulation parameters
with st.sidebar:
st.header("Simulation Parameters")
# Function to remove ticker
def remove_ticker(ticker_to_remove: str) -> None:
"""Remove a ticker from the portfolio."""
try:
logger.info(f"Removing ticker: {ticker_to_remove}")
current_allocations = list(st.session_state.etf_allocations)
st.session_state.etf_allocations = [etf for etf in current_allocations if etf["ticker"] != ticker_to_remove]
logger.info(f"Updated allocations after removal: {st.session_state.etf_allocations}")
st.rerun()
except Exception as e:
logger.error(f"Error removing ticker: {str(e)}")
st.error(f"Error removing ticker: {str(e)}")
# Add refresh data button at the top
if st.button("🔄 Refresh Data", use_container_width=True):
st.info("Refreshing ETF data...")
# Add your data refresh logic here
st.success("Data refreshed successfully!")
# Display current tickers in the main space
if st.session_state.etf_allocations:
st.subheader("Selected ETFs")
st.markdown("""
<style>
.ticker-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.ticker-item {
display: inline-flex;
align-items: center;
color: #2ecc71;
font-size: 1.1em;
font-weight: 500;
}
.ticker-close {
margin-left: 2px;
cursor: pointer;
font-size: 1.1em;
opacity: 0.7;
}
.ticker-close:hover {
opacity: 1;
}
</style>
""", unsafe_allow_html=True)
# Create a container for tickers
ticker_container = st.container()
with ticker_container:
# Display each ticker with a close button
for etf in st.session_state.etf_allocations:
col1, col2 = st.columns([0.05, 0.95]) # Adjusted column ratio
with col1:
if st.button("×", key=f"remove_{etf['ticker']}",
help=f"Remove {etf['ticker']} from portfolio"):
remove_ticker(etf['ticker'])
with col2:
st.markdown(f"<div class='ticker-item'>{etf['ticker']}</div>", unsafe_allow_html=True)
# Debug information
logger.info("=== Session State Debug ===")
logger.info(f"Full session state: {dict(st.session_state)}")
logger.info(f"ETF allocations type: {type(st.session_state.etf_allocations)}")
logger.info(f"ETF allocations content: {st.session_state.etf_allocations}")
logger.info("=== End Session State Debug ===")
def add_etf_to_portfolio(ticker: str) -> bool:
"""Add an ETF to the portfolio with proper validation and error handling."""
try:
logger.info("=== Adding ETF to Portfolio ===")
logger.info(f"Input ticker: {ticker}")
logger.info(f"Current allocations before adding: {st.session_state.etf_allocations}")
logger.info(f"Current allocations type: {type(st.session_state.etf_allocations)}")
# Validate ticker format
if not re.match(r'^[A-Z]{1,7}$', ticker.upper()):
logger.warning(f"Invalid ticker format: {ticker}")
st.error("Invalid ticker format. Must be 1-7 uppercase letters.")
return False
# Check if ticker already exists
if any(etf["ticker"] == ticker.upper() for etf in st.session_state.etf_allocations):
logger.warning(f"Ticker {ticker.upper()} already exists in portfolio")
st.warning(f"{ticker.upper()} is already in your portfolio.")
return False
# Verify ticker exists by fetching data
logger.info(f"Fetching data for ticker: {ticker.upper()}")
etf_data = fetch_etf_data([ticker.upper()])
logger.info(f"Fetched ETF data: {etf_data}")
if etf_data is None or etf_data.empty:
logger.warning(f"Unknown ticker: {ticker.upper()}")
st.error(f"Unknown ticker: {ticker.upper()}. Please enter a valid ETF ticker.")
return False
# Create new ETF entry
new_etf = {
"ticker": ticker.upper(),
"allocation": 0.0
}
logger.info(f"Created new ETF entry: {new_etf}")
# Update session state
current_allocations = list(st.session_state.etf_allocations)
current_allocations.append(new_etf)
st.session_state.etf_allocations = current_allocations
logger.info(f"Updated session state allocations: {st.session_state.etf_allocations}")
logger.info(f"Updated allocations type: {type(st.session_state.etf_allocations)}")
# Recalculate allocations based on risk tolerance
if len(st.session_state.etf_allocations) > 0:
risk_tolerance = st.session_state.risk_tolerance
tickers = [etf["ticker"] for etf in st.session_state.etf_allocations]
logger.info(f"Recalculating allocations for tickers: {tickers}")
df_data = fetch_etf_data(tickers)
logger.info(f"Fetched data for recalculation: {df_data}")
if df_data is not None and not df_data.empty:
etf_metrics = df_data.to_dict('records')
new_allocations = optimize_portfolio_allocation(
etf_metrics,
risk_tolerance,
pd.DataFrame()
)
logger.info(f"Calculated new allocations: {new_allocations}")
st.session_state.etf_allocations = new_allocations
logger.info(f"Updated session state with new allocations: {st.session_state.etf_allocations}")
logger.info("=== End Adding ETF to Portfolio ===")
return True
except Exception as e:
logger.error("=== Error Adding ETF to Portfolio ===")
logger.error(f"Error: {str(e)}")
logger.error(traceback.format_exc())
st.error(f"Error adding ETF: {str(e)}")
return False
def remove_etf_from_portfolio(index: int) -> bool:
"""Remove an ETF from the portfolio with proper validation and error handling."""
try:
logger.info(f"Attempting to remove ETF at index: {index}")
logger.info(f"Current allocations before removal: {st.session_state.etf_allocations}")
if not st.session_state.etf_allocations or index >= len(st.session_state.etf_allocations):
logger.warning(f"Invalid ETF index for removal: {index}")
return False
# Create new list without the removed ETF
current_allocations = list(st.session_state.etf_allocations)
removed_etf = current_allocations.pop(index)
st.session_state.etf_allocations = current_allocations
logger.info(f"Successfully removed ETF: {removed_etf}")
logger.info(f"Updated allocations: {st.session_state.etf_allocations}")
# Recalculate allocations if there are remaining ETFs
if st.session_state.etf_allocations:
risk_tolerance = st.session_state.risk_tolerance
tickers = [etf["ticker"] for etf in st.session_state.etf_allocations]
df_data = fetch_etf_data(tickers)
if df_data is not None and not df_data.empty:
etf_metrics = df_data.to_dict('records')
new_allocations = optimize_portfolio_allocation(
etf_metrics,
risk_tolerance,
pd.DataFrame()
)
st.session_state.etf_allocations = new_allocations
logger.info(f"Recalculated allocations: {new_allocations}")
return True
except Exception as e:
logger.error(f"Error removing ETF: {str(e)}")
logger.error(traceback.format_exc())
st.error(f"Error removing ETF: {str(e)}")
return False
# Sidebar for ETF input
with st.sidebar:
st.header("ETF Allocation")
# Create a container for ETF input
with st.container():
# Input field for ETF ticker only
new_ticker = st.text_input("ETF Ticker", help="Enter a valid ETF ticker (e.g., SCHD)")
# Add button to add ETF
add_etf_button = st.button("ADD ETF", use_container_width=True)
if add_etf_button:
logger.info("=== Add ETF Button Clicked ===")
logger.info(f"Input ticker: {new_ticker}")
logger.info(f"Current allocations: {st.session_state.etf_allocations}")
if not new_ticker:
st.error("Please enter an ETF ticker.")
logger.warning("No ticker provided")
elif len(st.session_state.etf_allocations) >= 10:
st.error("Maximum of 10 ETFs allowed in portfolio.")
logger.warning("Maximum ETF limit reached")
else:
if add_etf_to_portfolio(new_ticker):
st.success(f"Added {new_ticker.upper()} to portfolio.")
logger.info("Successfully added ETF, triggering rerun")
st.rerun()
# Display total allocation
if st.session_state.etf_allocations:
current_total = sum(etf["allocation"] for etf in st.session_state.etf_allocations)
st.metric("Total Allocation (%)", f"{current_total:.2f}")
# Add a warning if total is not 100%
if abs(current_total - 100) > 0.1:
st.warning("Total allocation should be 100%")
else:
st.info("No ETFs added yet. Please add ETFs to your portfolio.")
logger.info("No ETFs in portfolio")
# Mode selection
simulation_mode = st.radio(
@ -1406,6 +1713,7 @@ with st.sidebar:
options=["Conservative", "Moderate", "Aggressive"],
value="Moderate"
)
st.session_state.risk_tolerance = risk_tolerance
# Additional options
st.subheader("Additional Options")
@ -1424,31 +1732,12 @@ with st.sidebar:
index=1
)
# ETF Selection
st.subheader("ETF Selection")
# Create a form for ETF selection
with st.form("etf_selection_form"):
# Number of ETFs
num_etfs = st.number_input("Number of ETFs", min_value=1, max_value=10, value=3, step=1)
# Create columns for ETF inputs
etf_inputs = []
for i in range(num_etfs):
ticker = st.text_input(f"ETF {i+1} Ticker", key=f"ticker_{i}")
if ticker: # Only add non-empty tickers
etf_inputs.append({"ticker": ticker.upper().strip()})
# Submit button
submitted = st.form_submit_button("Run Portfolio Simulation", type="primary")
if submitted:
try:
if not etf_inputs:
st.error("Please enter at least one ETF ticker")
# Run simulation button
if st.button("Run Portfolio Simulation", type="primary", use_container_width=True):
if not st.session_state.etf_allocations:
st.error("Please add at least one ETF to your portfolio.")
else:
logger.info(f"Form submitted with {len(etf_inputs)} ETFs: {etf_inputs}")
try:
# Store parameters in session state
st.session_state.mode = simulation_mode
st.session_state.enable_drip = enable_drip == "Yes"
@ -1462,32 +1751,23 @@ with st.sidebar:
# Run simulation
logger.info("Starting portfolio simulation...")
logger.info(f"ETF inputs: {etf_inputs}")
logger.info(f"ETF allocations: {st.session_state.etf_allocations}")
df_data = fetch_etf_data([etf["ticker"] for etf in etf_inputs])
tickers = [etf["ticker"] for etf in st.session_state.etf_allocations]
df_data = fetch_etf_data(tickers)
logger.info(f"Fetched ETF data:\n{df_data}")
if df_data is not None and not df_data.empty:
logger.info("Calculating optimal allocations...")
# Calculate allocations based on risk tolerance
etf_allocations = optimize_portfolio_allocation(
df_data.to_dict('records'),
risk_tolerance,
pd.DataFrame() # Empty correlation matrix for now
)
logger.info(f"Optimal allocations: {etf_allocations}")
if simulation_mode == "Income Target":
logger.info(f"Allocating for income target: ${monthly_target}")
final_alloc = allocate_for_income(df_data, monthly_target, etf_allocations)
final_alloc = allocate_for_income(df_data, monthly_target, st.session_state.etf_allocations)
else:
logger.info(f"Allocating for capital target: ${initial_capital}")
final_alloc = allocate_for_capital(df_data, initial_capital, etf_allocations)
final_alloc = allocate_for_capital(df_data, initial_capital, st.session_state.etf_allocations)
logger.info(f"Final allocation result:\n{final_alloc}")
if final_alloc is not None and not final_alloc.empty:
# Store results in session state
st.session_state.simulation_run = True
st.session_state.df_data = df_data
st.session_state.final_alloc = final_alloc
@ -1495,17 +1775,12 @@ with st.sidebar:
st.rerun()
else:
st.error("Failed to generate portfolio allocation. Please check your inputs and try again.")
logger.error("Allocation returned empty DataFrame")
logger.error(f"df_data columns: {df_data.columns}")
logger.error(f"df_data shape: {df_data.shape}")
logger.error(f"df_data:\n{df_data}")
else:
st.error("Failed to fetch ETF data. Please check your tickers and try again.")
logger.error("ETF data fetch returned empty DataFrame")
except Exception as e:
st.error(f"Error running simulation: {str(e)}")
logger.error(f"Error in form submission: {str(e)}")
logger.error(f"Error in simulation: {str(e)}")
logger.error(traceback.format_exc())
# Add reset simulation button at the bottom of sidebar
@ -1671,7 +1946,8 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
min_value=0.0,
max_value=100.0,
step=0.1,
format="%.1f"
format="%.1f",
required=True
),
"Yield (%)": st.column_config.TextColumn("Yield (%)", disabled=True),
"Price Per Share": st.column_config.TextColumn("Price Per Share", disabled=True),
@ -1684,6 +1960,16 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
# Calculate total allocation
total_alloc = edited_df["Allocation (%)"].sum()
# Validate individual allocations
invalid_allocations = edited_df[
(edited_df["Allocation (%)"] <= 0) |
(edited_df["Allocation (%)"] > 100)
]
if not invalid_allocations.empty:
for _, row in invalid_allocations.iterrows():
st.error(f"Invalid allocation for {row['Ticker']}: must be between 0% and 100%")
# Display total allocation with color coding
if abs(total_alloc - 100) <= 0.1:
st.metric("Total Allocation (%)", f"{total_alloc:.2f}", delta=None)
@ -1691,9 +1977,7 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
st.metric("Total Allocation (%)", f"{total_alloc:.2f}",
delta=f"{total_alloc - 100:.2f}",
delta_color="off")
if abs(total_alloc - 100) > 0.1:
st.warning("Total allocation should be 100%")
st.error("Total allocation must be exactly 100%")
# Create columns for quick actions
col1, col2, col3 = st.columns(3)
@ -1709,7 +1993,7 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
# Submit button for manual edits
submitted = st.form_submit_button("Update Allocations",
disabled=abs(total_alloc - 100) > 0.1,
disabled=abs(total_alloc - 100) > 0.1 or not invalid_allocations.empty,
type="primary",
use_container_width=True)