Coverage for rivapy / instruments / deposit_specifications.py: 69%

68 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-27 14:36 +0000

1# TODO: 

2# - consider proper end date handling 

3# - move date handling to hasexpectedcf 

4# - correct _frequency, _dcc issues... 

5 

6from abc import abstractmethod as _abstractmethod 

7from typing import List as _List, Union as _Union, Tuple, Optional as _Optional 

8import numpy as np 

9import logging 

10from rivapy.instruments.bond_specifications import DeterministicCashflowBondSpecification 

11from datetime import datetime, date, timedelta 

12from rivapy.tools.holidays_compat import HolidayBase as _HolidayBase, ECB as _ECB 

13from dateutil.relativedelta import relativedelta 

14from rivapy.instruments.components import Issuer, NotionalStructure 

15 

16from rivapy.tools.datetools import ( 

17 Period, 

18 _date_to_datetime, 

19 _term_to_period, 

20 calc_end_day, 

21 calc_start_day, 

22 roll_day, 

23 next_or_previous_business_day, 

24 is_business_day, 

25 serialize_date, 

26) 

27from rivapy.tools.enums import DayCounterType, InterestRateIndex, RollConvention, SecuritizationLevel, Currency, Rating, RollRule, Instrument 

28 

29import rivapy.tools.interfaces as interfaces 

30 

31logger = logging.getLogger(__name__) 

32logger.setLevel(logging.DEBUG) 

33 

34 

35class DepositSpecification(DeterministicCashflowBondSpecification): 

36 

37 def __init__( 

38 self, 

39 obj_id: str, 

40 issue_date: _Optional[_Union[date, datetime]] = None, 

41 maturity_date: _Optional[_Union[date, datetime]] = None, 

42 currency: _Union[Currency, str] = "EUR", 

43 notional: _Union[NotionalStructure, float] = 100.0, 

44 rate: float = 0.00, 

45 term: _Optional[_Union[Period, str]] = None, 

46 day_count_convention: _Union[DayCounterType, str] = "ACT360", 

47 business_day_convention: _Union[RollConvention, str] = "ModifiedFollowing", 

48 roll_convention: _Union[RollRule, str] = "EOM", 

49 spot_days: int = 2, 

50 calendar: _Union[_HolidayBase, str] = _ECB(), 

51 issuer: _Optional[_Union[Issuer, str]] = None, 

52 securitization_level: _Union[SecuritizationLevel, str] = "NONE", 

53 payment_days: int = 0, 

54 adjust_start_date: bool = True, 

55 adjust_end_date: bool = False, 

56 ): 

57 """Create a short-term deposit specification. 

58 

59 Accrual start is adjusted according to the provided business day convention. Payment 

60 occurs on the maturity date (plus any settlement/payment days). For overnight ("O/N") 

61 and tomorrow-next ("T/N") deposits the :pyarg:`spot_days` is set to 0 and 1, 

62 respectively. 

63 

64 Args: 

65 obj_id (str): Identifier for the deposit (e.g. ISIN or internal id). 

66 issue_date (date | datetime, optional): Fixing date and start date (of accrual period) of the deposit 

67 is calculated based on the provided issue date given :pyarg:`spot_days` and :pyarg:`adjust_start_date`. Required 

68 if :pyarg:`maturity_date` is computed from :pyarg:`term`. 

69 maturity_date (date | datetime, optional): Maturity date. If ``None`` and 

70 :pyarg:`term` is provided, the maturity will be derived from 

71 :pyarg:`issue_date` and :pyarg:`term`. Corresponds to end date (of accrual period). If non business day, always adjusted according to 

72 :pyarg:`business_day_convention` while end date adjustment is controlled by :pyarg:`adjust_end_date`. 

73 currency (Currency | str, optional): Currency code or enum. Defaults to "EUR". 

74 notional (NotionalStructure | float, optional): Face value; maybe passed as float or notional structure, amount must be positive. Defaults to 100.0. 

75 rate (float, optional): Fixed deposit rate (coupon). Defaults to 0.0. 

76 term (Period | str, optional): Tenor of the deposit (e.g. "3M", "1Y", "O/N", "T/N"). 

77 day_count_convention (DayCounterType | str, optional): Day count convention. 

78 Defaults to :pydata:`DayCounterType.ACT360`. 

79 business_day_convention (RollConvention | str, optional): Business day convention 

80 used for rolling dates. Defaults to :pydata:`RollConvention.MODIFIED_FOLLOWING`. 

81 roll_convention (RollRule | str, optional): Roll rule when building schedules. 

82 Defaults to :pydata:`RollRule.EOM`. 

83 spot_days (int, optional): Settlement lag in days. Defaults to 2; overridden to 0 

84 for O/N and 1 for T/N when :pyarg:`term` is set accordingly. 

85 calendar (HolidayBase | str, optional): Holiday calendar to use. Defaults to ECB. 

86 issuer (Issuer | str, optional): Issuer identifier. 

87 securitization_level (SecuritizationLevel | str, optional): Securitization level. 

88 Defaults to :pydata:`SecuritizationLevel.NONE`. 

89 payment_days (int, optional): Days after maturity when payment occurs. Defaults to 0. 

90 adjust_start_date (bool, optional): If True, roll :pyarg:`issue_date` forward to a 

91 business day when required, to ensure accrual starts on a business day. The adjusted date will be used for calculations. Defaults to True. 

92 adjust_end_date (bool, optional): If True, roll :pyarg:`maturity_date` forward to a 

93 business day when required, to ensure accrual ends on a business day. The adjusted date will be used for calculations. Defaults to False. 

94 

95 Raises: 

96 ValueError: If neither :pyarg:`maturity_date` nor :pyarg:`term` is provided, or if 

97 :pyarg:`issue_date` is required to compute :pyarg:`maturity_date` but is missing. 

98 """ 

