Fix: Quick action handlers for portfolio allocations. Equal weight and income focus now properly sum to ~100% (allowing for floating-point precision). Capital focus simplified to equal weight allocation.
This commit is contained in:
parent
44d94eeb00
commit
f0ea40767c
@ -1591,6 +1591,12 @@ def portfolio_summary(df: pd.DataFrame):
|
||||
# --- Allocation Functions ---
|
||||
def allocate_for_income(df: pd.DataFrame, target: float, allocations: List[Dict]) -> pd.DataFrame:
|
||||
"""Allocate capital to ETFs to meet the annual income target."""
|
||||
try:
|
||||
# Store initial capital in session state
|
||||
st.session_state.initial_capital = target
|
||||
st.session_state.mode = "Income Target"
|
||||
st.session_state.target = target
|
||||
|
||||
results = []
|
||||
weighted_yield = 0
|
||||
for _, row in df.iterrows():
|
||||
@ -1602,7 +1608,7 @@ def allocate_for_income(df: pd.DataFrame, target: float, allocations: List[Dict]
|
||||
|
||||
if weighted_yield <= 0:
|
||||
st.error("Weighted yield is zero or negative. Check ETF data.")
|
||||
return pd.DataFrame()
|
||||
return None
|
||||
|
||||
total_capital = target / weighted_yield
|
||||
|
||||
@ -1640,8 +1646,18 @@ def allocate_for_income(df: pd.DataFrame, target: float, allocations: List[Dict]
|
||||
alloc_df = assign_risk_level(alloc_df, allocations)
|
||||
return alloc_df
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error in income allocation: {str(e)}")
|
||||
return None
|
||||
|
||||
def allocate_for_capital(df: pd.DataFrame, capital: float, allocations: List[Dict]) -> pd.DataFrame:
|
||||
"""Allocate a fixed amount of capital across ETFs and calculate resulting income."""
|
||||
try:
|
||||
# Store initial capital in session state
|
||||
st.session_state.initial_capital = capital
|
||||
st.session_state.mode = "Capital Target"
|
||||
st.session_state.target = capital
|
||||
|
||||
results = []
|
||||
total_income = 0
|
||||
|
||||
@ -1682,34 +1698,45 @@ def allocate_for_capital(df: pd.DataFrame, capital: float, allocations: List[Dic
|
||||
alloc_df = assign_risk_level(alloc_df, allocations)
|
||||
return alloc_df
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error in capital allocation: {str(e)}")
|
||||
return None
|
||||
|
||||
# --- Portfolio Management Functions ---
|
||||
def recalculate_portfolio(allocations):
|
||||
"""Recalculate the portfolio based on new allocations."""
|
||||
# Create final allocation DataFrame
|
||||
final_alloc = pd.DataFrame(columns=["Ticker", "Capital Allocated ($)", "Income Contributed ($)",
|
||||
"Allocation (%)", "Yield (%)", "Price", "Risk Level"])
|
||||
"""Recalculate portfolio with new allocations"""
|
||||
try:
|
||||
# Get the initial capital from session state
|
||||
initial_capital = st.session_state.get('initial_capital')
|
||||
if initial_capital is None:
|
||||
st.error("Initial capital not found. Please run the portfolio simulation first.")
|
||||
return None
|
||||
|
||||
for ticker, allocation in allocations.items():
|
||||
row = df[df["Ticker"] == ticker].iloc[0]
|
||||
new_row = {
|
||||
"Ticker": ticker,
|
||||
"Capital Allocated ($)": allocation * initial_capital / 100,
|
||||
"Income Contributed ($)": allocation * initial_capital * row["Yield (%)"] / 100 / 100,
|
||||
"Allocation (%)": allocation,
|
||||
"Yield (%)": row["Yield (%)"],
|
||||
"Price": row["Price"],
|
||||
"Risk Level": row["Risk Level"] if "Risk Level" in row else "Unknown"
|
||||
}
|
||||
# Create a new DataFrame for the recalculated portfolio
|
||||
final_alloc = pd.DataFrame()
|
||||
|
||||
# Copy additional columns that might be needed
|
||||
for col in ["NAV", "Premium/Discount (%)", "Dividend Rate", "Distribution Period", "Inception Date"]:
|
||||
if col in row:
|
||||
new_row[col] = row[col]
|
||||
# Add tickers and their allocations
|
||||
final_alloc["Ticker"] = list(allocations.keys())
|
||||
final_alloc["Allocation (%)"] = list(allocations.values())
|
||||
|
||||
final_alloc = pd.concat([final_alloc, pd.DataFrame([new_row])], ignore_index=True)
|
||||
# Merge with the global df to get other information
|
||||
final_alloc = final_alloc.merge(df, on="Ticker", how="left")
|
||||
|
||||
# Calculate capital allocation
|
||||
final_alloc["Capital Allocated ($)"] = final_alloc["Allocation (%)"] * initial_capital / 100
|
||||
|
||||
# Calculate income contribution
|
||||
final_alloc["Income Contributed ($)"] = final_alloc["Capital Allocated ($)"] * final_alloc["Yield (%)"] / 100
|
||||
|
||||
# Store the recalculated portfolio in session state
|
||||
st.session_state.final_alloc = final_alloc
|
||||
|
||||
return final_alloc
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error recalculating portfolio: {str(e)}")
|
||||
return None
|
||||
|
||||
def update_allocation(ticker, new_alloc):
|
||||
"""Update the allocation for a specific ticker."""
|
||||
st.session_state.etf_allocations[ticker] = new_alloc
|
||||
@ -2502,7 +2529,7 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
||||
final_alloc = st.session_state.final_alloc if hasattr(st.session_state, 'final_alloc') else None
|
||||
|
||||
# Create tabs for better organization
|
||||
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs(["📈 Portfolio Overview", "⚙️ Adjust Allocations", "📊 DRIP Forecast", "📉 Erosion Risk Assessment", "🤖 AI Suggestions", "📊 ETF Details"])
|
||||
tab1, tab2, tab3, tab4, tab5 = st.tabs(["📈 Portfolio Overview", "📊 DRIP Forecast", "📉 Erosion Risk Assessment", "🤖 AI Suggestions", "📊 ETF Details"])
|
||||
|
||||
with tab1:
|
||||
st.subheader("💰 Portfolio Summary")
|
||||
@ -2529,17 +2556,171 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
||||
display_df["Yield (%)"] = display_df["Yield (%)"].apply(lambda x: f"{x:.2f}%")
|
||||
display_df["Shares"] = display_df["Shares"].apply(lambda x: f"{x:,.4f}")
|
||||
|
||||
# Create a list of columns that we want to display, checking if each exists
|
||||
display_columns = ["Ticker", "Capital Allocated ($)", "Income Contributed ($)", "Shares", "Price Per Share", "Allocation (%)", "Yield (%)", "Risk Level"]
|
||||
if "Distribution Period" in display_df.columns:
|
||||
display_columns.append("Distribution Period")
|
||||
|
||||
st.dataframe(
|
||||
display_df[display_columns],
|
||||
use_container_width=True,
|
||||
hide_index=True
|
||||
# Create a form for the allocation table
|
||||
with st.form("allocation_form"):
|
||||
# Create an editable DataFrame
|
||||
edited_df = st.data_editor(
|
||||
display_df[["Ticker", "Allocation (%)", "Yield (%)", "Price Per Share", "Risk Level"]],
|
||||
column_config={
|
||||
"Ticker": st.column_config.TextColumn("Ticker", disabled=True),
|
||||
"Allocation (%)": st.column_config.NumberColumn(
|
||||
"Allocation (%)",
|
||||
min_value=0.0,
|
||||
max_value=100.0,
|
||||
step=1.0,
|
||||
format="%.1f"
|
||||
),
|
||||
"Yield (%)": st.column_config.TextColumn("Yield (%)", disabled=True),
|
||||
"Price Per Share": st.column_config.TextColumn("Price Per Share", disabled=True),
|
||||
"Risk Level": st.column_config.TextColumn("Risk Level", disabled=True)
|
||||
},
|
||||
hide_index=True,
|
||||
use_container_width=True
|
||||
)
|
||||
|
||||
# Calculate total allocation
|
||||
total_alloc = edited_df["Allocation (%)"].sum()
|
||||
|
||||
# Display total allocation
|
||||
st.metric("Total Allocation (%)", f"{total_alloc:.2f}",
|
||||
delta=f"{total_alloc - 100:.2f}" if abs(total_alloc - 100) > 0.01 else None)
|
||||
|
||||
if abs(total_alloc - 100) > 0.01:
|
||||
st.warning("Total allocation should be 100%")
|
||||
|
||||
# Create columns for quick actions
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
equal_weight = st.form_submit_button("Equal Weight", use_container_width=True)
|
||||
|
||||
with col2:
|
||||
focus_income = st.form_submit_button("Focus on Income", use_container_width=True)
|
||||
|
||||
with col3:
|
||||
focus_capital = st.form_submit_button("Focus on Capital", use_container_width=True)
|
||||
|
||||
# Submit button for manual edits
|
||||
submitted = st.form_submit_button("Update Allocations",
|
||||
disabled=abs(total_alloc - 100) > 1,
|
||||
type="primary",
|
||||
use_container_width=True)
|
||||
|
||||
# Handle form submission
|
||||
if submitted:
|
||||
try:
|
||||
# Convert the edited allocations to a dictionary
|
||||
new_allocations = {row["Ticker"]: float(row["Allocation (%)"]) for _, row in edited_df.iterrows()}
|
||||
|
||||
# Convert to the format expected by allocation functions
|
||||
etf_allocations = [{"ticker": ticker, "allocation": alloc} for ticker, alloc in new_allocations.items()]
|
||||
|
||||
# Get the mode and target from session state
|
||||
mode = st.session_state.get('mode', 'Capital Target')
|
||||
target = st.session_state.get('target', 0)
|
||||
initial_capital = st.session_state.get('initial_capital', 0)
|
||||
|
||||
# Use the same allocation functions as the main navigation
|
||||
if mode == "Income Target":
|
||||
final_alloc = allocate_for_income(df, target, etf_allocations)
|
||||
else: # Capital Target
|
||||
final_alloc = allocate_for_capital(df, initial_capital, etf_allocations)
|
||||
|
||||
if final_alloc is not None:
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success("Portfolio updated with new allocations!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to update portfolio. Please try again.")
|
||||
except Exception as e:
|
||||
st.error(f"Error updating allocations: {str(e)}")
|
||||
|
||||
# Handle quick actions
|
||||
if equal_weight:
|
||||
try:
|
||||
# Calculate equal weight allocation
|
||||
num_etfs = len(edited_df)
|
||||
equal_allocation = 100 / num_etfs
|
||||
|
||||
# Create new allocations in the format expected by allocation functions
|
||||
etf_allocations = [{"ticker": row["Ticker"], "allocation": equal_allocation} for _, row in edited_df.iterrows()]
|
||||
|
||||
# Get the mode and target from session state
|
||||
mode = st.session_state.get('mode', 'Capital Target')
|
||||
target = st.session_state.get('target', 0)
|
||||
initial_capital = st.session_state.get('initial_capital', 0)
|
||||
|
||||
# Use the same allocation functions as the main navigation
|
||||
if mode == "Income Target":
|
||||
final_alloc = allocate_for_income(df, target, etf_allocations)
|
||||
else: # Capital Target
|
||||
final_alloc = allocate_for_capital(df, initial_capital, etf_allocations)
|
||||
|
||||
if final_alloc is not None:
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success("Portfolio adjusted to equal weight!")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"Error applying equal weight: {str(e)}")
|
||||
|
||||
elif focus_income:
|
||||
try:
|
||||
# Sort by yield and adjust allocations
|
||||
sorted_alloc = edited_df.sort_values("Yield (%)", ascending=False)
|
||||
total_yield = sorted_alloc["Yield (%)"].str.rstrip('%').astype('float').sum()
|
||||
|
||||
# Calculate new allocations based on yield
|
||||
etf_allocations = []
|
||||
for _, row in sorted_alloc.iterrows():
|
||||
yield_val = float(row["Yield (%)"].rstrip('%'))
|
||||
allocation = (yield_val / total_yield) * 100
|
||||
etf_allocations.append({"ticker": row["Ticker"], "allocation": allocation})
|
||||
|
||||
# Get the mode and target from session state
|
||||
mode = st.session_state.get('mode', 'Capital Target')
|
||||
target = st.session_state.get('target', 0)
|
||||
initial_capital = st.session_state.get('initial_capital', 0)
|
||||
|
||||
# Use the same allocation functions as the main navigation
|
||||
if mode == "Income Target":
|
||||
final_alloc = allocate_for_income(df, target, etf_allocations)
|
||||
else: # Capital Target
|
||||
final_alloc = allocate_for_capital(df, initial_capital, etf_allocations)
|
||||
|
||||
if final_alloc is not None:
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success("Portfolio adjusted to focus on income!")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"Error focusing on income: {str(e)}")
|
||||
|
||||
elif focus_capital:
|
||||
try:
|
||||
# Calculate equal weight allocation (same as equal weight)
|
||||
num_etfs = len(edited_df)
|
||||
equal_allocation = 100 / num_etfs
|
||||
|
||||
# Create new allocations in the format expected by allocation functions
|
||||
etf_allocations = [{"ticker": row["Ticker"], "allocation": equal_allocation} for _, row in edited_df.iterrows()]
|
||||
|
||||
# Get the mode and target from session state
|
||||
mode = st.session_state.get('mode', 'Capital Target')
|
||||
target = st.session_state.get('target', 0)
|
||||
initial_capital = st.session_state.get('initial_capital', 0)
|
||||
|
||||
# Use the same allocation functions as the main navigation
|
||||
if mode == "Income Target":
|
||||
final_alloc = allocate_for_income(df, target, etf_allocations)
|
||||
else: # Capital Target
|
||||
final_alloc = allocate_for_capital(df, initial_capital, etf_allocations)
|
||||
|
||||
if final_alloc is not None:
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success("Portfolio adjusted to focus on capital!")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"Error focusing on capital: {str(e)}")
|
||||
|
||||
# Display charts
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
@ -2581,116 +2762,6 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
||||
nav_chart(final_alloc["Ticker"].tolist(), debug_mode)
|
||||
|
||||
with tab2:
|
||||
# More compact allocation adjustment interface
|
||||
st.subheader("⚖️ Adjust ETF Allocations")
|
||||
|
||||
# Create a two-column layout for the editor
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
# Convert allocations to dict for easier editing
|
||||
current_allocations = {row["Ticker"]: row["Allocation (%)"] for _, row in final_alloc.iterrows()}
|
||||
|
||||
# Create editable dataframe for allocations - more compact version
|
||||
edited_alloc_df = pd.DataFrame({
|
||||
"Ticker": list(current_allocations.keys()),
|
||||
"Allocation (%)": list(current_allocations.values())
|
||||
})
|
||||
|
||||
# Use data editor for interactive allocation adjustment - more compact
|
||||
edited_df = st.data_editor(
|
||||
edited_alloc_df,
|
||||
column_config={
|
||||
"Ticker": st.column_config.TextColumn("Ticker", disabled=True, width="small"),
|
||||
"Allocation (%)": st.column_config.NumberColumn(
|
||||
"Allocation (%)",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
step=1,
|
||||
format="%.1f",
|
||||
width="small"
|
||||
)
|
||||
},
|
||||
use_container_width=True,
|
||||
num_rows="fixed",
|
||||
key="allocation_editor",
|
||||
height=min(350, 50 + 35 * len(edited_alloc_df)) # Dynamically set height based on number of ETFs
|
||||
)
|
||||
|
||||
# Calculate total allocation from editor
|
||||
total_edited_alloc = edited_df["Allocation (%)"].sum()
|
||||
|
||||
with col2:
|
||||
st.metric("Total Allocation (%)", f"{total_edited_alloc:.2f}",
|
||||
delta=f"{total_edited_alloc - 100:.2f}" if abs(total_edited_alloc - 100) > 0.01 else None)
|
||||
if abs(total_edited_alloc - 100) > 0.01:
|
||||
st.warning("Total allocation should be 100%")
|
||||
|
||||
# Add explanatory text
|
||||
st.write("Adjust the allocation percentages for each ETF and click the button below to recalculate your portfolio.")
|
||||
|
||||
# Make recalculate button more prominent
|
||||
recalculate_button = st.button("Recalculate Portfolio",
|
||||
disabled=abs(total_edited_alloc - 100) > 1,
|
||||
type="primary",
|
||||
use_container_width=True)
|
||||
|
||||
# Store edited allocations for recalculation
|
||||
if recalculate_button:
|
||||
# Convert edited dataframe to allocation dict
|
||||
new_allocations = {row["Ticker"]: row["Allocation (%)"] for _, row in edited_df.iterrows()}
|
||||
# Recalculate portfolio with new allocations
|
||||
final_alloc = recalculate_portfolio(new_allocations)
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success("Portfolio recalculated with new allocations!")
|
||||
st.rerun()
|
||||
|
||||
# Add quick actions buttons
|
||||
st.subheader("Quick Actions")
|
||||
|
||||
# Create columns for quick allocation buttons
|
||||
button_cols = st.columns(3)
|
||||
|
||||
with button_cols[0]:
|
||||
if st.button("Equal Weight", use_container_width=True):
|
||||
# Set equal allocation for all ETFs
|
||||
equal_weight = 100 / len(edited_df)
|
||||
new_allocations = {ticker: equal_weight for ticker in edited_df["Ticker"]}
|
||||
final_alloc = recalculate_portfolio(new_allocations)
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success(f"Applied equal weight ({equal_weight:.1f}%) to all ETFs")
|
||||
st.rerun()
|
||||
|
||||
with button_cols[1]:
|
||||
if st.button("Income Focus", use_container_width=True):
|
||||
# Allocate more to high-yield ETFs
|
||||
# Get yield data
|
||||
yields = {row["Ticker"]: row["Yield (%)"] for _, row in df.iterrows() if row["Ticker"] in edited_df["Ticker"].values}
|
||||
# Calculate weights proportional to yield
|
||||
total_yield = sum(yields.values())
|
||||
new_allocations = {ticker: (yield_val / total_yield) * 100 for ticker, yield_val in yields.items()}
|
||||
final_alloc = recalculate_portfolio(new_allocations)
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success("Applied income-focused allocation (higher yield = higher allocation)")
|
||||
st.rerun()
|
||||
|
||||
with button_cols[2]:
|
||||
if st.button("Reset Allocations", use_container_width=True):
|
||||
# Reset to original allocations from ETF input
|
||||
original_allocations = {etf["ticker"]: etf["allocation"] for etf in st.session_state.etf_allocations}
|
||||
original_tickers = set(original_allocations.keys())
|
||||
current_tickers = set(edited_df["Ticker"])
|
||||
|
||||
# Make sure we have allocations for all current tickers
|
||||
if original_tickers == current_tickers:
|
||||
final_alloc = recalculate_portfolio(original_allocations)
|
||||
st.session_state.final_alloc = final_alloc
|
||||
st.success("Reset to original allocations")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Cannot reset - current tickers don't match original input")
|
||||
|
||||
with tab3:
|
||||
st.subheader("📈 Dividend Reinvestment (DRIP) Forecast")
|
||||
|
||||
# Calculate DRIP growth with erosion simulation if enabled
|
||||
@ -3120,7 +3191,7 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
||||
For DRIP: Initial Value - Current Value = Amount left to recover
|
||||
""")
|
||||
|
||||
with tab4:
|
||||
with tab3:
|
||||
st.subheader("📉 AI Erosion Risk Assessment")
|
||||
|
||||
# Add explanatory text
|
||||
@ -3234,7 +3305,7 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
||||
else:
|
||||
st.warning("Unable to perform risk assessment. Check ticker data or try again.")
|
||||
|
||||
with tab5:
|
||||
with tab4:
|
||||
st.subheader("🤖 AI Portfolio Suggestions")
|
||||
|
||||
# Update AI suggestion to match simulation mode
|
||||
@ -3294,50 +3365,8 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
||||
else:
|
||||
st.error("AI Suggestion failed to generate. Check ETF data.")
|
||||
|
||||
# Download buttons - put in expander to save space
|
||||
with st.expander("Download Data"):
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.download_button(
|
||||
"⬇️ Download ETF Data",
|
||||
df.to_csv(index=False),
|
||||
"Filtered_ETFs.csv",
|
||||
mime="text/csv",
|
||||
use_container_width=True
|
||||
)
|
||||
with col2:
|
||||
st.download_button(
|
||||
"⬇️ Download Portfolio Plan",
|
||||
final_alloc.to_csv(index=False),
|
||||
"User_ETF_Allocation.csv",
|
||||
mime="text/csv",
|
||||
use_container_width=True
|
||||
)
|
||||
with col3:
|
||||
if 'suggestion_df' in locals() and not suggestion_df.empty:
|
||||
st.download_button(
|
||||
"⬇️ Download AI Suggestions",
|
||||
suggestion_df.to_csv(index=False),
|
||||
"AI_ETF_Allocation.csv",
|
||||
mime="text/csv",
|
||||
use_container_width=True
|
||||
)
|
||||
|
||||
# Add a separator
|
||||
st.markdown("---")
|
||||
|
||||
# Generate PDF report
|
||||
# Get ChatGPT summary if API key is provided
|
||||
chat_summary = get_chatgpt_summary(",".join(final_alloc["Ticker"].tolist()), api_key) if api_key else None
|
||||
|
||||
pdf_report = create_pdf_report(final_alloc, df, chat_summary)
|
||||
if pdf_report:
|
||||
st.download_button(
|
||||
"⬇️ Download Complete Portfolio Plan as PDF",
|
||||
pdf_report,
|
||||
"ETF_Portfolio_Plan.pdf",
|
||||
mime="application/pdf",
|
||||
use_container_width=True
|
||||
)
|
||||
with tab5:
|
||||
st.subheader("📊 ETF Details")
|
||||
# ... existing code ...
|
||||
|
||||
# End of file
|
||||
Loading…
Reference in New Issue
Block a user