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

1import math 

2from typing import List, Tuple 

3 

4from .exceptions import DataError 

5from .type import PerformanceAnalysis, SharePriceHistory 

6 

7 

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. 

13 

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) 

17 

18 Returns: 

19 PerformanceAnalysis object containing essential yield and risk metrics for allocation decisions 

20 """ 

21 

22 daily_share_price: List[Tuple[int, float]] = share_price_history.price_history 

23 

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") 

26 

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] 

31 

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) 

38 

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) 

43 

44 # Calculate essential risk metrics 

45 volatility_30d = _calculate_volatility(daily_returns, 30) 

46 max_drawdown = _calculate_max_drawdown(prices) 

47 

48 # Calculate Sharpe ratio (mandatory for allocation decisions) 

49 sharpe_ratio = _calculate_sharpe_ratio(daily_returns, risk_free_rate) 

50 

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 ) 

62 

63 return performance_analysis 

64 

65 

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 

70 

71 start_price = prices[-days] 

72 end_price = prices[-1] 

73 

74 if start_price <= 0: 

75 return 0.0 

76 

77 # Calculate total return 

78 total_return = (end_price - start_price) / start_price 

79 

80 # Convert to APY (annualized) 

81 apy: float = (1 + total_return) ** (365 / days) - 1 

82 

83 return apy * 100 # Convert to percentage 

84 

85 

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 

90 

91 period_returns = returns[-days:] 

92 mean_return = sum(period_returns) / len(period_returns) 

93 

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) 

99 

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) 

104 

105 return annualized_volatility * 100 # Convert to percentage 

106 

107 

108def _calculate_max_drawdown(prices: List[float]) -> float: 

109 """Calculate maximum drawdown from peak.""" 

110 if not prices: 

111 return 0.0 

112 

113 max_drawdown = 0.0 

114 peak = prices[0] 

115 

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) 

122 

123 return max_drawdown * 100 # Convert to percentage 

124 

125 

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 

130 

131 mean_return = sum(returns) / len(returns) 

132 

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) 

136 

137 if std_dev == 0: 

138 return 0.0 

139 

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) 

145 

146 sharpe_ratio = (annualized_return - risk_free_rate) / annualized_volatility 

147 

148 return sharpe_ratio 

149 

150 

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 

155 

156 # Sort returns in ascending order 

157 sorted_returns = sorted(returns) 

158 

159 # Find the percentile 

160 index = int((1 - confidence_level) * len(sorted_returns)) 

161 var = sorted_returns[index] 

162 

163 return var * 100 # Convert to percentage 

164 

165 

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 

170 

171 # Calculate APY for the most recent period 

172 recent_apy = _calculate_apy(prices, days) 

173 

174 # Calculate APY for the previous period 

175 previous_prices = prices[:-days] 

176 previous_apy = _calculate_apy(previous_prices, days) 

177 

178 # Calculate trend (positive means increasing APY) 

179 trend = recent_apy - previous_apy 

180 

181 return trend