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})
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 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})
# 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 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]
# 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]
logger.info(f"Final allocations: {allocations}")
return allocations
# 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
})
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)}")
# 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)
# 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!")
# 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,88 +1732,55 @@ 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:
# 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:
try:
if not etf_inputs:
st.error("Please enter at least one ETF ticker")
# Store parameters in session state
st.session_state.mode = simulation_mode
st.session_state.enable_drip = enable_drip == "Yes"
st.session_state.enable_erosion = enable_erosion == "Yes"
if simulation_mode == "Income Target":
st.session_state.target = monthly_target
else:
logger.info(f"Form submitted with {len(etf_inputs)} ETFs: {etf_inputs}")
# Store parameters in session state
st.session_state.mode = simulation_mode
st.session_state.enable_drip = enable_drip == "Yes"
st.session_state.enable_erosion = enable_erosion == "Yes"
st.session_state.target = initial_capital
st.session_state.initial_capital = initial_capital
# Run simulation
logger.info("Starting portfolio simulation...")
logger.info(f"ETF allocations: {st.session_state.etf_allocations}")
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:
if simulation_mode == "Income Target":
st.session_state.target = monthly_target
logger.info(f"Allocating for income target: ${monthly_target}")
final_alloc = allocate_for_income(df_data, monthly_target, st.session_state.etf_allocations)
else:
st.session_state.target = initial_capital
st.session_state.initial_capital = initial_capital
logger.info(f"Allocating for capital target: ${initial_capital}")
final_alloc = allocate_for_capital(df_data, initial_capital, st.session_state.etf_allocations)
# Run simulation
logger.info("Starting portfolio simulation...")
logger.info(f"ETF inputs: {etf_inputs}")
logger.info(f"Final allocation result:\n{final_alloc}")
df_data = fetch_etf_data([etf["ticker"] for etf in etf_inputs])
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)
else:
logger.info(f"Allocating for capital target: ${initial_capital}")
final_alloc = allocate_for_capital(df_data, initial_capital, 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
st.success("Portfolio simulation completed!")
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}")
if final_alloc is not None and not final_alloc.empty:
st.session_state.simulation_run = True
st.session_state.df_data = df_data
st.session_state.final_alloc = final_alloc
st.success("Portfolio simulation completed!")
st.rerun()
else:
st.error("Failed to fetch ETF data. Please check your tickers and try again.")
logger.error("ETF data fetch returned empty DataFrame")
st.error("Failed to generate portfolio allocation. Please check your inputs and try again.")
else:
st.error("Failed to fetch ETF data. Please check your tickers and try again.")
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)