fixing Nav_erosion_service and drip_service services communication

This commit is contained in:
Pascal BIBEHE 2025-06-03 18:42:25 +02:00
parent c6797c94ee
commit 30e1bbcbd9
6 changed files with 436 additions and 93 deletions

View File

@ -67,6 +67,7 @@ class DRIPService:
self.MAX_EROSION_LEVEL = 9 self.MAX_EROSION_LEVEL = 9
self.MAX_MONTHLY_EROSION = 0.05 # 5% monthly max erosion self.MAX_MONTHLY_EROSION = 0.05 # 5% monthly max erosion
self.DISTRIBUTION_FREQUENCIES = {freq.display_name: freq for freq in DistributionFrequency} 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: def calculate_drip_growth(self, portfolio_df: pd.DataFrame, config: DripConfig) -> DripResult:
""" """
@ -85,8 +86,24 @@ class DRIPService:
# Initialize portfolio data # Initialize portfolio data
ticker_data = self._initialize_ticker_data(portfolio_df) ticker_data = self._initialize_ticker_data(portfolio_df)
# Handle erosion configuration
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 (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 # Pre-calculate distribution schedule for performance
distribution_schedule = self._create_distribution_schedule(ticker_data, config.months) distribution_schedule = self._create_distribution_schedule(ticker_data, config.months)
@ -203,11 +220,47 @@ class DRIPService:
if not hasattr(config, 'erosion_level') or config.erosion_type == "None": if not hasattr(config, 'erosion_level') or config.erosion_type == "None":
return ErosionConfig(erosion_type="None", erosion_level={}) 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( return ErosionConfig(
erosion_type=config.erosion_type, erosion_type=config.erosion_type,
erosion_level=config.erosion_level 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: 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
@ -278,20 +331,47 @@ class DRIPService:
if erosion_config.erosion_type == "None": if erosion_config.erosion_type == "None":
return return
for ticker in tickers: # Validate erosion configuration structure
# Get per-ticker erosion rates if not isinstance(erosion_config.erosion_level, dict):
ticker_rates = erosion_config.erosion_level.get("per_ticker", {}).get(ticker, {}) logger.warning(f"Invalid erosion_level format: {type(erosion_config.erosion_level)}")
nav_rate = ticker_rates.get("nav", 0.0) # Already in decimal form return
yield_rate = ticker_rates.get("yield", 0.0) # Already in decimal form
# Apply erosion directly 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 with fallback
ticker_rates = per_ticker_data.get(ticker, {})
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_prices'][ticker] *= (1 - nav_rate)
state['current_yields'][ticker] *= (1 - yield_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 # Log erosion application
logger.info(f"Applied erosion to {ticker}:") logger.info(f"Applied monthly erosion to {ticker}:")
logger.info(f" NAV: {nav_rate:.4%} -> New price: ${state['current_prices'][ticker]:.2f}") logger.info(f" NAV: {nav_rate:.4%} -> Price: ${original_price:.2f} -> ${state['current_prices'][ticker]:.2f}")
logger.info(f" Yield: {yield_rate:.4%} -> New yield: {state['current_yields'][ticker]:.2%}") logger.info(f" Yield: {yield_rate:.4%} -> Yield: {original_yield:.2%} -> {state['current_yields'][ticker]:.2%}")
def _reinvest_dividends( def _reinvest_dividends(
self, self,
@ -378,7 +458,24 @@ class DRIPService:
def _calculate_no_drip_scenario(self, portfolio_df: pd.DataFrame, config: DripConfig) -> Dict[str, float]: def _calculate_no_drip_scenario(self, portfolio_df: pd.DataFrame, config: DripConfig) -> Dict[str, float]:
"""Calculate scenario where dividends are not reinvested""" """Calculate scenario where dividends are not reinvested"""
ticker_data = self._initialize_ticker_data(portfolio_df) ticker_data = self._initialize_ticker_data(portfolio_df)
# Handle erosion configuration same way as main calculation
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 (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) state = self._initialize_simulation_state(ticker_data)
total_dividends = 0.0 total_dividends = 0.0

View File

@ -533,3 +533,59 @@ class NavErosionService:
f"Portfolio NAV Risk: {avg_nav_risk:.1f}/9 | " f"Portfolio NAV Risk: {avg_nav_risk:.1f}/9 | "
f"Portfolio Yield Risk: {avg_yield_risk:.1f}/9" f"Portfolio Yield Risk: {avg_yield_risk:.1f}/9"
) )
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
}

View File

@ -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()

View File

@ -1,15 +1,12 @@
from typing import Dict, List, Optional, Tuple, Any from typing import Dict, List, Optional, Tuple, Any
import pandas as pd import pandas as pd
import numpy as np import numpy as np
import logging
import traceback import traceback
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from .models import PortfolioAllocation, MonthlyData, DripConfig, DripResult from .models import PortfolioAllocation, MonthlyData, DripConfig, DripResult
from ..nav_erosion_service import NavErosionService from ..nav_erosion_service import NavErosionService
from .logger import logger
# Configure logging
logger = logging.getLogger(__name__)
class DistributionFrequency(Enum): class DistributionFrequency(Enum):
"""Enum for distribution frequencies""" """Enum for distribution frequencies"""
@ -69,13 +66,42 @@ class DripService:
# Get erosion data from nav_erosion_service # Get erosion data from nav_erosion_service
erosion_data = self.nav_erosion_service.analyze_etf_erosion_risk(portfolio_df["Ticker"].tolist()) erosion_data = self.nav_erosion_service.analyze_etf_erosion_risk(portfolio_df["Ticker"].tolist())
erosion_rates = { logger.info(f"Erosion data results: {erosion_data.results}")
result.ticker: {
"nav": result.estimated_nav_erosion / 100, # Convert to decimal # Initialize erosion rates dictionary
"yield": result.estimated_yield_erosion / 100 # Convert to decimal erosion_rates = {}
}
for result in erosion_data.results # 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 # Initialize portfolio data
ticker_data = self._initialize_ticker_data(portfolio_df) ticker_data = self._initialize_ticker_data(portfolio_df)
@ -89,6 +115,14 @@ class DripService:
# Run monthly simulation # Run monthly simulation
for month in range(1, config.months + 1): 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_result = self._simulate_month(
month, month,
simulation_state, simulation_state,
@ -98,6 +132,14 @@ class DripService:
) )
monthly_data.append(month_result) 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 # Calculate final results
return self._create_drip_result(monthly_data, simulation_state) return self._create_drip_result(monthly_data, simulation_state)
@ -177,7 +219,63 @@ class DripService:
) -> MonthlyData: ) -> MonthlyData:
"""Simulate a single month with improved accuracy""" """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( monthly_income = self._calculate_monthly_distributions(
month, state, ticker_data, distribution_schedule month, state, ticker_data, distribution_schedule
) )
@ -185,9 +283,6 @@ class DripService:
# Update cumulative income # Update cumulative income
state['cumulative_income'] += monthly_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) # Reinvest dividends (DRIP)
self._reinvest_dividends(month, state, distribution_schedule) self._reinvest_dividends(month, state, distribution_schedule)
@ -232,22 +327,6 @@ class DripService:
return monthly_income 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( def _reinvest_dividends(
self, self,
month: int, month: int,
@ -331,8 +410,8 @@ class DripService:
erosion_data = self.nav_erosion_service.analyze_etf_erosion_risk(portfolio_df["Ticker"].tolist()) erosion_data = self.nav_erosion_service.analyze_etf_erosion_risk(portfolio_df["Ticker"].tolist())
erosion_rates = { erosion_rates = {
result.ticker: { result.ticker: {
"nav": result.estimated_nav_erosion / 100, "nav": result.monthly_nav_erosion_rate,
"yield": result.estimated_yield_erosion / 100 "yield": result.monthly_yield_erosion_rate
} }
for result in erosion_data.results for result in erosion_data.results
} }
@ -349,7 +428,21 @@ class DripService:
total_dividends += monthly_dividends total_dividends += monthly_dividends
# Apply erosion # 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( final_value = sum(
state['current_shares'][ticker] * state['current_prices'][ticker] state['current_shares'][ticker] * state['current_prices'][ticker]

View File

@ -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: 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]: def _calculate_nav_risk(self, etf_data: Dict, etf_type: ETFType) -> Tuple[float, Dict]:
"""Calculate NAV risk components with ETF-type specific adjustments""" """Calculate NAV risk components with ETF-type specific adjustments"""
components = {} components = {}

View File

@ -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()