refactor: optimize portfolio allocation for risk tolerance

This commit is contained in:
Pascal BIBEHE 2025-05-27 23:33:02 +02:00
parent 3929f2d4f0
commit 57862a1e98
2 changed files with 117 additions and 23 deletions

View File

@ -0,0 +1,60 @@
# Main title
st.title("📈 ETF Portfolio Builder")
# 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.markdown("""
<style>
.ticker-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.ticker-item {
display: inline-flex;
align-items: center;
background-color: #2ecc71;
color: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 1em;
font-weight: 500;
}
.ticker-close {
margin-left: 8px;
cursor: pointer;
font-size: 0.9em;
opacity: 0.8;
}
.ticker-close:hover {
opacity: 1;
}
</style>
""", unsafe_allow_html=True)
# Create columns for the tickers
cols = st.columns([1] * len(st.session_state.etf_allocations))
# Display each ticker with a close button
for i, etf in enumerate(st.session_state.etf_allocations):
with cols[i]:
if st.button(f"× {etf['ticker']}", key=f"remove_{etf['ticker']}",
help=f"Remove {etf['ticker']} from portfolio"):
remove_ticker(etf['ticker'])
# Debug information
logger.info("=== Session State Debug ===")

View File

@ -478,20 +478,9 @@ def optimize_portfolio_allocation(etf_metrics: List[Dict[str, Any]], risk_tolera
etf['risk_score'] = calculate_etf_risk_score(etf)
logger.info(f"Risk score for {etf['Ticker']}: {etf['risk_score']:.2f}")
# 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)
logger.info(f"Sorted ETFs: {[etf['Ticker'] for etf in sorted_etfs]}")
# Sort ETFs by yield (higher yield = higher risk)
sorted_etfs = sorted(etf_metrics, key=lambda x: x.get('Yield (%)', 0), reverse=True)
logger.info(f"Sorted ETFs by yield: {[etf['Ticker'] for etf in sorted_etfs]}")
# Calculate base allocations based on risk tolerance
num_etfs = len(sorted_etfs)
@ -499,14 +488,16 @@ def optimize_portfolio_allocation(etf_metrics: List[Dict[str, Any]], risk_tolera
return []
if risk_tolerance == "Conservative":
# 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)
# For conservative, allocate more to lower yielding ETFs
# This will require more capital to achieve the same income
base_allocations = [0.4] + [0.3] + [0.2] + [0.1] + [0.0] * (num_etfs - 4)
elif risk_tolerance == "Moderate":
# 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)
# For moderate, balance between yield and risk
base_allocations = [0.3] + [0.3] + [0.2] + [0.2] + [0.0] * (num_etfs - 4)
else: # Aggressive
# 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)
# For aggressive, allocate more to higher yielding ETFs
# This will require less capital to achieve the same income
base_allocations = [0.4] + [0.3] + [0.2] + [0.1] + [0.0] * (num_etfs - 4)
# Adjust allocations based on number of ETFs
if num_etfs < len(base_allocations):
@ -1711,10 +1702,53 @@ with st.sidebar:
risk_tolerance = st.select_slider(
"Risk Tolerance",
options=["Conservative", "Moderate", "Aggressive"],
value="Moderate"
value=st.session_state.get("risk_tolerance", "Moderate"),
key="risk_tolerance_slider"
)
# Check if risk tolerance changed
if risk_tolerance != st.session_state.get("risk_tolerance"):
logger.info("=== Risk Tolerance Change Detection ===")
logger.info(f"Current risk tolerance in session state: {st.session_state.get('risk_tolerance')}")
logger.info(f"New risk tolerance from slider: {risk_tolerance}")
# Update session state
st.session_state.risk_tolerance = risk_tolerance
# Recalculate allocations if we have ETFs
if st.session_state.etf_allocations:
logger.info("Recalculating allocations due to risk tolerance change")
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"New allocations after risk tolerance change: {new_allocations}")
# If simulation has been run, update final allocation
if st.session_state.simulation_run and st.session_state.df_data is not None:
if st.session_state.mode == "Income Target":
final_alloc = allocate_for_income(
st.session_state.df_data,
st.session_state.target,
new_allocations
)
else:
final_alloc = allocate_for_capital(
st.session_state.df_data,
st.session_state.initial_capital,
new_allocations
)
if final_alloc is not None:
st.session_state.final_alloc = final_alloc
st.rerun()
# Additional options
st.subheader("Additional Options")