Fixing minor computing issues
This commit is contained in:
parent
19f713673e
commit
30dc087ce3
@ -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)"""
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user