Fixing minor computing issues

This commit is contained in:
Pascal BIBEHE 2025-06-03 19:59:44 +02:00
parent 19f713673e
commit 30dc087ce3
2 changed files with 91 additions and 29 deletions

View File

@ -328,12 +328,12 @@ class NoDRIPService:
return monthly_income return monthly_income
def _apply_monthly_erosion( def _apply_monthly_erosion(
self, self,
state: Dict[str, Any], state: Dict[str, Any],
erosion_config: ErosionConfig, erosion_config: ErosionConfig,
tickers: List[str] tickers: List[str]
) -> None: ) -> None:
"""Apply monthly erosion to prices and yields (reuse from DRIP service)""" """Apply monthly erosion to prices and yields"""
if erosion_config.erosion_type == "None": if erosion_config.erosion_type == "None":
return return
@ -355,29 +355,48 @@ class NoDRIPService:
logger.warning(f"No erosion rates found for ticker {ticker}, skipping erosion") logger.warning(f"No erosion rates found for ticker {ticker}, skipping erosion")
continue continue
nav_rate = ticker_rates.get("nav", 0.0) # Monthly rate in decimal form # Get annual erosion rates (assumed to be in percentage form, e.g., 6.7 for 6.7%)
yield_rate = ticker_rates.get("yield", 0.0) # Monthly rate in decimal form nav_annual_pct = ticker_rates.get("nav", 0.0)
yield_annual_pct = ticker_rates.get("yield", 0.0)
# Convert annual percentage to monthly rate using compound formula
# Monthly rate = (1 + annual_rate)^(1/12) - 1
nav_annual_rate = nav_annual_pct / 100.0 # Convert percentage to decimal
yield_annual_rate = yield_annual_pct / 100.0 # Convert percentage to decimal
# Calculate monthly erosion rates using compound formula
if nav_annual_rate > 0:
nav_monthly_rate = (1 + nav_annual_rate) ** (1/12) - 1
else:
nav_monthly_rate = 0.0
if yield_annual_rate > 0:
yield_monthly_rate = (1 + yield_annual_rate) ** (1/12) - 1
else:
yield_monthly_rate = 0.0
# Validate rates are reasonable (0 to 5% monthly max) # Validate rates are reasonable (0 to 5% monthly max)
nav_rate = max(0.0, min(nav_rate, self.MAX_MONTHLY_EROSION)) nav_monthly_rate = max(0.0, min(nav_monthly_rate, self.MAX_MONTHLY_EROSION))
yield_rate = max(0.0, min(yield_rate, self.MAX_MONTHLY_EROSION)) yield_monthly_rate = max(0.0, min(yield_monthly_rate, self.MAX_MONTHLY_EROSION))
# Store original values for logging # Store original values for logging
original_price = state['current_prices'][ticker] original_price = state['current_prices'][ticker]
original_yield = state['current_yields'][ticker] original_yield = state['current_yields'][ticker]
# Apply erosion directly (rates are already monthly) # Apply monthly erosion
state['current_prices'][ticker] *= (1 - nav_rate) state['current_prices'][ticker] *= (1 - nav_monthly_rate)
state['current_yields'][ticker] *= (1 - yield_rate) state['current_yields'][ticker] *= (1 - yield_monthly_rate)
# Ensure prices and yields don't go below reasonable minimums # Ensure prices and yields don't go below reasonable minimums
state['current_prices'][ticker] = max(state['current_prices'][ticker], 0.01) state['current_prices'][ticker] = max(state['current_prices'][ticker], 0.01)
state['current_yields'][ticker] = max(state['current_yields'][ticker], 0.0) state['current_yields'][ticker] = max(state['current_yields'][ticker], 0.0)
# Log erosion application # Log erosion application with both annual and monthly rates
logger.info(f"Applied monthly erosion to {ticker} (No-DRIP):") logger.info(f"Applied monthly erosion to {ticker} (No-DRIP):")
logger.info(f" NAV: {nav_rate:.4%} -> Price: ${original_price:.2f} -> ${state['current_prices'][ticker]:.2f}") logger.info(f" NAV: {nav_annual_pct:.1f}% annual -> {nav_monthly_rate:.4%} monthly")
logger.info(f" Yield: {yield_rate:.4%} -> Yield: {original_yield:.2%} -> {state['current_yields'][ticker]:.2%}") logger.info(f" Price: ${original_price:.2f} -> ${state['current_prices'][ticker]:.2f}")
logger.info(f" Yield: {yield_annual_pct:.1f}% annual -> {yield_monthly_rate:.4%} monthly")
logger.info(f" Yield: {original_yield:.2%} -> {state['current_yields'][ticker]:.2%}")
def _is_distribution_month(self, month: int, frequency: DistributionFrequency) -> bool: def _is_distribution_month(self, month: int, frequency: DistributionFrequency) -> bool:
"""Check if current month is a distribution month (reuse from DRIP service)""" """Check if current month is a distribution month (reuse from DRIP service)"""