99 self.rate = rate 

100 

101 # check and adjust spot_days for O/N and T/N deposits 

102 if term == "O/N": 

103 spd = 0 

104 logger.info("Setting spot_days to 0: O/N deposit.") 

105 elif term == "T/N": 

106 spd = 1 

107 logger.info("Setting spot_days to 1: T/N deposit.") 

108 else: 

109 spd = spot_days 

110 

111 if maturity_date is None and term is None: 

112 raise ValueError("Either maturity_date or term must be provided for DepositSpecification.") 

113 elif maturity_date is None and term is not None: 

114 # calculate maturity date from term and start date 

115 if issue_date is None: 

116 raise ValueError("issue_date must be provided if maturity_date is to be calculated from term.") 

117 # calculate maturity date from term and start date 

118 # roll_day signature: roll_day(day, calendar, business_day_convention, ...) 

119 # previously the calendar and business day convention were passed in the wrong order 

120 if adjust_start_date: 

121 help_date = roll_day(issue_date, calendar, business_day_convention) 

122 else: 

123 help_date = issue_date 

124 # _term_to_period returns a Period(years, months, days) 

125 period = _term_to_period(term) 

126 maturity_date = help_date + relativedelta(years=period.years, months=period.months, days=period.days) 

127 if isinstance(issue_date, date): 

128 issue_date = datetime.combine(issue_date, datetime.min.time()) 

129 if isinstance(maturity_date, date): 

130 maturity_date = datetime.combine(maturity_date, datetime.min.time()) 

131 

132 if term is None: 

133 term = f"{(maturity_date - issue_date).days}D" 

134 else: 

135 term = term 

136 

137 super().__init__( 

138 obj_id=obj_id, 

139 spot_days=spd, 

140 issue_date=issue_date, 

141 maturity_date=maturity_date, 

142 notional=notional, 

143 currency=currency, 

144 coupon=rate, 

145 frequency=term, 

146 day_count_convention=day_count_convention, 

147 business_day_convention=business_day_convention, 

148 roll_convention=roll_convention, 

149 calendar=calendar, 

150 notional_exchange=True, 

151 payment_days=payment_days, 

152 issuer=issuer, 

153 securitization_level=securitization_level, 

154 adjust_end_date=adjust_end_date, 

155 adjust_start_date=adjust_start_date, 

156 ) 

157 

158 @staticmethod 

159 def _create_sample( 

160 n_samples: int, seed: int = None, ref_date=None, issuers: _List[str] = None, sec_levels: _List[str] = None, currencies: _List[str] = None 

161 ) -> _List["DepositSpecification"]: 

162 if seed is not None: 

163 np.random.seed(seed) 

164 if ref_date is None: 

165 ref_date = datetime.now() 

166 else: 

167 ref_date = _date_to_datetime(ref_date) 

168 if issuers is None: 

169 issuers = ["Issuer_" + str(i) for i in range(int(n_samples / 2))] 

170 result = [] 

171 if currencies is None: 

172 currencies = list(Currency) 

173 if sec_levels is None: 

174 sec_levels = list(SecuritizationLevel) 

175 for i in range(n_samples): 

176 days = int(15.0 * 365.0 * np.random.beta(2.0, 2.0)) + 1 

177 start_date = ref_date + timedelta(days=np.random.randint(low=-365, high=0)) 

178 result.append( 

179 DepositSpecification( 

180 obj_id=f"Deposit_{i}", 

181 start_date=start_date, 

182 maturity_date=ref_date + timedelta(days=days), 

183 currency=np.random.choice(currencies), 

184 notional=np.random.choice([100.0, 1000.0, 10_000.0, 100_0000.0]), 

185 rate=np.random.choice([0.01, 0.02, 0.03, 0.04, 0.05]), 

186 issuer=np.random.choice(issuers), 

187 securitization_level=np.random.choice(sec_levels), 

188 ) 

189 ) 

190 return result 

191 

192 def _to_dict(self) -> dict: 

193 result = { 

194 "obj_id": self.obj_id, 

195 "issue_date": serialize_date(self.issue_date), 

196 "maturity_date": serialize_date(self.maturity_date), 

197 "currency": self.currency, 

198 "notional": self.notional, 

199 "rate": self.rate, 

200 "day_count_convention": self.day_count_convention, 

201 "roll_convention": self._roll_convention, 

202 "spot_days": self._spot_days, 

203 "business_day_convention": self.business_day_convention, 

204 "issuer": self.issuer, 

205 "securitization_level": self._securitization_level, 

206 "payment_days": self._payment_days, 

207 } 

208 return result 

209 

210 # region properties 

211 

212 def ins_type(self): 

213 """Return instrument type 

214 

215 Returns: 

216 Instrument: Forward rate agreement 

217 """ 

218 return Instrument.DEPOSIT 

219 

220 # temp placeholder 

221 def get_end_date(self): 

222 return self._end_date