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
« 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...
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
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
29import rivapy.tools.interfaces as interfaces
31logger = logging.getLogger(__name__)
32logger.setLevel(logging.DEBUG)
35class DepositSpecification(DeterministicCashflowBondSpecification):
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.
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.
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.
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
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
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())
132 if term is None:
133 term = f"{(maturity_date - issue_date).days}D"
134 else:
135 term = term
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 )
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
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
210 # region properties
212 def ins_type(self):
213 """Return instrument type
215 Returns:
216 Instrument: Forward rate agreement
217 """
218 return Instrument.DEPOSIT
220 # temp placeholder
221 def get_end_date(self):
222 return self._end_date