View File

@ -350,29 +350,48 @@ class DRIPService:
logger.warning(f"No erosion rates found for ticker {ticker}, skipping erosion") logger.warning(f"No erosion rates found for ticker {ticker}, skipping erosion")
continue continue
nav_rate = ticker_rates.get("nav", 0.0) # Monthly rate in decimal form # Get annual erosion rates (assumed to be in percentage form, e.g., 6.7 for 6.7%)
yield_rate = ticker_rates.get("yield", 0.0) # Monthly rate in decimal form nav_annual_pct = ticker_rates.get("nav", 0.0)
yield_annual_pct = ticker_rates.get("yield", 0.0)
# Convert annual percentage to monthly rate using compound formula
# Monthly rate = (1 + annual_rate)^(1/12) - 1
nav_annual_rate = nav_annual_pct / 100.0 # Convert percentage to decimal
yield_annual_rate = yield_annual_pct / 100.0 # Convert percentage to decimal
# Calculate monthly erosion rates using compound formula
if nav_annual_rate > 0:
nav_monthly_rate = (1 + nav_annual_rate) ** (1/12) - 1
else:
nav_monthly_rate = 0.0
if yield_annual_rate > 0:
yield_monthly_rate = (1 + yield_annual_rate) ** (1/12) - 1
else:
yield_monthly_rate = 0.0
# Validate rates are reasonable (0 to 5% monthly max) # Validate rates are reasonable (0 to 5% monthly max)
nav_rate = max(0.0, min(nav_rate, self.MAX_MONTHLY_EROSION)) nav_monthly_rate = max(0.0, min(nav_monthly_rate, self.MAX_MONTHLY_EROSION))
yield_rate = max(0.0, min(yield_rate, self.MAX_MONTHLY_EROSION)) yield_monthly_rate = max(0.0, min(yield_monthly_rate, self.MAX_MONTHLY_EROSION))
# Store original values for logging # Store original values for logging
original_price = state['current_prices'][ticker] original_price = state['current_prices'][ticker]
original_yield = state['current_yields'][ticker] original_yield = state['current_yields'][ticker]
# Apply erosion directly (rates are already monthly) # Apply monthly erosion
state['current_prices'][ticker] *= (1 - nav_rate) state['current_prices'][ticker] *= (1 - nav_monthly_rate)
state['current_yields'][ticker] *= (1 - yield_rate) state['current_yields'][ticker] *= (1 - yield_monthly_rate)
# Ensure prices and yields don't go below reasonable minimums # Ensure prices and yields don't go below reasonable minimums
state['current_prices'][ticker] = max(state['current_prices'][ticker], 0.01) state['current_prices'][ticker] = max(state['current_prices'][ticker], 0.01)
state['current_yields'][ticker] = max(state['current_yields'][ticker], 0.0) state['current_yields'][ticker] = max(state['current_yields'][ticker], 0.0)
# Log erosion application # Log erosion application with both annual and monthly rates
logger.info(f"Applied monthly erosion to {ticker}:") logger.info(f"Applied monthly erosion to {ticker}:")
logger.info(f" NAV: {nav_rate:.4%} -> Price: ${original_price:.2f} -> ${state['current_prices'][ticker]:.2f}") logger.info(f" NAV: {nav_annual_pct:.1f}% annual -> {nav_monthly_rate:.4%} monthly")
logger.info(f" Yield: {yield_rate:.4%} -> Yield: {original_yield:.2%} -> {state['current_yields'][ticker]:.2%}") logger.info(f" Price: ${original_price:.2f} -> ${state['current_prices'][ticker]:.2f}")
logger.info(f" Yield: {yield_annual_pct:.1f}% annual -> {yield_monthly_rate:.4%} monthly")
logger.info(f" Yield: {original_yield:.2%} -> {state['current_yields'][ticker]:.2%}")
def _reinvest_dividends( def _reinvest_dividends(
self, self,
@ -621,23 +640,43 @@ class DRIPService:
initial_investment: float, initial_investment: float,
value_extractor: callable value_extractor: callable
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Calculate break-even analysis for a strategy""" """
Calculate break-even analysis for a strategy.
Break-even occurs when total value exceeds initial investment,
accounting for the erosion effects on the portfolio.
"""
break_even_month = None break_even_month = None
profit_at_break_even = 0.0 profit_at_break_even = 0.0
max_total_return = -float('inf')
logger.info(f"=== Break-Even Analysis for {strategy_name} ===")
logger.info(f"Initial Investment: ${initial_investment:,.2f}")
for month_data in monthly_data: for month_data in monthly_data:
total_value = value_extractor(month_data) total_value = value_extractor(month_data)
profit = total_value - initial_investment total_return_pct = ((total_value - initial_investment) / initial_investment) * 100
max_total_return = max(max_total_return, total_return_pct)
if profit > 0 and break_even_month is None: logger.info(f"Month {month_data.month}: Total Value = ${total_value:,.2f}, Return = {total_return_pct:+.2f}%")
# Break-even when total value exceeds initial investment by a meaningful margin (>0.1%)
# This accounts for rounding errors and ensures true profitability
if total_return_pct > 0.1 and break_even_month is None:
break_even_month = month_data.month break_even_month = month_data.month
profit_at_break_even = profit profit_at_break_even = total_value - initial_investment
logger.info(f"Break-even achieved in month {break_even_month} with {total_return_pct:.2f}% return")
break break
# Additional validation: if max return is still negative, definitely no break-even
if max_total_return < 0:
logger.info(f"Maximum return achieved: {max_total_return:.2f}% - Never breaks even")
break_even_month = None
# Format break-even time # Format break-even time
if break_even_month is None: if break_even_month is None:
months_to_break_even = "Never (within simulation period)" months_to_break_even = "Never (within simulation period)"
logger.info(f"Result: No break-even within {len(monthly_data)} months")
else: else:
years = break_even_month // 12 years = break_even_month // 12
months = break_even_month % 12 months = break_even_month % 12
@ -645,13 +684,17 @@ class DRIPService:
months_to_break_even = f"{years} year(s) and {months} month(s)" months_to_break_even = f"{years} year(s) and {months} month(s)"
else: else:
months_to_break_even = f"{months} month(s)" months_to_break_even = f"{months} month(s)"
logger.info(f"Result: Break-even in {months_to_break_even}")
logger.info(f"=== End Break-Even Analysis for {strategy_name} ===")
return { return {
'strategy_name': strategy_name, 'strategy_name': strategy_name,
'break_even_month': break_even_month, 'break_even_month': break_even_month,
'profit_at_break_even': profit_at_break_even, 'profit_at_break_even': profit_at_break_even,
'months_to_break_even': months_to_break_even, 'months_to_break_even': months_to_break_even,
'initial_investment': initial_investment 'initial_investment': initial_investment,
'max_return_pct': max_total_return
} }
def _print_strategy_comparison( def _print_strategy_comparison(