Fixing minor computing issues
This commit is contained in:
parent
19f713673e
commit
30dc087ce3
@ -328,12 +328,12 @@ class NoDRIPService:
|
||||
return monthly_income
|
||||
|
||||
def _apply_monthly_erosion(
|
||||
self,
|
||||
state: Dict[str, Any],
|
||||
erosion_config: ErosionConfig,
|
||||
self,
|
||||
state: Dict[str, Any],
|
||||
erosion_config: ErosionConfig,
|
||||
tickers: List[str]
|
||||
) -> 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":
|
||||
return
|
||||
|
||||
@ -355,29 +355,48 @@ class NoDRIPService:
|
||||
logger.warning(f"No erosion rates found for ticker {ticker}, skipping erosion")
|
||||
continue
|
||||
|
||||
nav_rate = ticker_rates.get("nav", 0.0) # Monthly rate in decimal form
|
||||
yield_rate = ticker_rates.get("yield", 0.0) # Monthly rate in decimal form
|
||||
# Get annual erosion rates (assumed to be in percentage form, e.g., 6.7 for 6.7%)
|
||||
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)
|
||||
nav_rate = max(0.0, min(nav_rate, self.MAX_MONTHLY_EROSION))
|
||||
yield_rate = max(0.0, min(yield_rate, self.MAX_MONTHLY_EROSION))
|
||||
nav_monthly_rate = max(0.0, min(nav_monthly_rate, self.MAX_MONTHLY_EROSION))
|
||||
yield_monthly_rate = max(0.0, min(yield_monthly_rate, self.MAX_MONTHLY_EROSION))
|
||||
|
||||
# Store original values for logging
|
||||
original_price = state['current_prices'][ticker]
|
||||
original_yield = state['current_yields'][ticker]
|
||||
|
||||
# Apply erosion directly (rates are already monthly)
|
||||
state['current_prices'][ticker] *= (1 - nav_rate)
|
||||
state['current_yields'][ticker] *= (1 - yield_rate)
|
||||
# Apply monthly erosion
|
||||
state['current_prices'][ticker] *= (1 - nav_monthly_rate)
|
||||
state['current_yields'][ticker] *= (1 - yield_monthly_rate)
|
||||
|
||||
# Ensure prices and yields don't go below reasonable minimums
|
||||
state['current_prices'][ticker] = max(state['current_prices'][ticker], 0.01)
|
||||
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" NAV: {nav_rate:.4%} -> Price: ${original_price:.2f} -> ${state['current_prices'][ticker]:.2f}")
|
||||
logger.info(f" Yield: {yield_rate:.4%} -> Yield: {original_yield:.2%} -> {state['current_yields'][ticker]:.2%}")
|
||||
logger.info(f" NAV: {nav_annual_pct:.1f}% annual -> {nav_monthly_rate:.4%} monthly")
|
||||
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:
|
||||
"""Check if current month is a distribution month (reuse from DRIP service)"""
|
||||
|
||||
@ -350,29 +350,48 @@ class DRIPService:
|
||||
logger.warning(f"No erosion rates found for ticker {ticker}, skipping erosion")
|
||||
continue
|
||||
|
||||
nav_rate = ticker_rates.get("nav", 0.0) # Monthly rate in decimal form
|
||||
yield_rate = ticker_rates.get("yield", 0.0) # Monthly rate in decimal form
|
||||
# Get annual erosion rates (assumed to be in percentage form, e.g., 6.7 for 6.7%)
|
||||
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)
|
||||
nav_rate = max(0.0, min(nav_rate, self.MAX_MONTHLY_EROSION))
|
||||
yield_rate = max(0.0, min(yield_rate, self.MAX_MONTHLY_EROSION))
|
||||
nav_monthly_rate = max(0.0, min(nav_monthly_rate, self.MAX_MONTHLY_EROSION))
|
||||
yield_monthly_rate = max(0.0, min(yield_monthly_rate, self.MAX_MONTHLY_EROSION))
|
||||
|
||||
# Store original values for logging
|
||||
original_price = state['current_prices'][ticker]
|
||||
original_yield = state['current_yields'][ticker]
|
||||
|
||||
# Apply erosion directly (rates are already monthly)
|
||||
state['current_prices'][ticker] *= (1 - nav_rate)
|
||||
state['current_yields'][ticker] *= (1 - yield_rate)
|
||||
# Apply monthly erosion
|
||||
state['current_prices'][ticker] *= (1 - nav_monthly_rate)
|
||||
state['current_yields'][ticker] *= (1 - yield_monthly_rate)
|
||||
|
||||
# Ensure prices and yields don't go below reasonable minimums
|
||||
state['current_prices'][ticker] = max(state['current_prices'][ticker], 0.01)
|
||||
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" NAV: {nav_rate:.4%} -> Price: ${original_price:.2f} -> ${state['current_prices'][ticker]:.2f}")
|
||||
logger.info(f" Yield: {yield_rate:.4%} -> Yield: {original_yield:.2%} -> {state['current_yields'][ticker]:.2%}")
|
||||
logger.info(f" NAV: {nav_annual_pct:.1f}% annual -> {nav_monthly_rate:.4%} monthly")
|
||||
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(
|
||||
self,
|
||||
@ -621,23 +640,43 @@ class DRIPService:
|
||||
initial_investment: float,
|
||||
value_extractor: callable
|
||||
) -> 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
|
||||
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:
|
||||
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
|
||||
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
|
||||
|
||||
# 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
|
||||
if break_even_month is None:
|
||||
months_to_break_even = "Never (within simulation period)"
|
||||
logger.info(f"Result: No break-even within {len(monthly_data)} months")
|
||||
else:
|
||||
years = 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)"
|
||||
else:
|
||||
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 {
|
||||
'strategy_name': strategy_name,
|
||||
'break_even_month': break_even_month,
|
||||
'profit_at_break_even': profit_at_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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user