From 30e1bbcbd9f9e5367a263db8ecfc010ad58c60ca Mon Sep 17 00:00:00 2001 From: Pascal Date: Tue, 3 Jun 2025 18:42:25 +0200 Subject: [PATCH] fixing Nav_erosion_service and drip_service services communication --- ETF_Portal/services/drip_service/service.py | 121 +++++++++++-- .../services/nav_erosion_service/service.py | 58 ++++++- services/drip_service/logger.py | 23 +++ services/drip_service/service.py | 161 ++++++++++++++---- services/nav_erosion_service/service.py | 121 ++++++++++++- test_erosion.py | 45 ----- 6 files changed, 436 insertions(+), 93 deletions(-) create mode 100644 services/drip_service/logger.py delete mode 100644 test_erosion.py diff --git a/ETF_Portal/services/drip_service/service.py b/ETF_Portal/services/drip_service/service.py index efc01bd..543ac63 100644 --- a/ETF_Portal/services/drip_service/service.py +++ b/ETF_Portal/services/drip_service/service.py @@ -67,6 +67,7 @@ class DRIPService: self.MAX_EROSION_LEVEL = 9 self.MAX_MONTHLY_EROSION = 0.05 # 5% monthly max erosion self.DISTRIBUTION_FREQUENCIES = {freq.display_name: freq for freq in DistributionFrequency} + self.nav_erosion_service = NavErosionService() def calculate_drip_growth(self, portfolio_df: pd.DataFrame, config: DripConfig) -> DripResult: """ @@ -85,8 +86,24 @@ class DRIPService: # Initialize portfolio data ticker_data = self._initialize_ticker_data(portfolio_df) + + # Handle erosion configuration erosion_config = self._parse_erosion_config(config) + # If erosion is requested but no proper erosion_level is provided, calculate it + if (config.erosion_type != "None" and + (not hasattr(config, 'erosion_level') or + not isinstance(config.erosion_level, dict) or + "per_ticker" not in config.erosion_level)): + + logger.info(f"Calculating erosion rates for erosion type: {config.erosion_type}") + tickers = list(ticker_data.keys()) + calculated_erosion = self.calculate_erosion_from_analysis(tickers) + erosion_config = ErosionConfig( + erosion_type=config.erosion_type, + erosion_level=calculated_erosion + ) + # Pre-calculate distribution schedule for performance distribution_schedule = self._create_distribution_schedule(ticker_data, config.months) @@ -203,10 +220,46 @@ class DRIPService: if not hasattr(config, 'erosion_level') or config.erosion_type == "None": return ErosionConfig(erosion_type="None", erosion_level={}) + # Check if erosion_level is already in the correct format + if isinstance(config.erosion_level, dict) and "per_ticker" in config.erosion_level: + return ErosionConfig( + erosion_type=config.erosion_type, + erosion_level=config.erosion_level + ) + + # If erosion_level is not in the correct format, it might be a NavErosionAnalysis + # or we need to calculate it from scratch return ErosionConfig( erosion_type=config.erosion_type, erosion_level=config.erosion_level ) + + def calculate_erosion_from_analysis(self, tickers: List[str]) -> Dict: + """ + Calculate erosion rates using NavErosionService + + Args: + tickers: List of ticker symbols to analyze + + Returns: + Dict in format expected by _apply_monthly_erosion + """ + try: + # Use NavErosionService to analyze the tickers + analysis = self.nav_erosion_service.analyze_etf_erosion_risk(tickers) + + # Convert to format expected by DRIP service + erosion_config = self.nav_erosion_service.convert_to_drip_erosion_config(analysis) + + logger.info(f"Calculated erosion rates for tickers: {tickers}") + logger.info(f"Erosion configuration: {erosion_config}") + + return erosion_config + + except Exception as e: + logger.error(f"Error calculating erosion rates: {str(e)}") + logger.warning("Falling back to no erosion") + return {"per_ticker": {ticker: {"nav": 0.0, "yield": 0.0} for ticker in tickers}} def _normalize_erosion_rate(self, erosion_level: float) -> float: """Convert erosion level (0-9) to monthly rate with validation""" @@ -269,29 +322,56 @@ class DRIPService: 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""" if erosion_config.erosion_type == "None": return + + # Validate erosion configuration structure + if not isinstance(erosion_config.erosion_level, dict): + logger.warning(f"Invalid erosion_level format: {type(erosion_config.erosion_level)}") + return + + per_ticker_data = erosion_config.erosion_level.get("per_ticker", {}) + if not per_ticker_data: + logger.warning("No per_ticker erosion data found in erosion_level") + return 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 + # Get per-ticker erosion rates with fallback + ticker_rates = per_ticker_data.get(ticker, {}) - # Apply erosion directly + if not ticker_rates: + 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 + + # 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)) + + # 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) + # 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 - 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%}") + 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%}") def _reinvest_dividends( self, @@ -378,7 +458,24 @@ class DRIPService: def _calculate_no_drip_scenario(self, portfolio_df: pd.DataFrame, config: DripConfig) -> Dict[str, float]: """Calculate scenario where dividends are not reinvested""" ticker_data = self._initialize_ticker_data(portfolio_df) + + # Handle erosion configuration same way as main calculation erosion_config = self._parse_erosion_config(config) + + # If erosion is requested but no proper erosion_level is provided, calculate it + if (config.erosion_type != "None" and + (not hasattr(config, 'erosion_level') or + not isinstance(config.erosion_level, dict) or + "per_ticker" not in config.erosion_level)): + + logger.info(f"Calculating erosion rates for no-DRIP scenario with erosion type: {config.erosion_type}") + tickers = list(ticker_data.keys()) + calculated_erosion = self.calculate_erosion_from_analysis(tickers) + erosion_config = ErosionConfig( + erosion_type=config.erosion_type, + erosion_level=calculated_erosion + ) + state = self._initialize_simulation_state(ticker_data) total_dividends = 0.0 @@ -386,7 +483,7 @@ class DRIPService: for month in range(1, config.months + 1): # Calculate dividends but don't reinvest monthly_dividends = self._calculate_monthly_distributions( - month, state, ticker_data, + month, state, ticker_data, self._create_distribution_schedule(ticker_data, config.months) ) total_dividends += monthly_dividends diff --git a/ETF_Portal/services/nav_erosion_service/service.py b/ETF_Portal/services/nav_erosion_service/service.py index 5b1d9bc..d94155e 100644 --- a/ETF_Portal/services/nav_erosion_service/service.py +++ b/ETF_Portal/services/nav_erosion_service/service.py @@ -532,4 +532,60 @@ class NavErosionService: return ( f"Portfolio NAV Risk: {avg_nav_risk:.1f}/9 | " f"Portfolio Yield Risk: {avg_yield_risk:.1f}/9" - ) \ No newline at end of file + ) + + def convert_to_drip_erosion_config(self, analysis: NavErosionAnalysis) -> Dict: + """ + Convert NavErosionAnalysis results to format expected by DRIPService. + + Args: + analysis: NavErosionAnalysis object from analyze_etf_erosion_risk() + + Returns: + Dict in format expected by DRIPService: + { + "per_ticker": { + "TICKER": { + "nav": monthly_nav_erosion_rate, + "yield": monthly_yield_erosion_rate + } + } + } + """ + per_ticker_erosion = {} + + for result in analysis.results: + # Convert annual erosion rates to monthly rates + # Monthly rate = (1 + annual_rate)^(1/12) - 1 + # For small rates, approximately annual_rate / 12 + + annual_nav_erosion = result.estimated_nav_erosion + annual_yield_erosion = result.estimated_yield_erosion + + # Convert to monthly rates using compound formula for accuracy + if annual_nav_erosion > 0: + monthly_nav_rate = (1 + annual_nav_erosion) ** (1/12) - 1 + else: + monthly_nav_rate = 0.0 + + if annual_yield_erosion > 0: + monthly_yield_rate = (1 + annual_yield_erosion) ** (1/12) - 1 + else: + monthly_yield_rate = 0.0 + + # Cap maximum monthly erosion at 5% for safety + monthly_nav_rate = min(monthly_nav_rate, 0.05) + monthly_yield_rate = min(monthly_yield_rate, 0.05) + + per_ticker_erosion[result.ticker] = { + "nav": monthly_nav_rate, + "yield": monthly_yield_rate + } + + logger.info(f"Converted erosion rates for {result.ticker}:") + logger.info(f" Annual NAV erosion: {annual_nav_erosion:.2%} -> Monthly: {monthly_nav_rate:.4%}") + logger.info(f" Annual Yield erosion: {annual_yield_erosion:.2%} -> Monthly: {monthly_yield_rate:.4%}") + + return { + "per_ticker": per_ticker_erosion + } \ No newline at end of file diff --git a/services/drip_service/logger.py b/services/drip_service/logger.py new file mode 100644 index 0000000..900e36c --- /dev/null +++ b/services/drip_service/logger.py @@ -0,0 +1,23 @@ +import logging +import sys + +def setup_logger(): + # Create logger + logger = logging.getLogger('drip_service') + logger.setLevel(logging.DEBUG) + + # Create console handler with formatting + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.DEBUG) + + # Create formatter + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_handler.setFormatter(formatter) + + # Add handler to logger + logger.addHandler(console_handler) + + return logger + +# Create logger instance +logger = setup_logger() \ No newline at end of file diff --git a/services/drip_service/service.py b/services/drip_service/service.py index 19a2e79..3ef8082 100644 --- a/services/drip_service/service.py +++ b/services/drip_service/service.py @@ -1,15 +1,12 @@ from typing import Dict, List, Optional, Tuple, Any import pandas as pd import numpy as np -import logging import traceback from dataclasses import dataclass, field from enum import Enum from .models import PortfolioAllocation, MonthlyData, DripConfig, DripResult from ..nav_erosion_service import NavErosionService - -# Configure logging -logger = logging.getLogger(__name__) +from .logger import logger class DistributionFrequency(Enum): """Enum for distribution frequencies""" @@ -69,13 +66,42 @@ class DripService: # Get erosion data from nav_erosion_service erosion_data = self.nav_erosion_service.analyze_etf_erosion_risk(portfolio_df["Ticker"].tolist()) - erosion_rates = { - result.ticker: { - "nav": result.estimated_nav_erosion / 100, # Convert to decimal - "yield": result.estimated_yield_erosion / 100 # Convert to decimal - } - for result in erosion_data.results - } + logger.info(f"Erosion data results: {erosion_data.results}") + + # Initialize erosion rates dictionary + erosion_rates = {} + + # Use erosion rates from nav_erosion_service + for ticker in portfolio_df["Ticker"]: + # Find the result for this ticker in erosion_data.results + result = next((r for r in erosion_data.results if r.ticker == ticker), None) + + if result: + erosion_rates[ticker] = { + "nav": result.monthly_nav_erosion_rate, + "yield": result.monthly_yield_erosion_rate + } + logger.info(f"=== EROSION RATE DEBUG ===") + logger.info(f"Ticker: {ticker}") + logger.info(f"Erosion rates from nav_erosion_service:") + logger.info(f" NAV: {erosion_rates[ticker]['nav']:.4%}") + logger.info(f" Yield: {erosion_rates[ticker]['yield']:.4%}") + logger.info(f"=== END EROSION RATE DEBUG ===\n") + else: + # Use default erosion rates if not found + erosion_rates[ticker] = { + "nav": 0.05, # 5% per month (very high, for test) + "yield": 0.07 # 7% per month (very high, for test) + } + logger.info(f"=== EROSION RATE DEBUG ===") + logger.info(f"Ticker: {ticker}") + logger.info(f"Using default erosion rates:") + logger.info(f" NAV: {erosion_rates[ticker]['nav']:.4%}") + logger.info(f" Yield: {erosion_rates[ticker]['yield']:.4%}") + logger.info(f"=== END EROSION RATE DEBUG ===\n") + + # Log the final erosion rates dictionary + logger.info(f"Final erosion rates dictionary: {erosion_rates}") # Initialize portfolio data ticker_data = self._initialize_ticker_data(portfolio_df) @@ -89,6 +115,14 @@ class DripService: # Run monthly simulation for month in range(1, config.months + 1): + logger.info(f"\n=== Starting Month {month} ===") + logger.info(f"Initial state for month {month}:") + for ticker in ticker_data.keys(): + logger.info(f" {ticker}:") + logger.info(f" Price: ${simulation_state['current_prices'][ticker]:.2f}") + logger.info(f" Yield: {simulation_state['current_yields'][ticker]:.2%}") + logger.info(f" Shares: {simulation_state['current_shares'][ticker]:.4f}") + month_result = self._simulate_month( month, simulation_state, @@ -97,6 +131,14 @@ class DripService: distribution_schedule ) monthly_data.append(month_result) + + logger.info(f"Final state for month {month}:") + for ticker in ticker_data.keys(): + logger.info(f" {ticker}:") + logger.info(f" Price: ${simulation_state['current_prices'][ticker]:.2f}") + logger.info(f" Yield: {simulation_state['current_yields'][ticker]:.2%}") + logger.info(f" Shares: {simulation_state['current_shares'][ticker]:.4f}") + logger.info(f"=== End Month {month} ===\n") # Calculate final results return self._create_drip_result(monthly_data, simulation_state) @@ -177,7 +219,63 @@ class DripService: ) -> MonthlyData: """Simulate a single month with improved accuracy""" - # Calculate monthly income from distributions + # Debug logging for erosion rates + logger.info(f"\n=== EROSION RATES DEBUG ===") + logger.info(f"Erosion rates dictionary: {erosion_rates}") + for ticker, rates in erosion_rates.items(): + logger.info(f" {ticker}:") + logger.info(f" nav: {rates['nav']:.4%}") + logger.info(f" yield: {rates['yield']:.4%}") + logger.info(f"=== END EROSION RATES DEBUG ===\n") + + # Apply erosion first + for ticker, rates in erosion_rates.items(): + if ticker in state['current_prices']: + # Get monthly erosion rates (already in decimal form) + monthly_nav_erosion = rates['nav'] + monthly_yield_erosion = rates['yield'] + + # Get current values + old_price = state['current_prices'][ticker] + old_yield = state['current_yields'][ticker] + + # Debug logging + logger.info(f"\n=== EROSION CALCULATION DEBUG ===") + logger.info(f"Ticker: {ticker}") + logger.info(f"Raw erosion rates from nav_erosion_service:") + logger.info(f" monthly_nav_erosion: {monthly_nav_erosion:.4%}") + logger.info(f" monthly_yield_erosion: {monthly_yield_erosion:.4%}") + logger.info(f"Current values:") + logger.info(f" old_price: ${old_price:.4f}") + logger.info(f" old_yield: {old_yield:.4%}") + + # Calculate new values + new_price = old_price * (1 - monthly_nav_erosion) + new_yield = old_yield * (1 - monthly_yield_erosion) + + logger.info(f"Calculated new values:") + logger.info(f" new_price = ${old_price:.4f} * (1 - {monthly_nav_erosion:.4%})") + logger.info(f" new_price = ${old_price:.4f} * {1 - monthly_nav_erosion:.4f}") + logger.info(f" new_price = ${new_price:.4f}") + logger.info(f" new_yield = {old_yield:.4%} * (1 - {monthly_yield_erosion:.4%})") + logger.info(f" new_yield = {old_yield:.4%} * {1 - monthly_yield_erosion:.4f}") + logger.info(f" new_yield = {new_yield:.4%}") + + # Apply the new values with bounds checking + state['current_prices'][ticker] = max(0.01, new_price) # Prevent zero/negative prices + state['current_yields'][ticker] = max(0.0, new_yield) # Prevent negative yields + + logger.info(f"Final values after bounds checking:") + logger.info(f" final_price: ${state['current_prices'][ticker]:.4f}") + logger.info(f" final_yield: {state['current_yields'][ticker]:.4%}") + logger.info(f"=== END EROSION CALCULATION DEBUG ===\n") + + # Log the actual erosion being applied + logger.info(f"Applied erosion to {ticker}:") + logger.info(f" NAV: {monthly_nav_erosion:.4%} -> New price: ${state['current_prices'][ticker]:.2f}") + logger.info(f" Yield: {monthly_yield_erosion:.4%} -> New yield: {state['current_yields'][ticker]:.2%}") + + # Calculate monthly income from distributions using eroded values monthly_income = self._calculate_monthly_distributions( month, state, ticker_data, distribution_schedule ) @@ -185,9 +283,6 @@ class DripService: # Update cumulative income state['cumulative_income'] += monthly_income - # Apply erosion to prices and yields using nav_erosion_service data - self._apply_monthly_erosion(state, erosion_rates) - # Reinvest dividends (DRIP) self._reinvest_dividends(month, state, distribution_schedule) @@ -232,22 +327,6 @@ class DripService: return monthly_income - def _apply_monthly_erosion( - self, - state: Dict[str, Any], - erosion_rates: Dict[str, Dict[str, float]] - ) -> None: - """Apply erosion to current prices and yields using nav_erosion_service data""" - for ticker, rates in erosion_rates.items(): - if ticker in state['current_prices']: - # Apply monthly erosion rates - monthly_nav_erosion = rates['nav'] / 12 - monthly_yield_erosion = rates['yield'] / 12 - - # Apply erosion with bounds checking - state['current_prices'][ticker] = max(0.01, state['current_prices'][ticker] * (1 - monthly_nav_erosion)) - state['current_yields'][ticker] = max(0.0, state['current_yields'][ticker] * (1 - monthly_yield_erosion)) - def _reinvest_dividends( self, month: int, @@ -331,8 +410,8 @@ class DripService: erosion_data = self.nav_erosion_service.analyze_etf_erosion_risk(portfolio_df["Ticker"].tolist()) erosion_rates = { result.ticker: { - "nav": result.estimated_nav_erosion / 100, - "yield": result.estimated_yield_erosion / 100 + "nav": result.monthly_nav_erosion_rate, + "yield": result.monthly_yield_erosion_rate } for result in erosion_data.results } @@ -349,7 +428,21 @@ class DripService: total_dividends += monthly_dividends # Apply erosion - self._apply_monthly_erosion(state, erosion_rates) + for ticker, rates in erosion_rates.items(): + if ticker in state['current_prices']: + # Get monthly erosion rates (already in decimal form) + monthly_nav_erosion = rates['nav'] + monthly_yield_erosion = rates['yield'] + + # Apply NAV erosion (decrease price) + old_price = state['current_prices'][ticker] + new_price = old_price * (1 - monthly_nav_erosion) + state['current_prices'][ticker] = max(0.01, new_price) # Prevent zero/negative prices + + # Apply yield erosion (decrease yield) + old_yield = state['current_yields'][ticker] + new_yield = old_yield * (1 - monthly_yield_erosion) + state['current_yields'][ticker] = max(0.0, new_yield) # Prevent negative yields final_value = sum( state['current_shares'][ticker] * state['current_prices'][ticker] diff --git a/services/nav_erosion_service/service.py b/services/nav_erosion_service/service.py index 9f9fb02..3383fbd 100644 --- a/services/nav_erosion_service/service.py +++ b/services/nav_erosion_service/service.py @@ -1,6 +1,125 @@ -from typing import Dict, Tuple +from typing import Dict, List, Tuple +from .models import NavErosionConfig, NavErosionResult, NavErosionAnalysis +from enum import Enum +from dataclasses import dataclass +import streamlit as st + +class ETFType(Enum): + INCOME = "Income" + GROWTH = "Growth" + BALANCED = "Balanced" + +@dataclass +class NavErosionResult: + """Result of NAV erosion analysis for a single ETF""" + ticker: str + nav_erosion_rate: float # Annual NAV erosion rate + yield_erosion_rate: float # Annual yield erosion rate + monthly_nav_erosion_rate: float # Monthly NAV erosion rate + monthly_yield_erosion_rate: float # Monthly yield erosion rate + risk_level: int + risk_explanation: str + max_drawdown: float + volatility: float + is_new_etf: bool + etf_age_years: float + +@dataclass +class NavErosionAnalysis: + """Complete NAV erosion analysis results""" + results: List[NavErosionResult] + portfolio_nav_risk: float = 0.0 + portfolio_erosion_rate: float = 0.0 + risk_summary: str = "" class NavErosionService: + def __init__(self): + self.NAV_COMPONENT_WEIGHTS = { + 'drawdown': 0.4, + 'volatility': 0.3, + 'sharpe': 0.15, + 'sortino': 0.15 + } + + # Default erosion rates based on risk level (0-9) + self.RISK_TO_EROSION = { + 0: 0.01, # 1% annual + 1: 0.02, # 2% annual + 2: 0.03, # 3% annual + 3: 0.04, # 4% annual + 4: 0.05, # 5% annual + 5: 0.06, # 6% annual + 6: 0.07, # 7% annual + 7: 0.08, # 8% annual + 8: 0.09, # 9% annual + 9: 0.10 # 10% annual + } + + def analyze_etf_erosion_risk(self, tickers: List[str]) -> NavErosionAnalysis: + """Analyze NAV erosion risk for a list of ETFs""" + results = [] + + print("\n=== NAV EROSION SERVICE DEBUG ===") + print(f"Session state keys: {st.session_state.keys()}") + print(f"Erosion level from session state: {st.session_state.get('erosion_level')}") + + for ticker in tickers: + # Get erosion rates from session state + erosion_level = st.session_state.get('erosion_level', {'nav': 5.0, 'yield': 5.0}) + annual_nav_erosion = erosion_level['nav'] / 100 # Convert from percentage to decimal + annual_yield_erosion = erosion_level['yield'] / 100 # Convert from percentage to decimal + + # Convert annual rates to monthly + monthly_nav_erosion = 1 - (1 - annual_nav_erosion) ** (1/12) + monthly_yield_erosion = 1 - (1 - annual_yield_erosion) ** (1/12) + + print(f"\n=== NAV EROSION SERVICE DEBUG ===") + print(f"Ticker: {ticker}") + print(f"Session State Values:") + print(f" Annual NAV Erosion: {annual_nav_erosion:.4%}") + print(f" Annual Yield Erosion: {annual_yield_erosion:.4%}") + print(f" Monthly NAV Erosion: {monthly_nav_erosion:.4%}") + print(f" Monthly Yield Erosion: {monthly_yield_erosion:.4%}") + print(f"=== END NAV EROSION SERVICE DEBUG ===\n") + + result = NavErosionResult( + ticker=ticker, + nav_erosion_rate=annual_nav_erosion, + yield_erosion_rate=annual_yield_erosion, + monthly_nav_erosion_rate=monthly_nav_erosion, + monthly_yield_erosion_rate=monthly_yield_erosion, + risk_level=5, # Arbitrary risk level + risk_explanation="Using erosion rates from session state", + max_drawdown=0.2, + volatility=0.25, + is_new_etf=False, + etf_age_years=1.0 + ) + results.append(result) + print(f"Created NavErosionResult for {ticker}:") + print(f" monthly_nav_erosion_rate: {result.monthly_nav_erosion_rate:.4%}") + print(f" monthly_yield_erosion_rate: {result.monthly_yield_erosion_rate:.4%}") + + # Calculate portfolio-level metrics + portfolio_nav_risk = sum(r.risk_level for r in results) / len(results) + portfolio_erosion_rate = sum(r.nav_erosion_rate for r in results) / len(results) + + analysis = NavErosionAnalysis( + results=results, + portfolio_nav_risk=portfolio_nav_risk, + portfolio_erosion_rate=portfolio_erosion_rate, + risk_summary="Portfolio has moderate NAV erosion risk" + ) + + print("\nFinal NavErosionAnalysis:") + for r in analysis.results: + print(f" {r.ticker}:") + print(f" monthly_nav_erosion_rate: {r.monthly_nav_erosion_rate:.4%}") + print(f" monthly_yield_erosion_rate: {r.monthly_yield_erosion_rate:.4%}") + print("=== END NAV EROSION SERVICE DEBUG ===\n") + + return analysis + def _calculate_nav_risk(self, etf_data: Dict, etf_type: ETFType) -> Tuple[float, Dict]: """Calculate NAV risk components with ETF-type specific adjustments""" components = {} diff --git a/test_erosion.py b/test_erosion.py deleted file mode 100644 index 173b282..0000000 --- a/test_erosion.py +++ /dev/null @@ -1,45 +0,0 @@ -from ETF_Portal.services.nav_erosion_service import NavErosionService -import logging - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -def test_portfolio(): - # Initialize service - service = NavErosionService() - - # Test portfolio - portfolio = ['VTI', 'DEPI', 'MSTY', 'JEPI', 'VOO'] - - try: - # Analyze portfolio - analysis = service.analyze_etf_erosion_risk(portfolio) - - # Print results - print("\nPortfolio Analysis Results:") - print("=" * 50) - print(f"Portfolio NAV Risk: {analysis.portfolio_nav_risk:.1f}/9") - print(f"Portfolio Yield Risk: {analysis.portfolio_yield_risk:.1f}/9") - print("\nDetailed Results:") - print("=" * 50) - - for result in analysis.results: - print(f"\n{result.ticker}:") - print(f" NAV Erosion Risk: {result.nav_erosion_risk:.1f}/9") - print(f" Yield Erosion Risk: {result.yield_erosion_risk:.1f}/9") - print(f" Estimated NAV Erosion: {result.estimated_nav_erosion:.1%}") - print(f" Estimated Yield Erosion: {result.estimated_yield_erosion:.1%}") - print(f" NAV Risk Explanation: {result.nav_risk_explanation}") - print(f" Yield Risk Explanation: {result.yield_risk_explanation}") - if result.component_risks: - print(" Component Risks:") - for component, value in result.component_risks.items(): - print(f" {component}: {value:.1%}") - - except Exception as e: - logger.error(f"Error during analysis: {str(e)}") - raise - -if __name__ == "__main__": - test_portfolio() \ No newline at end of file