introduce a bug for nav & yield erosion
This commit is contained in:
parent
30dc087ce3
commit
0712578445
@ -1,4 +1,5 @@
|
|||||||
from typing import Dict, List, Optional, Tuple, Any
|
from __future__ import annotations
|
||||||
|
from typing import Dict, Optional, Tuple, Any
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
@ -113,10 +114,9 @@ class NoDRIPService:
|
|||||||
erosion_config = self._parse_erosion_config(config)
|
erosion_config = self._parse_erosion_config(config)
|
||||||
|
|
||||||
# If erosion is requested but no proper erosion_level is provided, calculate it
|
# If erosion is requested but no proper erosion_level is provided, calculate it
|
||||||
if (config.erosion_type != "None" and
|
if (erosion_config.erosion_type != "None" and
|
||||||
(not hasattr(config, 'erosion_level') or
|
(not isinstance(erosion_config.erosion_level, dict) or
|
||||||
not isinstance(config.erosion_level, dict) or
|
"per_ticker" not in erosion_config.erosion_level)):
|
||||||
"per_ticker" not in config.erosion_level)):
|
|
||||||
|
|
||||||
logger.info(f"Calculating erosion rates for No-DRIP with erosion type: {config.erosion_type}")
|
logger.info(f"Calculating erosion rates for No-DRIP with erosion type: {config.erosion_type}")
|
||||||
tickers = list(ticker_data.keys())
|
tickers = list(ticker_data.keys())
|
||||||
@ -238,9 +238,13 @@ class NoDRIPService:
|
|||||||
|
|
||||||
def _parse_erosion_config(self, config: DripConfig) -> ErosionConfig:
|
def _parse_erosion_config(self, config: DripConfig) -> ErosionConfig:
|
||||||
"""Parse and validate erosion configuration (reuse from DRIP service)"""
|
"""Parse and validate erosion configuration (reuse from DRIP service)"""
|
||||||
if not hasattr(config, 'erosion_level') or config.erosion_type == "None":
|
if config.erosion_type == "None":
|
||||||
return ErosionConfig(erosion_type="None", erosion_level={})
|
return ErosionConfig(erosion_type="None", erosion_level={})
|
||||||
|
|
||||||
|
if not hasattr(config, 'erosion_level') or config.erosion_level is None:
|
||||||
|
# erosion_level not provided - will be calculated automatically later
|
||||||
|
return ErosionConfig(erosion_type=config.erosion_type, erosion_level={})
|
||||||
|
|
||||||
# Check if erosion_level is already in the correct format
|
# Check if erosion_level is already in the correct format
|
||||||
if isinstance(config.erosion_level, dict) and "per_ticker" in config.erosion_level:
|
if isinstance(config.erosion_level, dict) and "per_ticker" in config.erosion_level:
|
||||||
return ErosionConfig(
|
return ErosionConfig(
|
||||||
@ -253,7 +257,7 @@ class NoDRIPService:
|
|||||||
erosion_level=config.erosion_level
|
erosion_level=config.erosion_level
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_erosion_from_analysis(self, tickers: List[str]) -> Dict:
|
def _calculate_erosion_from_analysis(self, tickers: list[str]) -> Dict:
|
||||||
"""Calculate erosion rates using NavErosionService (reuse from DRIP service)"""
|
"""Calculate erosion rates using NavErosionService (reuse from DRIP service)"""
|
||||||
try:
|
try:
|
||||||
# Use NavErosionService to analyze the tickers
|
# Use NavErosionService to analyze the tickers
|
||||||
@ -269,10 +273,11 @@ class NoDRIPService:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error calculating erosion rates for No-DRIP: {str(e)}")
|
logger.error(f"Error calculating erosion rates for No-DRIP: {str(e)}")
|
||||||
logger.warning("Falling back to no erosion")
|
logger.warning("Falling back to default erosion rates")
|
||||||
return {"per_ticker": {ticker: {"nav": 0.0, "yield": 0.0} for ticker in tickers}}
|
# Use reasonable default erosion rates instead of zero
|
||||||
|
return {"per_ticker": {ticker: {"nav": 6.0, "yield": 4.0} for ticker in tickers}}
|
||||||
|
|
||||||
def _create_distribution_schedule(self, ticker_data: Dict[str, TickerData], total_months: int) -> Dict[str, List[int]]:
|
def _create_distribution_schedule(self, ticker_data: dict[str, TickerData], total_months: int) -> dict[str, list[int]]:
|
||||||
"""Pre-calculate which months each ticker pays distributions (reuse from DRIP service)"""
|
"""Pre-calculate which months each ticker pays distributions (reuse from DRIP service)"""
|
||||||
schedule = {}
|
schedule = {}
|
||||||
|
|
||||||
@ -288,7 +293,7 @@ class NoDRIPService:
|
|||||||
|
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
def _initialize_simulation_state(self, ticker_data: Dict[str, TickerData]) -> Dict[str, Any]:
|
def _initialize_simulation_state(self, ticker_data: dict[str, TickerData]) -> dict[str, Any]:
|
||||||
"""Initialize simulation state variables"""
|
"""Initialize simulation state variables"""
|
||||||
return {
|
return {
|
||||||
'original_shares': {ticker: data.shares for ticker, data in ticker_data.items()}, # Constant
|
'original_shares': {ticker: data.shares for ticker, data in ticker_data.items()}, # Constant
|
||||||
@ -300,9 +305,9 @@ class NoDRIPService:
|
|||||||
def _calculate_monthly_distributions(
|
def _calculate_monthly_distributions(
|
||||||
self,
|
self,
|
||||||
month: int,
|
month: int,
|
||||||
state: Dict[str, Any],
|
state: dict[str, Any],
|
||||||
ticker_data: Dict[str, TickerData],
|
ticker_data: dict[str, TickerData],
|
||||||
distribution_schedule: Dict[str, List[int]]
|
distribution_schedule: dict[str, list[int]]
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Calculate distributions for the current month (reuse logic from DRIP service)"""
|
"""Calculate distributions for the current month (reuse logic from DRIP service)"""
|
||||||
monthly_income = 0.0
|
monthly_income = 0.0
|
||||||
@ -329,9 +334,9 @@ class NoDRIPService:
|
|||||||
|
|
||||||
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"""
|
"""Apply monthly erosion to prices and yields"""
|
||||||
if erosion_config.erosion_type == "None":
|
if erosion_config.erosion_type == "None":
|
||||||
@ -411,7 +416,7 @@ class NoDRIPService:
|
|||||||
else:
|
else:
|
||||||
return True # Default to monthly for unknown
|
return True # Default to monthly for unknown
|
||||||
|
|
||||||
def _create_no_drip_result(self, monthly_data: List[NoDRIPMonthlyData], state: Dict[str, Any]) -> NoDRIPResult:
|
def _create_no_drip_result(self, monthly_data: list[NoDRIPMonthlyData], state: dict[str, Any]) -> NoDRIPResult:
|
||||||
"""Create final No-DRIP result object"""
|
"""Create final No-DRIP result object"""
|
||||||
if not monthly_data:
|
if not monthly_data:
|
||||||
raise ValueError("No monthly data generated")
|
raise ValueError("No monthly data generated")
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from typing import Dict, List, Optional, Tuple, Any
|
from __future__ import annotations
|
||||||
|
from typing import Dict, Optional, Tuple, Any
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
@ -92,10 +93,9 @@ class DRIPService:
|
|||||||
erosion_config = self._parse_erosion_config(config)
|
erosion_config = self._parse_erosion_config(config)
|
||||||
|
|
||||||
# If erosion is requested but no proper erosion_level is provided, calculate it
|
# If erosion is requested but no proper erosion_level is provided, calculate it
|
||||||
if (config.erosion_type != "None" and
|
if (erosion_config.erosion_type != "None" and
|
||||||
(not hasattr(config, 'erosion_level') or
|
(not isinstance(erosion_config.erosion_level, dict) or
|
||||||
not isinstance(config.erosion_level, dict) or
|
"per_ticker" not in erosion_config.erosion_level)):
|
||||||
"per_ticker" not in config.erosion_level)):
|
|
||||||
|
|
||||||
logger.info(f"Calculating erosion rates for erosion type: {config.erosion_type}")
|
logger.info(f"Calculating erosion rates for erosion type: {config.erosion_type}")
|
||||||
tickers = list(ticker_data.keys())
|
tickers = list(ticker_data.keys())
|
||||||
@ -218,9 +218,13 @@ class DRIPService:
|
|||||||
|
|
||||||
def _parse_erosion_config(self, config: DripConfig) -> ErosionConfig:
|
def _parse_erosion_config(self, config: DripConfig) -> ErosionConfig:
|
||||||
"""Parse and validate erosion configuration"""
|
"""Parse and validate erosion configuration"""
|
||||||
if not hasattr(config, 'erosion_level') or config.erosion_type == "None":
|
if config.erosion_type == "None":
|
||||||
return ErosionConfig(erosion_type="None", erosion_level={})
|
return ErosionConfig(erosion_type="None", erosion_level={})
|
||||||
|
|
||||||
|
if not hasattr(config, 'erosion_level') or config.erosion_level is None:
|
||||||
|
# erosion_level not provided - will be calculated automatically later
|
||||||
|
return ErosionConfig(erosion_type=config.erosion_type, erosion_level={})
|
||||||
|
|
||||||
# Check if erosion_level is already in the correct format
|
# Check if erosion_level is already in the correct format
|
||||||
if isinstance(config.erosion_level, dict) and "per_ticker" in config.erosion_level:
|
if isinstance(config.erosion_level, dict) and "per_ticker" in config.erosion_level:
|
||||||
return ErosionConfig(
|
return ErosionConfig(
|
||||||
@ -235,7 +239,7 @@ class DRIPService:
|
|||||||
erosion_level=config.erosion_level
|
erosion_level=config.erosion_level
|
||||||
)
|
)
|
||||||
|
|
||||||
def calculate_erosion_from_analysis(self, tickers: List[str]) -> Dict:
|
def calculate_erosion_from_analysis(self, tickers: list[str]) -> Dict:
|
||||||
"""
|
"""
|
||||||
Calculate erosion rates using NavErosionService
|
Calculate erosion rates using NavErosionService
|
||||||
|
|
||||||
@ -259,15 +263,16 @@ class DRIPService:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error calculating erosion rates: {str(e)}")
|
logger.error(f"Error calculating erosion rates: {str(e)}")
|
||||||
logger.warning("Falling back to no erosion")
|
logger.warning("Falling back to default erosion rates")
|
||||||
return {"per_ticker": {ticker: {"nav": 0.0, "yield": 0.0} for ticker in tickers}}
|
# Use reasonable default erosion rates instead of zero
|
||||||
|
return {"per_ticker": {ticker: {"nav": 6.0, "yield": 4.0} for ticker in tickers}}
|
||||||
|
|
||||||
def _normalize_erosion_rate(self, erosion_level: float) -> float:
|
def _normalize_erosion_rate(self, erosion_level: float) -> float:
|
||||||
"""Convert erosion level (0-9) to monthly rate with validation"""
|
"""Convert erosion level (0-9) to monthly rate with validation"""
|
||||||
rate = (erosion_level / self.MAX_EROSION_LEVEL) * self.MAX_MONTHLY_EROSION
|
rate = (erosion_level / self.MAX_EROSION_LEVEL) * self.MAX_MONTHLY_EROSION
|
||||||
return min(max(0.0, rate), self.MAX_MONTHLY_EROSION)
|
return min(max(0.0, rate), self.MAX_MONTHLY_EROSION)
|
||||||
|
|
||||||
def _create_distribution_schedule(self, ticker_data: Dict[str, TickerData], total_months: int) -> Dict[str, List[int]]:
|
def _create_distribution_schedule(self, ticker_data: dict[str, TickerData], total_months: int) -> dict[str, list[int]]:
|
||||||
"""Pre-calculate which months each ticker pays distributions"""
|
"""Pre-calculate which months each ticker pays distributions"""
|
||||||
schedule = {}
|
schedule = {}
|
||||||
|
|
||||||
@ -283,7 +288,7 @@ class DRIPService:
|
|||||||
|
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
def _initialize_simulation_state(self, ticker_data: Dict[str, TickerData]) -> Dict[str, Any]:
|
def _initialize_simulation_state(self, ticker_data: dict[str, TickerData]) -> dict[str, Any]:
|
||||||
"""Initialize simulation state variables"""
|
"""Initialize simulation state variables"""
|
||||||
return {
|
return {
|
||||||
'current_shares': {ticker: data.shares for ticker, data in ticker_data.items()},
|
'current_shares': {ticker: data.shares for ticker, data in ticker_data.items()},
|
||||||
@ -295,9 +300,9 @@ class DRIPService:
|
|||||||
def _calculate_monthly_distributions(
|
def _calculate_monthly_distributions(
|
||||||
self,
|
self,
|
||||||
month: int,
|
month: int,
|
||||||
state: Dict[str, Any],
|
state: dict[str, Any],
|
||||||
ticker_data: Dict[str, TickerData],
|
ticker_data: dict[str, TickerData],
|
||||||
distribution_schedule: Dict[str, List[int]]
|
distribution_schedule: dict[str, list[int]]
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Calculate distributions for the current month"""
|
"""Calculate distributions for the current month"""
|
||||||
monthly_income = 0.0
|
monthly_income = 0.0
|
||||||
@ -324,9 +329,9 @@ class DRIPService:
|
|||||||
|
|
||||||
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"""
|
"""Apply monthly erosion to prices and yields"""
|
||||||
if erosion_config.erosion_type == "None":
|
if erosion_config.erosion_type == "None":
|
||||||
@ -396,8 +401,8 @@ class DRIPService:
|
|||||||
def _reinvest_dividends(
|
def _reinvest_dividends(
|
||||||
self,
|
self,
|
||||||
month: int,
|
month: int,
|
||||||
state: Dict[str, Any],
|
state: dict[str, Any],
|
||||||
distribution_schedule: Dict[str, List[int]]
|
distribution_schedule: dict[str, list[int]]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Reinvest dividends for tickers that distributed in this month"""
|
"""Reinvest dividends for tickers that distributed in this month"""
|
||||||
|
|
||||||
@ -434,7 +439,7 @@ class DRIPService:
|
|||||||
else:
|
else:
|
||||||
return True # Default to monthly for unknown
|
return True # Default to monthly for unknown
|
||||||
|
|
||||||
def _create_drip_result(self, monthly_data: List[MonthlyData], state: Dict[str, Any]) -> DripResult:
|
def _create_drip_result(self, monthly_data: list[MonthlyData], state: dict[str, Any]) -> DripResult:
|
||||||
"""Create final DRIP result object"""
|
"""Create final DRIP result object"""
|
||||||
if not monthly_data:
|
if not monthly_data:
|
||||||
raise ValueError("No monthly data generated")
|
raise ValueError("No monthly data generated")
|
||||||
@ -455,7 +460,7 @@ class DRIPService:
|
|||||||
self,
|
self,
|
||||||
portfolio_df: pd.DataFrame,
|
portfolio_df: pd.DataFrame,
|
||||||
config: DripConfig,
|
config: DripConfig,
|
||||||
tickers: Optional[List[str]] = None
|
tickers: Optional[list[str]] = None
|
||||||
) -> DRIPPortfolioResult:
|
) -> DRIPPortfolioResult:
|
||||||
"""
|
"""
|
||||||
Forecast DRIP growth for an entire portfolio.
|
Forecast DRIP growth for an entire portfolio.
|
||||||
@ -548,7 +553,7 @@ class DRIPService:
|
|||||||
self,
|
self,
|
||||||
portfolio_df: pd.DataFrame,
|
portfolio_df: pd.DataFrame,
|
||||||
config: DripConfig
|
config: DripConfig
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate and compare DRIP vs No-DRIP strategies with detailed analysis.
|
Calculate and compare DRIP vs No-DRIP strategies with detailed analysis.
|
||||||
This method runs both simulations and displays comparison tables.
|
This method runs both simulations and displays comparison tables.
|
||||||
@ -636,10 +641,10 @@ class DRIPService:
|
|||||||
def _calculate_break_even_analysis(
|
def _calculate_break_even_analysis(
|
||||||
self,
|
self,
|
||||||
strategy_name: str,
|
strategy_name: str,
|
||||||
monthly_data: List,
|
monthly_data: list,
|
||||||
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,
|
Break-even occurs when total value exceeds initial investment,
|
||||||
@ -705,8 +710,8 @@ class DRIPService:
|
|||||||
winner: str,
|
winner: str,
|
||||||
advantage_amount: float,
|
advantage_amount: float,
|
||||||
advantage_percentage: float,
|
advantage_percentage: float,
|
||||||
drip_break_even: Dict[str, Any],
|
drip_break_even: dict[str, Any],
|
||||||
no_drip_break_even: Dict[str, Any]
|
no_drip_break_even: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Print detailed strategy comparison table"""
|
"""Print detailed strategy comparison table"""
|
||||||
|
|
||||||
|
|||||||
@ -2103,7 +2103,7 @@ if st.session_state.simulation_run and st.session_state.df_data is not None:
|
|||||||
config = DripConfig(
|
config = DripConfig(
|
||||||
months=12,
|
months=12,
|
||||||
erosion_type=st.session_state.get("erosion_type", "Conservative"),
|
erosion_type=st.session_state.get("erosion_type", "Conservative"),
|
||||||
erosion_level={} # Let the service calculate this automatically
|
erosion_level=None # Let the service calculate this automatically
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate DRIP vs No-DRIP comparison using the integrated method
|
# Calculate DRIP vs No-DRIP comparison using the integrated method
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user