feat: current dividend trend calculation implementation
This commit is contained in:
parent
8bec6cd8e8
commit
156b218c94
@ -143,6 +143,63 @@ class NavErosionService:
|
|||||||
payout_ratio = min(yield_ratio / 0.20, 1.0)
|
payout_ratio = min(yield_ratio / 0.20, 1.0)
|
||||||
return payout_ratio
|
return payout_ratio
|
||||||
|
|
||||||
|
def _calculate_dividend_trend(self, etf_data: Dict) -> Tuple[float, str]:
|
||||||
|
"""
|
||||||
|
Calculate dividend trend score and direction.
|
||||||
|
Returns a tuple of (trend_score, trend_direction)
|
||||||
|
trend_score: float between -1 and 1
|
||||||
|
trend_direction: str ('Increasing', 'Decreasing', 'Stable', 'Unknown')
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not etf_data.get('dividends'):
|
||||||
|
logger.warning("No dividend data available for trend calculation")
|
||||||
|
return 0.0, "Unknown"
|
||||||
|
|
||||||
|
# Convert dividends to DataFrame
|
||||||
|
dividends = pd.DataFrame.from_dict(etf_data['dividends'], orient='index', columns=['Dividends'])
|
||||||
|
dividends.index = pd.to_datetime(dividends.index)
|
||||||
|
|
||||||
|
# Resample to monthly and calculate rolling averages
|
||||||
|
monthly_divs = dividends.resample('M')['Dividends'].sum()
|
||||||
|
if len(monthly_divs) < 6: # Need at least 6 months of data
|
||||||
|
return 0.0, "Unknown"
|
||||||
|
|
||||||
|
# Calculate 3-month and 6-month moving averages
|
||||||
|
ma3 = monthly_divs.rolling(window=3).mean()
|
||||||
|
ma6 = monthly_divs.rolling(window=6).mean()
|
||||||
|
|
||||||
|
# Calculate trend metrics
|
||||||
|
recent_ma3 = ma3.iloc[-3:].mean()
|
||||||
|
recent_ma6 = ma6.iloc[-6:].mean()
|
||||||
|
|
||||||
|
# Calculate year-over-year growth
|
||||||
|
yearly_divs = dividends.resample('Y')['Dividends'].sum()
|
||||||
|
if len(yearly_divs) >= 2:
|
||||||
|
yoy_growth = (yearly_divs.iloc[-1] / yearly_divs.iloc[-2]) - 1
|
||||||
|
else:
|
||||||
|
yoy_growth = 0
|
||||||
|
|
||||||
|
# Calculate trend score (-1 to 1)
|
||||||
|
ma_trend = (recent_ma3 / recent_ma6) - 1 if recent_ma6 > 0 else 0
|
||||||
|
trend_score = (ma_trend * 0.7 + yoy_growth * 0.3) # Weighted combination
|
||||||
|
|
||||||
|
# Normalize trend score to -1 to 1 range
|
||||||
|
trend_score = max(min(trend_score, 1), -1)
|
||||||
|
|
||||||
|
# Determine trend direction
|
||||||
|
if abs(trend_score) < 0.05:
|
||||||
|
direction = "Stable"
|
||||||
|
elif trend_score > 0:
|
||||||
|
direction = "Increasing"
|
||||||
|
else:
|
||||||
|
direction = "Decreasing"
|
||||||
|
|
||||||
|
return trend_score, direction
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating dividend trend: {str(e)}")
|
||||||
|
return 0.0, "Unknown"
|
||||||
|
|
||||||
def analyze_etf_erosion_risk(self, tickers: List[str]) -> NavErosionAnalysis:
|
def analyze_etf_erosion_risk(self, tickers: List[str]) -> NavErosionAnalysis:
|
||||||
"""Analyze erosion risk for a list of ETFs."""
|
"""Analyze erosion risk for a list of ETFs."""
|
||||||
results = []
|
results = []
|
||||||
@ -164,6 +221,9 @@ class NavErosionService:
|
|||||||
yield_risk, yield_components = self._calculate_yield_risk(etf_data, etf_type)
|
yield_risk, yield_components = self._calculate_yield_risk(etf_data, etf_type)
|
||||||
structural_risk, structural_components = self._calculate_structural_risk(etf_data)
|
structural_risk, structural_components = self._calculate_structural_risk(etf_data)
|
||||||
|
|
||||||
|
# Calculate dividend trend
|
||||||
|
trend_score, trend_direction = self._calculate_dividend_trend(etf_data)
|
||||||
|
|
||||||
# Calculate final risk scores
|
# Calculate final risk scores
|
||||||
final_nav_risk = round(
|
final_nav_risk = round(
|
||||||
nav_risk * self.NAV_RISK_WEIGHT +
|
nav_risk * self.NAV_RISK_WEIGHT +
|
||||||
@ -186,14 +246,15 @@ class NavErosionService:
|
|||||||
yield_risk_explanation=(
|
yield_risk_explanation=(
|
||||||
f"Dividend stability: {yield_components['stability']:.1%}, "
|
f"Dividend stability: {yield_components['stability']:.1%}, "
|
||||||
f"Growth: {yield_components['growth']:.1%}, "
|
f"Growth: {yield_components['growth']:.1%}, "
|
||||||
f"Payout ratio: {yield_components['payout']:.1%}"
|
f"Payout ratio: {yield_components['payout']:.1%}, "
|
||||||
|
f"Trend: {trend_direction}"
|
||||||
),
|
),
|
||||||
etf_age_years=etf_data.get('age_years', 3),
|
etf_age_years=etf_data.get('age_years', 3),
|
||||||
max_drawdown=round(etf_data.get('max_drawdown', 0.0), 3),
|
max_drawdown=round(etf_data.get('max_drawdown', 0.0), 3),
|
||||||
volatility=round(etf_data.get('volatility', 0.0), 3),
|
volatility=round(etf_data.get('volatility', 0.0), 3),
|
||||||
sharpe_ratio=round(etf_data.get('sharpe_ratio', 0.0), 2),
|
sharpe_ratio=round(etf_data.get('sharpe_ratio', 0.0), 2),
|
||||||
sortino_ratio=round(etf_data.get('sortino_ratio', 0.0), 2),
|
sortino_ratio=round(etf_data.get('sortino_ratio', 0.0), 2),
|
||||||
dividend_trend=round(etf_data.get('dividend_trend', 0.0), 3),
|
dividend_trend=trend_score,
|
||||||
component_risks={
|
component_risks={
|
||||||
'nav': nav_components,
|
'nav': nav_components,
|
||||||
'yield': yield_components,
|
'yield': yield_components,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user