Coverage for yield_analysis_sdk\analysis.py: 79%
80 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-02 19:22 +0800
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-02 19:22 +0800
1import math
2from typing import List, Tuple
4from .exceptions import DataError
5from .type import PerformanceAnalysis, SharePriceHistory
8def analyze_yield_with_daily_share_price(
9 share_price_history: SharePriceHistory, risk_free_rate: float = 0.05
10) -> PerformanceAnalysis:
11 """
12 Analyze yield metrics from daily share price data and return essential metrics for allocation decisions.
14 Args:
15 share_price_history: SharePriceHistory object containing daily share prices
16 risk_free_rate: Annual risk-free rate (default 0.05 = 5% for current market conditions)
18 Returns:
19 PerformanceAnalysis object containing essential yield and risk metrics for allocation decisions
20 """
22 daily_share_price: List[Tuple[int, float]] = share_price_history.price_history
24 if not daily_share_price or len(daily_share_price) < 2:
25 raise DataError("At least 2 daily share prices are required for analysis")
27 # sort daily_share_price by timestamp in ascending order
28 daily_share_price.sort(key=lambda x: x[0])
29 # extract price from daily_share_price
30 prices: list[float] = [price for timestamp, price in daily_share_price]
32 # Calculate daily returns
33 daily_returns = []
34 for i in range(1, len(prices)):
35 if prices[i - 1] > 0: # Avoid division by zero
36 daily_return = (prices[i] - prices[i - 1]) / prices[i - 1]
37 daily_returns.append(daily_return)
39 # Calculate core APY metrics (7d, 30d, and 90d are most important for allocation decisions)
40 apy_7d = _calculate_apy(prices, 7)
41 apy_30d = _calculate_apy(prices, 30)
42 apy_90d = _calculate_apy(prices, 90)
44 # Calculate essential risk metrics
45 volatility_30d = _calculate_volatility(daily_returns, 30)
46 max_drawdown = _calculate_max_drawdown(prices)
48 # Calculate Sharpe ratio (mandatory for allocation decisions)
49 sharpe_ratio = _calculate_sharpe_ratio(daily_returns, risk_free_rate)
51 # Create PerformanceAnalysis object
52 performance_analysis = PerformanceAnalysis(
53 apy_7d=apy_7d,
54 apy_30d=apy_30d,
55 apy_90d=apy_90d,
56 volatility_30d=volatility_30d,
57 max_drawdown=max_drawdown,
58 sharpe_ratio=sharpe_ratio,
59 current_price=prices[-1],
60 analysis_period_days=len(prices),
61 )
63 return performance_analysis
66def _calculate_apy(prices: List[float], days: int) -> float:
67 """Calculate APY for a given period."""
68 if len(prices) < days:
69 return 0.0
71 start_price = prices[-days]
72 end_price = prices[-1]
74 if start_price <= 0:
75 return 0.0
77 # Calculate total return
78 total_return = (end_price - start_price) / start_price
80 # Convert to APY (annualized)
81 apy: float = (1 + total_return) ** (365 / days) - 1
83 return apy * 100 # Convert to percentage
86def _calculate_volatility(returns: List[float], days: int) -> float:
87 """Calculate volatility (standard deviation of returns) for a given period."""
88 if len(returns) < days:
89 return 0.0
91 period_returns = returns[-days:]
92 mean_return = sum(period_returns) / len(period_returns)
94 # Calculate sample variance (using n-1 for sample standard deviation)
95 variance = sum((r - mean_return) ** 2 for r in period_returns) / (
96 len(period_returns) - 1
97 )
98 volatility = math.sqrt(variance)
100 # Annualize volatility (assuming daily returns)
101 # For daily data: annual_vol = daily_vol * sqrt(252) (trading days)
102 # For daily data: annual_vol = daily_vol * sqrt(365) (calendar days)
103 annualized_volatility = volatility * math.sqrt(365)
105 return annualized_volatility * 100 # Convert to percentage
108def _calculate_max_drawdown(prices: List[float]) -> float:
109 """Calculate maximum drawdown from peak."""
110 if not prices:
111 return 0.0
113 max_drawdown = 0.0
114 peak = prices[0]
116 for price in prices:
117 if price > peak:
118 peak = price
119 else:
120 drawdown = (peak - price) / peak
121 max_drawdown = max(max_drawdown, drawdown)
123 return max_drawdown * 100 # Convert to percentage
126def _calculate_sharpe_ratio(returns: List[float], risk_free_rate: float) -> float:
127 """Calculate Sharpe ratio using proper annualization."""
128 if not returns:
129 return 0.0
131 mean_return = sum(returns) / len(returns)
133 # Calculate sample variance (using n-1 for sample standard deviation)
134 variance = sum((r - mean_return) ** 2 for r in returns) / (len(returns) - 1)
135 std_dev = math.sqrt(variance)
137 if std_dev == 0:
138 return 0.0
140 # Annualize returns and volatility (assuming daily data)
141 # Daily return to annual: daily_return * 365
142 # Daily volatility to annual: daily_vol * sqrt(365)
143 annualized_return = mean_return * 365
144 annualized_volatility = std_dev * math.sqrt(365)
146 sharpe_ratio = (annualized_return - risk_free_rate) / annualized_volatility
148 return sharpe_ratio
151def _calculate_var(returns: List[float], confidence_level: float) -> float:
152 """Calculate Value at Risk at given confidence level."""
153 if not returns:
154 return 0.0
156 # Sort returns in ascending order
157 sorted_returns = sorted(returns)
159 # Find the percentile
160 index = int((1 - confidence_level) * len(sorted_returns))
161 var = sorted_returns[index]
163 return var * 100 # Convert to percentage
166def _calculate_apy_trend(prices: List[float], days: int) -> float:
167 """Calculate APY trend over a period."""
168 if len(prices) < days * 2:
169 return 0.0
171 # Calculate APY for the most recent period
172 recent_apy = _calculate_apy(prices, days)
174 # Calculate APY for the previous period
175 previous_prices = prices[:-days]
176 previous_apy = _calculate_apy(previous_prices, days)
178 # Calculate trend (positive means increasing APY)
179 trend = recent_apy - previous_apy
181 return trend