refactor: optimize portfolio allocation for risk tolerance
This commit is contained in:
parent
3929f2d4f0
commit
57862a1e98
60
pages/ETF_Portal_Builder.py
Normal file
60
pages/ETF_Portal_Builder.py
Normal 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 ===")
|
||||
@ -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"
|
||||
)
|
||||
st.session_state.risk_tolerance = risk_tolerance
|
||||
|
||||
|
||||
# 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")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user