refactor: remove duplicate ETF display from sidebar for cleaner UI
This commit is contained in:
parent
e0dc6e57eb
commit
3929f2d4f0
@ -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,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}")
|
||||
st.session_state.target = initial_capital
|
||||
st.session_state.initial_capital = initial_capital
|
||||
|
||||
# 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"
|
||||
# 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)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user