From 783b2580d88d8a3404d46421a78d565b46a1a2c3 Mon Sep 17 00:00:00 2001 From: Pascal Date: Thu, 5 Jun 2025 12:31:05 +0200 Subject: [PATCH] Allocation (%) can be updated --- pages/ETF_Portfolio_Builder.py | 152 +++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 63 deletions(-) diff --git a/pages/ETF_Portfolio_Builder.py b/pages/ETF_Portfolio_Builder.py index f461bad..c05953d 100644 --- a/pages/ETF_Portfolio_Builder.py +++ b/pages/ETF_Portfolio_Builder.py @@ -1133,62 +1133,22 @@ def portfolio_summary(final_alloc: pd.DataFrame) -> None: if final_alloc is None or final_alloc.empty: st.warning("No portfolio data available.") return - + try: - # Calculate key metrics - total_capital = final_alloc["Capital Allocated ($)"].sum() - total_income = final_alloc["Income Contributed ($)"].sum() - - # Calculate weighted average yield - weighted_yield = (final_alloc["Allocation (%)"] * final_alloc["Yield (%)"]).sum() / 100 - - # Display metrics in columns - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("Total Capital", format_large_number(total_capital)) - - with col2: - st.metric("Annual Income", format_large_number(total_income)) - st.metric("Monthly Income", format_large_number(total_income/12)) - - with col3: - st.metric("Average Yield", f"{weighted_yield:.2f}%") - st.metric("Effective Yield", f"{(total_income/total_capital*100):.2f}%") - - # Display allocation chart - fig = px.pie( - final_alloc, - values="Allocation (%)", - names="Ticker", - title="Portfolio Allocation by ETF", - hover_data={ - "Ticker": True, - "Allocation (%)": ":.2f", - "Yield (%)": ":.2f", - "Capital Allocated ($)": ":,.2f", - "Income Contributed ($)": ":,.2f" - } - ) - st.plotly_chart(fig, use_container_width=True) - - # Display detailed allocation table - st.subheader("Detailed Allocation") + # Use the allocation from session state for all calculations and display display_df = final_alloc.copy() + numeric_columns = ["Allocation (%)", "Yield (%)", "Price", "Shares", "Capital Allocated ($)", "Income Contributed ($)"] + for col in numeric_columns: + if col in display_df.columns: + display_df[col] = pd.to_numeric(display_df[col], errors='coerce') display_df["Monthly Income"] = display_df["Income Contributed ($)"] / 12 - - # Format large numbers in the display DataFrame - display_df["Capital Allocated ($)"] = display_df["Capital Allocated ($)"].apply(format_large_number) - display_df["Income Contributed ($)"] = display_df["Income Contributed ($)"].apply(format_large_number) - display_df["Monthly Income"] = display_df["Monthly Income"].apply(format_large_number) - - # Ensure data_source column exists and rename it for display + display_df["Capital Allocated ($)"] = display_df["Capital Allocated ($)"].apply(lambda x: f"${float(x):,.2f}") + display_df["Income Contributed ($)"] = display_df["Income Contributed ($)"].apply(lambda x: f"${float(x):,.2f}") + display_df["Monthly Income"] = display_df["Monthly Income"].apply(lambda x: f"${float(x):,.2f}") if "data_source" in display_df.columns: display_df = display_df.rename(columns={"data_source": "Data Source"}) else: display_df["Data Source"] = "Unknown" - - # Select and order columns for display display_columns = [ "Ticker", "Allocation (%)", @@ -1201,15 +1161,43 @@ def portfolio_summary(final_alloc: pd.DataFrame) -> None: "Risk Level", "Data Source" ] - - # Format the display - st.dataframe( - display_df[display_columns].style.format({ - "Allocation (%)": "{:.2f}%", - "Yield (%)": "{:.2f}%", - "Price": "${:,.2f}", - "Shares": "{:,.4f}" - }), + editor_key = "allocation_editor" + def parse_currency(val): + if isinstance(val, str): + return float(val.replace('$', '').replace(',', '')) + return float(val) + # --- Portfolio Summary Metrics (use session state allocation) --- + total_capital = display_df["Capital Allocated ($)"].apply(parse_currency).sum() + total_income = display_df["Income Contributed ($)"].apply(parse_currency).sum() + weighted_yield = (display_df["Allocation (%)"] * display_df["Yield (%)"]).sum() / 100 + col1, col2, col3 = st.columns(3) + with col1: + st.metric("Total Capital", f"${total_capital:,.2f}") + with col2: + st.metric("Annual Income", f"${total_income:,.2f}") + st.metric("Monthly Income", f"${total_income/12:,.2f}") + with col3: + st.metric("Average Yield", f"{weighted_yield:.2f}%") + st.metric("Effective Yield", f"{(total_income/total_capital*100):.2f}%") + # --- Pie chart (use session state allocation) --- + fig = px.pie( + display_df, + values="Allocation (%)", + names="Ticker", + title="Portfolio Allocation by ETF", + hover_data={ + "Ticker": True, + "Allocation (%)": ":.2f", + "Yield (%)": ":.2f", + "Capital Allocated ($)": ":,.2f", + "Income Contributed ($)": ":,.2f" + } + ) + st.plotly_chart(fig, use_container_width=True) + # --- Editable allocation table below the pie chart --- + st.subheader("Detailed Allocation") + edited_df = st.data_editor( + display_df[display_columns], column_config={ "Ticker": st.column_config.TextColumn("Ticker", disabled=True), "Allocation (%)": st.column_config.NumberColumn( @@ -1220,9 +1208,21 @@ def portfolio_summary(final_alloc: pd.DataFrame) -> None: format="%.1f", required=True ), - "Yield (%)": st.column_config.TextColumn("Yield (%)", disabled=True), - "Price": st.column_config.TextColumn("Price", disabled=True), - "Shares": st.column_config.TextColumn("Shares", disabled=True), + "Yield (%)": st.column_config.NumberColumn( + "Yield (%)", + format="%.2f", + disabled=True + ), + "Price": st.column_config.NumberColumn( + "Price", + format="%.2f", + disabled=True + ), + "Shares": st.column_config.NumberColumn( + "Shares", + format="%.2f", + disabled=True + ), "Capital Allocated ($)": st.column_config.TextColumn("Capital Allocated ($)", disabled=True), "Monthly Income": st.column_config.TextColumn("Monthly Income", disabled=True), "Income Contributed ($)": st.column_config.TextColumn("Income Contributed ($)", disabled=True), @@ -1230,9 +1230,35 @@ def portfolio_summary(final_alloc: pd.DataFrame) -> None: "Data Source": st.column_config.TextColumn("Data Source", disabled=True) }, hide_index=True, - use_container_width=True + use_container_width=True, + key=editor_key ) - + edited_df = edited_df.reset_index(drop=True) + # --- Update Allocations Button --- + if st.button("Update Allocations", type="primary"): + try: + edited_df["Allocation (%)"] = pd.to_numeric(edited_df["Allocation (%)"], errors='coerce') + total_allocation = edited_df["Allocation (%)"].sum() + if abs(total_allocation - 100.0) > 0.01: + st.error(f"Total allocation must be 100%. Current total: {total_allocation:.2f}%") + else: + edited_df["Capital Allocated ($)"] = edited_df["Capital Allocated ($)"].apply(parse_currency) + edited_df["Income Contributed ($)"] = edited_df["Income Contributed ($)"].apply(parse_currency) + edited_df["Monthly Income"] = edited_df["Monthly Income"].apply(parse_currency) + total_capital = edited_df["Capital Allocated ($)"].sum() + for idx, row in edited_df.iterrows(): + edited_df.at[idx, "Capital Allocated ($)"] = total_capital * (row["Allocation (%)"] / 100) + edited_df.at[idx, "Income Contributed ($)"] = edited_df.at[idx, "Capital Allocated ($)"] * (row["Yield (%)"] / 100) + edited_df.at[idx, "Monthly Income"] = edited_df.at[idx, "Income Contributed ($)"] / 12 + edited_df.at[idx, "Shares"] = edited_df.at[idx, "Capital Allocated ($)"] / row["Price"] + # Update the session state with new allocations and trigger rerun + st.session_state.final_alloc = edited_df + st.success("Allocations updated successfully! Click 'Run Portfolio Simulation' to recalculate.") + st.rerun() + except Exception as e: + st.error(f"Error updating allocations: {str(e)}") + logger.error(f"Error in allocation update: {str(e)}") + logger.error(traceback.format_exc()) except Exception as e: st.error(f"Error calculating portfolio summary: {str(e)}") logger.error(f"Error in portfolio_summary: {str(e)}")