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)
|
etf['risk_score'] = calculate_etf_risk_score(etf)
|
||||||
logger.info(f"Risk score for {etf['Ticker']}: {etf['risk_score']:.2f}")
|
logger.info(f"Risk score for {etf['Ticker']}: {etf['risk_score']:.2f}")
|
||||||
|
|
||||||
# Sort ETFs by risk score based on risk tolerance
|
# Sort ETFs by yield (higher yield = higher risk)
|
||||||
if risk_tolerance == "Conservative":
|
sorted_etfs = sorted(etf_metrics, key=lambda x: x.get('Yield (%)', 0), reverse=True)
|
||||||
# For conservative, prefer lower risk
|
logger.info(f"Sorted ETFs by yield: {[etf['Ticker'] for etf in sorted_etfs]}")
|
||||||
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]}")
|
|
||||||
|
|
||||||
# Calculate base allocations based on risk tolerance
|
# Calculate base allocations based on risk tolerance
|
||||||
num_etfs = len(sorted_etfs)
|
num_etfs = len(sorted_etfs)
|
||||||
@ -499,14 +488,16 @@ def optimize_portfolio_allocation(etf_metrics: List[Dict[str, Any]], risk_tolera
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if risk_tolerance == "Conservative":
|
if risk_tolerance == "Conservative":
|
||||||
# 50% to low risk, 30% to medium risk, 20% to high risk
|
# For conservative, allocate more to lower yielding ETFs
|
||||||
base_allocations = [0.5] + [0.3] + [0.2] + [0.0] * (num_etfs - 3)
|
# 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":
|
elif risk_tolerance == "Moderate":
|
||||||
# 40% to medium risk, 30% to low risk, 30% to high risk
|
# For moderate, balance between yield and risk
|
||||||
base_allocations = [0.4] + [0.3] + [0.3] + [0.0] * (num_etfs - 3)
|
base_allocations = [0.3] + [0.3] + [0.2] + [0.2] + [0.0] * (num_etfs - 4)
|
||||||
else: # Aggressive
|
else: # Aggressive
|
||||||
# 40% to high risk, 40% to medium risk, 20% to low risk
|
# For aggressive, allocate more to higher yielding ETFs
|
||||||
base_allocations = [0.4] + [0.4] + [0.2] + [0.0] * (num_etfs - 3)
|
# 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
|
# Adjust allocations based on number of ETFs
|
||||||
if num_etfs < len(base_allocations):
|
if num_etfs < len(base_allocations):
|
||||||
@ -1711,9 +1702,52 @@ with st.sidebar:
|
|||||||
risk_tolerance = st.select_slider(
|
risk_tolerance = st.select_slider(
|
||||||
"Risk Tolerance",
|
"Risk Tolerance",
|
||||||
options=["Conservative", "Moderate", "Aggressive"],
|
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
|
# Additional options
|
||||||
st.subheader("Additional Options")
|
st.subheader("Additional Options")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user