Compare commits
No commits in common. "c6797c94eee5c64e5fab1af1023ff809289d7a5c" and "edf2ce5e9cef6a609f62881d5affb3ddbb173070" have entirely different histories.
c6797c94ee
...
edf2ce5e9c
@ -58,7 +58,10 @@ class TickerData:
|
||||
class ErosionConfig:
|
||||
"""Configuration for erosion calculations"""
|
||||
erosion_type: str
|
||||
erosion_level: Dict[str, Dict[str, float]] # Changed to match NavErosionService output
|
||||
use_per_ticker: bool = False
|
||||
global_nav_rate: float = 0.0
|
||||
global_yield_rate: float = 0.0
|
||||
per_ticker_rates: Dict[str, Dict[str, float]] = field(default_factory=dict)
|
||||
|
||||
class DRIPService:
|
||||
"""Enhanced DRIP calculation service with improved performance and accuracy"""
|
||||
@ -94,9 +97,6 @@ class DRIPService:
|
||||
simulation_state = self._initialize_simulation_state(ticker_data)
|
||||
monthly_data: List[MonthlyData] = []
|
||||
|
||||
# Create monthly tracking table
|
||||
monthly_tracking = []
|
||||
|
||||
# Run monthly simulation
|
||||
for month in range(1, config.months + 1):
|
||||
# Calculate monthly income from distributions
|
||||
@ -120,17 +120,6 @@ class DRIPService:
|
||||
for ticker in ticker_data.keys()
|
||||
)
|
||||
|
||||
# Add to monthly tracking
|
||||
monthly_tracking.append({
|
||||
'Month': month,
|
||||
'Portfolio Value': total_value,
|
||||
'Monthly Income': monthly_income,
|
||||
'Cumulative Income': simulation_state['cumulative_income'],
|
||||
'Shares': {ticker: simulation_state['current_shares'][ticker] for ticker in ticker_data.keys()},
|
||||
'Prices': {ticker: simulation_state['current_prices'][ticker] for ticker in ticker_data.keys()},
|
||||
'Yields': {ticker: simulation_state['current_yields'][ticker] for ticker in ticker_data.keys()}
|
||||
})
|
||||
|
||||
# Create monthly data
|
||||
monthly_data.append(MonthlyData(
|
||||
month=month,
|
||||
@ -142,18 +131,6 @@ class DRIPService:
|
||||
yields=simulation_state['current_yields'].copy()
|
||||
))
|
||||
|
||||
# Print monthly tracking table
|
||||
print("\nMonthly DRIP Simulation Results:")
|
||||
print("=" * 100)
|
||||
print(f"{'Month':<6} {'Portfolio Value':<15} {'Monthly Income':<15} {'Cumulative Income':<15} {'Shares':<15}")
|
||||
print("-" * 100)
|
||||
|
||||
for month_data in monthly_tracking:
|
||||
shares_str = ", ".join([f"{ticker}: {shares:.4f}" for ticker, shares in month_data['Shares'].items()])
|
||||
print(f"{month_data['Month']:<6} ${month_data['Portfolio Value']:<14.2f} ${month_data['Monthly Income']:<14.2f} ${month_data['Cumulative Income']:<14.2f} {shares_str}")
|
||||
|
||||
print("=" * 100)
|
||||
|
||||
# Calculate final results
|
||||
return self._create_drip_result(monthly_data, simulation_state)
|
||||
|
||||
@ -201,13 +178,21 @@ class DRIPService:
|
||||
def _parse_erosion_config(self, config: DripConfig) -> ErosionConfig:
|
||||
"""Parse and validate erosion configuration"""
|
||||
if not hasattr(config, 'erosion_level') or config.erosion_type == "None":
|
||||
return ErosionConfig(erosion_type="None", erosion_level={})
|
||||
return ErosionConfig(erosion_type="None")
|
||||
|
||||
erosion_level = config.erosion_level
|
||||
|
||||
if isinstance(erosion_level, dict):
|
||||
return ErosionConfig(
|
||||
erosion_type=config.erosion_type,
|
||||
erosion_level=config.erosion_level
|
||||
use_per_ticker=erosion_level.get("use_per_ticker", False),
|
||||
global_nav_rate=self._normalize_erosion_rate(erosion_level.get("global", {}).get("nav", 0)),
|
||||
global_yield_rate=self._normalize_erosion_rate(erosion_level.get("global", {}).get("yield", 0)),
|
||||
per_ticker_rates=erosion_level.get("per_ticker", {})
|
||||
)
|
||||
|
||||
return ErosionConfig(erosion_type="None")
|
||||
|
||||
def _normalize_erosion_rate(self, erosion_level: float) -> float:
|
||||
"""Convert erosion level (0-9) to monthly rate with validation"""
|
||||
rate = (erosion_level / self.MAX_EROSION_LEVEL) * self.MAX_MONTHLY_EROSION
|
||||
@ -254,18 +239,11 @@ class DRIPService:
|
||||
price = state['current_prices'][ticker]
|
||||
yield_rate = state['current_yields'][ticker]
|
||||
|
||||
# Calculate distribution amount using current (eroded) values
|
||||
# Calculate distribution amount
|
||||
distribution_yield = yield_rate / data.distribution_freq.payments_per_year
|
||||
distribution_amount = shares * price * distribution_yield
|
||||
monthly_income += distribution_amount
|
||||
|
||||
# Log distribution calculation
|
||||
logger.info(f"Month {month} distribution for {ticker}:")
|
||||
logger.info(f" Shares: {shares:.4f}")
|
||||
logger.info(f" Price: ${price:.2f}")
|
||||
logger.info(f" Yield: {yield_rate:.2%}")
|
||||
logger.info(f" Distribution: ${distribution_amount:.2f}")
|
||||
|
||||
return monthly_income
|
||||
|
||||
def _apply_monthly_erosion(
|
||||
@ -274,24 +252,22 @@ class DRIPService:
|
||||
erosion_config: ErosionConfig,
|
||||
tickers: List[str]
|
||||
) -> None:
|
||||
"""Apply monthly erosion to prices and yields"""
|
||||
if erosion_config.erosion_type == "None":
|
||||
return
|
||||
"""Apply erosion to current prices and yields"""
|
||||
|
||||
for ticker in tickers:
|
||||
# Get per-ticker erosion rates
|
||||
ticker_rates = erosion_config.erosion_level.get("per_ticker", {}).get(ticker, {})
|
||||
nav_rate = ticker_rates.get("nav", 0.0) # Already in decimal form
|
||||
yield_rate = ticker_rates.get("yield", 0.0) # Already in decimal form
|
||||
if erosion_config.use_per_ticker and ticker in erosion_config.per_ticker_rates:
|
||||
# Use per-ticker erosion rates
|
||||
ticker_rates = erosion_config.per_ticker_rates[ticker]
|
||||
nav_erosion = self._normalize_erosion_rate(ticker_rates.get("nav", 0))
|
||||
yield_erosion = self._normalize_erosion_rate(ticker_rates.get("yield", 0))
|
||||
else:
|
||||
# Use global erosion rates
|
||||
nav_erosion = erosion_config.global_nav_rate
|
||||
yield_erosion = erosion_config.global_yield_rate
|
||||
|
||||
# Apply erosion directly
|
||||
state['current_prices'][ticker] *= (1 - nav_rate)
|
||||
state['current_yields'][ticker] *= (1 - yield_rate)
|
||||
|
||||
# Log erosion application
|
||||
logger.info(f"Applied erosion to {ticker}:")
|
||||
logger.info(f" NAV: {nav_rate:.4%} -> New price: ${state['current_prices'][ticker]:.2f}")
|
||||
logger.info(f" Yield: {yield_rate:.4%} -> New yield: {state['current_yields'][ticker]:.2%}")
|
||||
# Apply erosion with bounds checking
|
||||
state['current_prices'][ticker] *= max(0.01, 1 - nav_erosion)
|
||||
state['current_yields'][ticker] *= max(0.0, 1 - yield_erosion)
|
||||
|
||||
def _reinvest_dividends(
|
||||
self,
|
||||
@ -307,20 +283,15 @@ class DRIPService:
|
||||
price = state['current_prices'][ticker]
|
||||
yield_rate = state['current_yields'][ticker]
|
||||
|
||||
# Calculate dividend income using current (eroded) values
|
||||
dividend_income = shares * price * yield_rate / 12
|
||||
# Calculate dividend income
|
||||
# Note: This uses the distribution frequency from the original ticker data
|
||||
dividend_income = shares * price * yield_rate / 12 # Simplified monthly calculation
|
||||
|
||||
# Purchase additional shares at current price
|
||||
# Purchase additional shares
|
||||
if price > 0:
|
||||
new_shares = dividend_income / price
|
||||
state['current_shares'][ticker] += new_shares
|
||||
|
||||
# Log reinvestment
|
||||
logger.info(f"Month {month} reinvestment for {ticker}:")
|
||||
logger.info(f" Dividend Income: ${dividend_income:.2f}")
|
||||
logger.info(f" New Shares: {new_shares:.4f}")
|
||||
logger.info(f" Total Shares: {state['current_shares'][ticker]:.4f}")
|
||||
|
||||
def _is_distribution_month(self, month: int, frequency: DistributionFrequency) -> bool:
|
||||
"""Check if current month is a distribution month"""
|
||||
if frequency == DistributionFrequency.MONTHLY:
|
||||
|
||||
@ -1624,21 +1624,6 @@ if 'etf_allocations' not in st.session_state:
|
||||
if 'risk_tolerance' not in st.session_state:
|
||||
st.session_state.risk_tolerance = "Moderate"
|
||||
logger.info("Initialized risk_tolerance in session state")
|
||||
if 'erosion_level' not in st.session_state:
|
||||
st.session_state.erosion_level = {
|
||||
"nav": 5.0, # Default 5% annual NAV erosion
|
||||
"yield": 5.0 # Default 5% annual yield erosion
|
||||
}
|
||||
logger.info("Initialized erosion_level in session state")
|
||||
if 'erosion_type' not in st.session_state:
|
||||
st.session_state.erosion_type = "NAV & Yield Erosion"
|
||||
logger.info("Initialized erosion_type in session state")
|
||||
if 'per_ticker_erosion' not in st.session_state:
|
||||
st.session_state.per_ticker_erosion = {}
|
||||
logger.info("Initialized per_ticker_erosion in session state")
|
||||
if 'use_per_ticker_erosion' not in st.session_state:
|
||||
st.session_state.use_per_ticker_erosion = False
|
||||
logger.info("Initialized use_per_ticker_erosion in session state")
|
||||
|
||||
# Main title
|
||||
st.title("📈 ETF Portfolio Builder")
|
||||
@ -2095,20 +2080,6 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
total_income = portfolio_result.total_income
|
||||
accumulated_cash = portfolio_result.accumulated_cash
|
||||
|
||||
# Calculate initial values
|
||||
initial_investment = sum(
|
||||
etf_result.initial_value
|
||||
for etf_result in portfolio_result.etf_results.values()
|
||||
)
|
||||
initial_monthly_income = sum(
|
||||
etf_result.initial_value * (etf_result.average_yield / 12)
|
||||
for etf_result in portfolio_result.etf_results.values()
|
||||
)
|
||||
|
||||
# Calculate variations
|
||||
portfolio_variation = ((total_value - initial_investment) / initial_investment) * 100
|
||||
monthly_income_variation = ((portfolio_result.monthly_income - initial_monthly_income) / initial_monthly_income) * 100
|
||||
|
||||
# Create columns for key metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
@ -2116,15 +2087,12 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
st.metric(
|
||||
"Portfolio Value",
|
||||
f"${total_value:,.2f}",
|
||||
f"{portfolio_variation:+.1f}%" if portfolio_variation >= 0 else f"{portfolio_variation:.1f}%",
|
||||
delta_color="off" if portfolio_variation < 0 else "normal"
|
||||
f"${accumulated_cash:,.2f} cash" if accumulated_cash > 0 else None
|
||||
)
|
||||
with col2:
|
||||
st.metric(
|
||||
"Monthly Income",
|
||||
f"${portfolio_result.monthly_income:,.2f}",
|
||||
f"{monthly_income_variation:+.1f}%" if monthly_income_variation >= 0 else f"{monthly_income_variation:.1f}%",
|
||||
delta_color="off" if monthly_income_variation < 0 else "normal"
|
||||
f"${portfolio_result.monthly_income:,.2f}"
|
||||
)
|
||||
with col3:
|
||||
st.metric(
|
||||
@ -2140,14 +2108,14 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
drip_service = DRIPService()
|
||||
|
||||
# Calculate DRIP scenario
|
||||
drip_result = drip_service.forecast_portfolio(
|
||||
portfolio_result = drip_service.forecast_portfolio(
|
||||
portfolio_df=final_alloc,
|
||||
config=DripConfig(
|
||||
months=12,
|
||||
erosion_type=st.session_state.get("erosion_type", "None"),
|
||||
erosion_level={
|
||||
"nav": float(st.session_state.erosion_level.get("nav", 5.0)),
|
||||
"yield": float(st.session_state.erosion_level.get("yield", 5.0))
|
||||
"nav": st.session_state.get("erosion_level", {}).get("nav", 0),
|
||||
"yield": st.session_state.get("erosion_level", {}).get("yield", 0)
|
||||
}
|
||||
),
|
||||
tickers=tickers
|
||||
@ -2160,8 +2128,8 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
months=12,
|
||||
erosion_type=st.session_state.get("erosion_type", "None"),
|
||||
erosion_level={
|
||||
"nav": float(st.session_state.erosion_level.get("nav", 5.0)),
|
||||
"yield": float(st.session_state.erosion_level.get("yield", 5.0))
|
||||
"nav": st.session_state.get("erosion_level", {}).get("nav", 0),
|
||||
"yield": st.session_state.get("erosion_level", {}).get("yield", 0)
|
||||
}
|
||||
),
|
||||
tickers=tickers
|
||||
@ -2171,7 +2139,7 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
comparison_data = {
|
||||
"Strategy": ["DRIP", "No-DRIP"],
|
||||
"Portfolio Value": [
|
||||
drip_result.total_value,
|
||||
portfolio_result.total_value,
|
||||
nodrip_result.total_value
|
||||
],
|
||||
"Accumulated Cash": [
|
||||
@ -2179,7 +2147,7 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
nodrip_result.accumulated_cash
|
||||
],
|
||||
"Total Value": [
|
||||
drip_result.total_value,
|
||||
portfolio_result.total_value,
|
||||
nodrip_result.total_value + nodrip_result.accumulated_cash
|
||||
]
|
||||
}
|
||||
@ -2191,7 +2159,7 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
fig.add_trace(go.Bar(
|
||||
name="DRIP",
|
||||
x=["Portfolio Value"],
|
||||
y=[drip_result.total_value],
|
||||
y=[portfolio_result.total_value],
|
||||
marker_color="#1f77b4"
|
||||
))
|
||||
|
||||
@ -2233,12 +2201,12 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
"Share Growth"
|
||||
],
|
||||
"DRIP": [
|
||||
f"${drip_result.total_value:,.2f}",
|
||||
f"${portfolio_result.total_value:,.2f}",
|
||||
"$0.00",
|
||||
f"${drip_result.total_value:,.2f}",
|
||||
f"${drip_result.monthly_income:,.2f}",
|
||||
f"${drip_result.total_income:,.2f}",
|
||||
f"{((drip_result.etf_results[tickers[0]].final_shares / drip_result.etf_results[tickers[0]].initial_shares - 1) * 100):.1f}%"
|
||||
f"${portfolio_result.total_value:,.2f}",
|
||||
f"${portfolio_result.monthly_income:,.2f}",
|
||||
f"${portfolio_result.total_income:,.2f}",
|
||||
f"{((portfolio_result.etf_results[tickers[0]].final_shares / portfolio_result.etf_results[tickers[0]].initial_shares - 1) * 100):.1f}%"
|
||||
],
|
||||
"No-DRIP": [
|
||||
f"${nodrip_result.total_value:,.2f}",
|
||||
@ -2261,70 +2229,6 @@ def display_drip_forecast(portfolio_result, tickers):
|
||||
- Portfolio value changes due to NAV erosion and share growth (DRIP) or cash accumulation (No-DRIP)
|
||||
""")
|
||||
|
||||
# Add detailed allocation table for validation
|
||||
st.subheader("Detailed Allocation")
|
||||
|
||||
# Create detailed allocation data
|
||||
allocation_data = []
|
||||
for ticker, etf_result in portfolio_result.etf_results.items():
|
||||
# Get initial values
|
||||
initial_value = etf_result.initial_value
|
||||
initial_shares = etf_result.initial_shares
|
||||
initial_yield = etf_result.average_yield
|
||||
initial_monthly_income = initial_value * (initial_yield / 12)
|
||||
|
||||
# Get final values for comparison
|
||||
final_value = etf_result.final_value
|
||||
final_shares = etf_result.final_shares
|
||||
final_monthly_income = final_value * (etf_result.average_yield / 12)
|
||||
|
||||
# Calculate variations
|
||||
value_variation = ((final_value - initial_value) / initial_value) * 100
|
||||
shares_variation = ((final_shares - initial_shares) / initial_shares) * 100
|
||||
income_variation = ((final_monthly_income - initial_monthly_income) / initial_monthly_income) * 100
|
||||
|
||||
allocation_data.append({
|
||||
"Ticker": ticker,
|
||||
"Initial Value": f"${initial_value:,.2f}",
|
||||
"Initial Shares": f"{initial_shares:,.4f}",
|
||||
"Initial Monthly Income": f"${initial_monthly_income:,.2f}",
|
||||
"Final Value": f"${final_value:,.2f}",
|
||||
"Final Shares": f"{final_shares:,.4f}",
|
||||
"Final Monthly Income": f"${final_monthly_income:,.2f}",
|
||||
"Value Change": f"{value_variation:+.1f}%",
|
||||
"Shares Change": f"{shares_variation:+.1f}%",
|
||||
"Income Change": f"{income_variation:+.1f}%"
|
||||
})
|
||||
|
||||
# Create DataFrame and display
|
||||
allocation_df = pd.DataFrame(allocation_data)
|
||||
st.dataframe(
|
||||
allocation_df,
|
||||
use_container_width=True,
|
||||
hide_index=True,
|
||||
column_config={
|
||||
"Ticker": st.column_config.TextColumn("Ticker", disabled=True),
|
||||
"Initial Value": st.column_config.TextColumn("Initial Value", disabled=True),
|
||||
"Initial Shares": st.column_config.TextColumn("Initial Shares", disabled=True),
|
||||
"Initial Monthly Income": st.column_config.TextColumn("Initial Monthly Income", disabled=True),
|
||||
"Final Value": st.column_config.TextColumn("Final Value", disabled=True),
|
||||
"Final Shares": st.column_config.TextColumn("Final Shares", disabled=True),
|
||||
"Final Monthly Income": st.column_config.TextColumn("Final Monthly Income", disabled=True),
|
||||
"Value Change": st.column_config.TextColumn("Value Change", disabled=True),
|
||||
"Shares Change": st.column_config.TextColumn("Shares Change", disabled=True),
|
||||
"Income Change": st.column_config.TextColumn("Income Change", disabled=True)
|
||||
}
|
||||
)
|
||||
|
||||
# Add explanation
|
||||
st.info("""
|
||||
**Table Explanation:**
|
||||
- Initial Values: Starting values before DRIP and erosion effects
|
||||
- Final Values: Values after applying DRIP and erosion effects
|
||||
- Changes: Percentage variations between initial and final values
|
||||
- Positive changes indicate growth, negative changes indicate erosion
|
||||
""")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error calculating DRIP forecast: {str(e)}")
|
||||
logger.error(f"DRIP forecast error: {str(e)}")
|
||||
@ -2366,99 +2270,45 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
||||
st.error(f"Error displaying capital investment information: {str(e)}")
|
||||
|
||||
with tab2:
|
||||
st.subheader("DRIP Forecast")
|
||||
st.subheader("Dividend Reinvestment (DRIP) Forecast")
|
||||
st.write("This forecast shows the growth of your portfolio over time if dividends are reinvested instead of taken as income.")
|
||||
|
||||
# Calculate DRIP scenario
|
||||
logger.info("Calculating DRIP scenario...")
|
||||
try:
|
||||
# Get portfolio data
|
||||
tickers = final_alloc["Ticker"].tolist()
|
||||
initial_investment = final_alloc["Capital Allocated ($)"].sum()
|
||||
risk_tolerance = st.session_state.get("risk_tolerance", "Moderate")
|
||||
|
||||
# DRIP simulation parameters
|
||||
months = st.slider("Forecast Period (Months)", 1, 60, 12)
|
||||
|
||||
# Initialize DRIP service
|
||||
from ETF_Portal.services.drip_service import DRIPService
|
||||
|
||||
# Initialize DRIP service
|
||||
drip_service = DRIPService()
|
||||
|
||||
# Get erosion values from nav_erosion_service
|
||||
from ETF_Portal.services.nav_erosion_service import NavErosionService
|
||||
erosion_service = NavErosionService()
|
||||
erosion_analysis = erosion_service.analyze_etf_erosion_risk(final_alloc["Ticker"].tolist())
|
||||
|
||||
# Update erosion values if analysis is available
|
||||
if erosion_analysis and erosion_analysis.results:
|
||||
# Use the highest erosion values from the analysis
|
||||
nav_erosion = max(result.estimated_nav_erosion * 100 for result in erosion_analysis.results)
|
||||
yield_erosion = max(result.estimated_yield_erosion * 100 for result in erosion_analysis.results)
|
||||
|
||||
st.session_state.erosion_level = {
|
||||
"nav": float(nav_erosion),
|
||||
"yield": float(yield_erosion)
|
||||
}
|
||||
st.session_state.erosion_type = "NAV & Yield Erosion"
|
||||
|
||||
# Create DRIP config with per-ticker rates
|
||||
config = DripConfig(
|
||||
months=12,
|
||||
erosion_type=st.session_state.erosion_type,
|
||||
erosion_level={
|
||||
"nav": float(st.session_state.erosion_level.get("nav", 5.0)),
|
||||
"yield": float(st.session_state.erosion_level.get("yield", 5.0))
|
||||
}
|
||||
)
|
||||
|
||||
# Debug information
|
||||
st.write("Debug Information:")
|
||||
st.write(f"Session state erosion_level: {st.session_state.erosion_level}")
|
||||
st.write(f"Session state erosion_type: {st.session_state.erosion_type}")
|
||||
|
||||
# Calculate DRIP result
|
||||
drip_result = drip_service.calculate_drip_growth(
|
||||
# Calculate DRIP forecast
|
||||
portfolio_result = drip_service.forecast_portfolio(
|
||||
portfolio_df=final_alloc,
|
||||
config=config
|
||||
config=DripConfig(
|
||||
months=months,
|
||||
erosion_type=st.session_state.get("erosion_type", "None"),
|
||||
erosion_level={
|
||||
"nav": st.session_state.get("erosion_level", {}).get("nav", 0),
|
||||
"yield": st.session_state.get("erosion_level", {}).get("yield", 0)
|
||||
}
|
||||
),
|
||||
tickers=tickers
|
||||
)
|
||||
|
||||
# Display summary metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Portfolio Value", f"${drip_result.final_portfolio_value:,.2f}")
|
||||
with col2:
|
||||
# Calculate monthly income from total income
|
||||
monthly_income = drip_result.total_income / 12
|
||||
st.metric("Monthly Income", f"${monthly_income:,.2f}")
|
||||
with col3:
|
||||
st.metric("Total Income", f"${drip_result.total_income:,.2f}")
|
||||
# Display DRIP forecast results
|
||||
display_drip_forecast(portfolio_result, tickers)
|
||||
|
||||
# Display monthly tracking table
|
||||
st.subheader("Monthly Details")
|
||||
|
||||
# Create DataFrame for monthly tracking
|
||||
monthly_data = []
|
||||
for month_data in drip_result.monthly_data:
|
||||
shares_str = ", ".join([f"{ticker}: {shares:.4f}" for ticker, shares in month_data.shares.items()])
|
||||
monthly_data.append({
|
||||
'Month': month_data.month,
|
||||
'Portfolio Value': f"${month_data.total_value:,.2f}",
|
||||
'Monthly Income': f"${month_data.monthly_income:,.2f}",
|
||||
'Cumulative Income': f"${month_data.cumulative_income:,.2f}",
|
||||
'Shares': shares_str,
|
||||
'Prices': ", ".join([f"{ticker}: ${price:.2f}" for ticker, price in month_data.prices.items()]),
|
||||
'Yields': ", ".join([f"{ticker}: {yield_rate:.2%}" for ticker, yield_rate in month_data.yields.items()])
|
||||
})
|
||||
|
||||
# Convert to DataFrame and display
|
||||
monthly_df = pd.DataFrame(monthly_data)
|
||||
st.dataframe(monthly_df, use_container_width=True)
|
||||
|
||||
# Add download button for the data
|
||||
csv = monthly_df.to_csv(index=False)
|
||||
st.download_button(
|
||||
label="Download Monthly Data",
|
||||
data=csv,
|
||||
file_name="drip_monthly_details.csv",
|
||||
mime="text/csv"
|
||||
)
|
||||
|
||||
st.write("DRIP scenario calculated successfully")
|
||||
except Exception as e:
|
||||
st.error(f"Error calculating DRIP scenario: {str(e)}")
|
||||
st.error(traceback.format_exc())
|
||||
st.stop()
|
||||
st.error(f"Error calculating DRIP forecast: {str(e)}")
|
||||
logger.error(f"DRIP forecast error: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
with tab3:
|
||||
st.subheader("📉 Erosion Risk Assessment")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user