Coverage for rivapy / instruments / ir_swap_specification.py: 81%

451 statements  

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

1from abc import abstractmethod as _abstractmethod 

2from locale import currency 

3from typing import List as _List, Union as _Union, Tuple, Dict 

4import numpy as np 

5from datetime import datetime, date, timedelta 

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

7from rivapy.instruments._logger import logger 

8 

9from rivapy.tools.datetools import Period, Schedule, _date_to_datetime, _datetime_to_date_list, _term_to_period, serialize_date 

10from rivapy.tools.enums import DayCounterType, RollConvention, SecuritizationLevel, Currency, Rating, Instrument 

11from rivapy.tools._validators import _check_positivity, _check_start_before_end, _string_to_calendar, _is_ascending_date_list 

12import rivapy.tools.interfaces as interfaces 

13from rivapy.tools.datetools import Period, Schedule 

14 

15from rivapy.instruments.bond_specifications import BondBaseSpecification 

16from rivapy.instruments.components import NotionalStructure, ConstNotionalStructure, VariableNotionalStructure # , ResettingNotionalStructure 

17from rivapy.tools.enums import IrLegType 

18 

19# Base each swap leg, off of the IRSwapBaseSpecification 

20# This IRSwapBaseSpecification is in turn, based off of the BondBaseSpecification 

21# Can think about basing the float/fixed leg off of the BondFlaoting/Fixed Note class... 

22 

23# WIP 

24 

25 

26class IrSwapLegSpecification(interfaces.FactoryObject): 

27 """Base interest rate swap leg specification used to define both fixed and floating legs.""" 

28 

29 def __init__( 

30 self, 

31 obj_id: str, 

32 notional: _Union[float, NotionalStructure], 

33 start_dates: _List[datetime], 

34 end_dates: _List[datetime], 

35 pay_dates: _List[datetime], 

36 currency: _Union[Currency, str], 

37 day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

38 ): 

39 """Constructor for the IrSwapLegSpecification class used to define both fixed and floating legs. 

40 

41 Args: 

42 obj_id (str): obj ID for the instrument. 

43 notional (_Union[float, NotionalStructure]): If given a singular float, will convert to ConstNotinalStructure. Contains the notional information. 

44 start_dates (_List[datetime]): start date of interest accrual period. 

45 end_dates (_List[datetime]): end date of interest accrual period. 

46 pay_dates (_List[datetime]): Dates when both legs of the swap exchange cash flows. 

47 currency (_Union[Currency, str]): The currency of the swap 

48 day_count_convention (_Union[DayCounterType, str], optional): The day count convention used. Defaults to DayCounterType.ThirtyU360. 

49 """ 

50 self.obj_id = obj_id 

51 self.notional_structure = notional 

52 self.start_dates = start_dates 

53 self.end_dates = end_dates 

54 self.pay_dates = pay_dates 

55 self.currency = Currency.to_string(currency) 

56 self.day_count_convention = day_count_convention 

57 

58 @property 

59 def currency(self) -> str: 

60 """The swap leg's currency as a string.""" 

61 return self._currency 

62 

63 @currency.setter 

64 def currency(self, value: _Union[Currency, str]): 

65 self._currency = Currency.to_string(value) 

66 

67 @property 

68 def start_dates(self) -> _List[datetime]: 

69 """start dates for the interest periods 

70 

71 Returns: 

72 _List[datetime]: _description_ 

73 """ 

74 return self._start_dates 

75 

76 @start_dates.setter 

77 def start_dates(self, value: _List[datetime]): 

78 self._start_dates = value 

79 

80 @property 

81 def end_dates(self) -> _List[datetime]: 

82 """end dates for the interest periods 

83 

84 Returns: 

85 _List[datetime]: _description_ 

86 """ 

87 return self._end_dates 

88 

89 @end_dates.setter 

90 def end_dates(self, value: _List[datetime]): 

91 self._end_dates = value 

92 

93 @property 

94 def pay_dates(self) -> _List[datetime]: 

95 """pay dates for the interest periods 

96 

97 Returns: 

98 _List[datetime]: 

99 """ 

100 return self._pay_dates 

101 

102 @pay_dates.setter 

103 def pay_dates(self, value: _List[datetime]): 

104 self._pay_dates = value 

105 

106 @property 

107 def notional_structure(self) -> NotionalStructure: 

108 """Return the notionals 

109 

110 Returns: 

111 NotionalStructure: class object detailing the notionals, start dates, ... 

112 """ 

113 return self._notional_structure 

114 

115 @notional_structure.setter 

116 def notional_structure(self, value: _Union[float, NotionalStructure]): 

117 """If only a float is given, assume a constant notional and create a ConstNotionalStructure. 

118 

119 Args: 

120 value (_Union[float, NotionalStructure]): 

121 """ 

122 if isinstance(value, (int, float)): 

123 self._notional_structure = ConstNotionalStructure(float(value)) 

124 else: 

125 self._notional_structure = value 

126 

127 # @abstractmethod 

128 # def reset_dates(self) -> _List[datetime]: 

129 # """ #TODO brought over from pyvacon, for float leg? 

130 # """ 

131 # pass 

132 

133 def _to_dict(self) -> Dict: 

134 return_dict = { 

135 "obj_id": self.obj_id, 

136 "notional": self.notional_structure, 

137 "start_dates": self.start_dates, 

138 "end_dates": self.end_dates, 

139 "pay_dates": self.pay_dates, 

140 "currency": self.currency, 

141 "day_count_convention": self.day_count_convention, 

142 } 

143 return return_dict 

144 

145 @staticmethod 

146 def _create_sample(n_samples: int, seed: int = None): 

147 pass # TODO 

148 

149 

150class IrFixedLegSpecification(IrSwapLegSpecification): 

151 """Specification for a fixed leg for an interest rate swap.""" 

152 

153 def __init__( 

154 self, 

155 fixed_rate: float, 

156 obj_id: str, 

157 notional: _Union[float, NotionalStructure], 

158 start_dates: _List[datetime], 

159 end_dates: _List[datetime], 

160 pay_dates: _List[datetime], 

161 currency: _Union[Currency, str], 

162 day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

163 ): 

164 """Constructor for a fixed leg of an interest rate swap. 

165 

166 Args: 

167 fixed_rate (float): The fixed interest rate defining this leg of the swap. 

168 obj_id (str): obj ID for the instrument. 

169 notional (_Union[float, NotionalStructure]): If given a singular float, will convert to ConstNotinalStructure. Contains the notional information. 

170 start_dates (_List[datetime]): start date of interest accrual period. 

171 end_dates (_List[datetime]): end date of interest accrual period. 

172 pay_dates (_List[datetime]): Dates when both legs of the swap exchange cash flows. 

173 currency (_Union[Currency, str]): The currency of the swap 

174 day_count_convention (_Union[DayCounterType, str], optional): The day count convention used. Defaults to DayCounterType.ThirtyU360. 

175 """ 

176 super().__init__(obj_id, notional, start_dates, end_dates, pay_dates, currency, day_count_convention) 

177 self.fixed_rate = fixed_rate # _check_positivity(fixed_rate) # TODO is there a need for it to be always positive? 

178 

179 # region properties 

180 @property 

181 def leg_type(self) -> str: 

182 return IrLegType.FIXED 

183 

184 @property 

185 def fixed_rate(self) -> float: 

186 return self._fixed_rate 

187 

188 @fixed_rate.setter 

189 def fixed_rate(self, value: float): 

190 self._fixed_rate = value 

191 

192 # @property 

193 # def reset_dates(self) -> _List[datetime]: 

194 # return self.start_dates 

195 

196 @property 

197 def udl_id(self) -> str: 

198 return "" # fixed leg has no underlying 

199 

200 def _to_dict(self): 

201 

202 return_dict = super()._to_dict() 

203 return_dict["fixed_rate"] = self.fixed_rate 

204 return return_dict 

205 

206 @staticmethod 

207 def _create_sample(n_samples: int, seed: int = None): 

208 pass # TODO 

209 

210 # endregion 

211 

212 def get_NotionalStructure(self): 

213 

214 return self.notional_structure 

215 

216 

217class IrFloatLegSpecification(IrSwapLegSpecification): 

218 def __init__( 

219 self, 

220 obj_id: str, 

221 notional: _Union[float, NotionalStructure], 

222 reset_dates: _List[datetime], 

223 start_dates: _List[datetime], 

224 end_dates: _List[datetime], 

225 rate_start_dates: _List[datetime], # are these needed here? or are they obtained from the underlying 

226 rate_end_dates: _List[datetime], 

227 pay_dates: _List[datetime], 

228 currency: _Union[Currency, str], 

229 udl_id: str, 

230 fixing_id: str, 

231 day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

232 rate_day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

233 spread: float = 0.0, 

234 ): 

235 """Constructor for a floating leg of an interest rate swap. 

236 

237 Args: 

238 obj_id (str): obj ID for the instrument. 

239 notional (_Union[float, NotionalStructure]): If given a singular float, will convert to ConstNotinalStructure. Contains the notional information. 

240 reset_dates (_List[datetime]): Date on which the floating rate (e.g., SOFR, LIBOR) is determined 

241 start_dates (_List[datetime]): Date the entire swap begins (effective date) 

242 end_dates (_List[datetime]): Date the swap matures 

243 rate_start_dates (_List[datetime]): start dates for the determination of the underlying rate 

244 rate_end_dates (_List[datetime]): end dates for the determination of the underlying rate 

245 pay_dates (_List[datetime]): Dates when both legs of the swap exchange cash flows. 

246 currency (_Union[Currency, str]): The currency of the swap 

247 udl_id (str): ID of the underlying rate 

248 fixing_id (str): fixing id 

249 day_count_convention (_Union[DayCounterType, str], optional): The day count convention used.. Defaults to DayCounterType.ThirtyU360. 

250 rate_day_count_convention (_Union[DayCounterType, str], optional): The day count convention used for the underlying 

251 . Defaults to DayCounterType.ThirtyU360. 

252 spread (float, optional): Defaults to 0.0. 

253 """ 

254 super().__init__(obj_id, notional, start_dates, end_dates, pay_dates, currency, day_count_convention) 

255 self.reset_dates = reset_dates # TODO: ADD setters to get rid of error notification? 

256 self.rate_start_dates = rate_start_dates 

257 self.rate_end_dates = rate_end_dates 

258 self._spread = spread 

259 self.udl_id = udl_id 

260 self.fixing_id = fixing_id 

261 self.rate_day_count_convention = DayCounterType.to_string(rate_day_count_convention) 

262 

263 # TODO to_dict method 

264 

265 # region properties 

266 @property 

267 def leg_type(self) -> str: 

268 return IrLegType.FLOAT 

269 

270 @property 

271 def reset_dates(self) -> _List[datetime]: 

272 return self._reset_dates 

273 

274 @reset_dates.setter 

275 def reset_dates(self, value): 

276 self._reset_dates = value 

277 

278 @property 

279 def udl_id(self) -> str: 

280 return self._udl_id 

281 

282 @udl_id.setter 

283 def udl_id(self, value): 

284 self._udl_id = value 

285 

286 @property 

287 def fixing_id(self) -> str: 

288 return self._fixing_id 

289 

290 @fixing_id.setter 

291 def fixing_id(self, value): 

292 self._fixing_id = value 

293 

294 @property 

295 def spread(self) -> float: 

296 return self._spread 

297 

298 @spread.setter 

299 def spread(self, value): 

300 self._spread = value 

301 

302 @property 

303 def rate_day_count(self) -> str: 

304 return self.rate_day_count 

305 

306 @property 

307 def rate_start_dates(self) -> _List[datetime]: 

308 return self._rate_start_dates 

309 

310 @rate_start_dates.setter 

311 def rate_start_dates(self, value): 

312 self._rate_start_dates = value 

313 

314 @property 

315 def rate_end_dates(self) -> _List[datetime]: 

316 return self._rate_end_dates 

317 

318 @rate_end_dates.setter 

319 def rate_end_dates(self, value): 

320 self._rate_end_dates = value 

321 

322 def get_underlyings(self) -> Dict[str, str]: 

323 return {self.udl_id: self.fixing_id} 

324 

325 # endregion 

326 

327 def get_NotionalStructure(self): 

328 

329 return self.notional_structure 

330 

331 

332class IrOISLegSpecification(IrSwapLegSpecification): 

333 def __init__( 

334 self, 

335 obj_id: str, 

336 notional: _Union[float, NotionalStructure], 

337 rate_reset_dates: _List[datetime], 

338 start_dates: _List[datetime], 

339 end_dates: _List[datetime], 

340 rate_start_dates: _List[datetime], # are these needed here? or are they obtained from the underlying 

341 rate_end_dates: _List[datetime], 

342 pay_dates: _List[datetime], 

343 currency: _Union[Currency, str], 

344 udl_id: str, 

345 fixing_id: str, 

346 day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

347 rate_day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

348 spread: float = 0.0, 

349 ): 

350 

351 # fixingLag? 

352 # payLag? 

353 # freq? 

354 # rate currency vs notional currency? 

355 # leg holidays, and rate holidays - again to be dealt with in the scheduler? 

356 # FYI scheduler needs, start date, end date, freq, roll convention, holiday... 

357 # for the reset dates, will need fixing lag 

358 # for the pay dates generation, will need payLag 

359 

360 """Constructor for a floating leg of an overnight index swap. 

361 

362 Args: 

363 obj_id (str): obj ID for the instrument. 

364 notional (_Union[float, NotionalStructure]): If given a singular float, will convert to ConstNotinalStructure. Contains the notional information. 

365 reset_dates (_List[datetime]): Date on which the floating rate (e.g., SOFR, LIBOR) is determined 

366 start_dates (_List[datetime]): Date the entire swap begins (effective date) 

367 end_dates (_List[datetime]): Date the swap matures 

368 rate_start_dates (_List[datetime]): start dates for the determination of the underlying rate 

369 rate_end_dates (_List[datetime]): end dates for the determination of the underlying rate 

370 pay_dates (_List[datetime]): Dates when both legs of the swap exchange cash flows. 

371 currency (_Union[Currency, str]): The currency of the swap 

372 udl_id (str): ID of the underlying rate 

373 fixing_id (str): fixing id 

374 day_count_convention (_Union[DayCounterType, str], optional): The day count convention used.. Defaults to DayCounterType.ThirtyU360. 

375 rate_day_count_convention (_Union[DayCounterType, str], optional): The day count convention used for the underlying 

376 . Defaults to DayCounterType.ThirtyU360. 

377 spread (float, optional): . Defaults to 0.0. 

378 """ 

379 super().__init__(obj_id, notional, start_dates, end_dates, pay_dates, currency, day_count_convention) 

380 self.rate_reset_dates = rate_reset_dates 

381 self.rate_start_dates = rate_start_dates 

382 self.rate_end_dates = rate_end_dates 

383 self._spread = spread 

384 self.udl_id = udl_id 

385 self.fixing_id = fixing_id 

386 self.rate_day_count_convention = DayCounterType.to_string(rate_day_count_convention) 

387 

388 # TODO to_dict method 

389 

390 # region properties 

391 @property 

392 def leg_type(self) -> str: 

393 return IrLegType.OIS 

394 

395 @property 

396 def reset_dates(self) -> _List[datetime]: 

397 return self._rate_reset_dates 

398 

399 @reset_dates.setter 

400 def rate_reset_dates(self, value): 

401 self._rate_reset_dates = value 

402 

403 @property 

404 def udl_id(self) -> str: 

405 return self._udl_id 

406 

407 @udl_id.setter 

408 def udl_id(self, value): 

409 self._udl_id = value 

410 

411 @property 

412 def fixing_id(self) -> str: 

413 return self._fixing_id 

414 

415 @fixing_id.setter 

416 def fixing_id(self, value): 

417 self._fixing_id = value 

418 

419 @property 

420 def spread(self) -> float: 

421 return self._spread 

422 

423 @spread.setter 

424 def spread(self, value): 

425 self._spread = value 

426 

427 @property 

428 def rate_day_count(self) -> str: 

429 return self.rate_day_count 

430 

431 @property 

432 def rate_start_dates(self) -> _List[datetime]: 

433 return self._rate_start_dates 

434 

435 @rate_start_dates.setter 

436 def rate_start_dates(self, value): 

437 self._rate_start_dates = value 

438 

439 @property 

440 def rate_end_dates(self) -> _List[datetime]: 

441 return self._rate_end_dates 

442 

443 @rate_end_dates.setter 

444 def rate_end_dates(self, value): 

445 self._rate_end_dates = value 

446 

447 def get_underlyings(self) -> Dict[str, str]: 

448 return {self.udl_id: self.fixing_id} 

449 

450 # endregion 

451 

452 def get_NotionalStructure(self): 

453 

454 return self.notional_structure 

455 

456 @staticmethod 

457 def ois_scheduler_2D(start_dates: _List[datetime], end_dates: _List[datetime]): 

458 """The OIS makeshift scheduler to account for expected 2D array structure of 

459 >>> rivapy.instruments.interest_rate_swap_pricer.populate_cashflows_ois 

460 Alternatively, the rivapy Scheduler class is used. 

461 This is a simplified version for testing purposes. 

462 Args: 

463 start_dates (_List[datetime]): start dates for a given accrual period, containing the daily start rates inside that accrual period 

464 end_dates (_List[datetime]): _description_ 

465 

466 Returns: 

467 _type_: Multi-dimensional lists of datetimes 

468 """ 

469 

470 daily_rate_start_dates = [] # 2D list: coupon i -> list of daily starts 

471 daily_rate_end_dates = [] # 2D list: coupon i -> list of daily ends 

472 daily_rate_reset_dates = [] # 2D list: coupon i -> list of reset dates 

473 pay_dates = [] # 1D list: one pay date per coupon 

474 

475 for i in range(len(start_dates)): 

476 

477 # for this test we keep it simple and ignore conventions e.g. business day or so. i.e just take every day 

478 num_days = (end_dates[i] - start_dates[i]).days 

479 daily_schedule = [start_dates[i] + timedelta(days=j) for j in range(num_days)] 

480 

481 # Build start/end date pairs for accrual periods 

482 starts = daily_schedule[:-1] # all except last 

483 ends = daily_schedule[1:] # all except first 

484 

485 daily_rate_start_dates.append(starts) 

486 daily_rate_end_dates.append(ends) 

487 

488 # 4. Compute reset dates (fixing lag applied to each start) 

489 # resets = [add_business_days(start, fixingLag, rateHolidays) 

490 # for start in starts] 

491 # assume simple case reset date is the same as start date 

492 resets = starts # reset dates are equal to start dates if spot lag is 0. 

493 daily_rate_reset_dates.append(resets) 

494 

495 # Compute payment date for the coupon 

496 # pay_date = add_business_days(end_dates[i], payLag, holidays) 

497 # assume simple case, pay date is end date 

498 pay_date = end_dates[i] 

499 pay_dates.append(pay_date) 

500 

501 return [daily_rate_start_dates, daily_rate_end_dates, daily_rate_reset_dates, pay_dates] 

502 

503 

504class InterestRateSwapSpecification(interfaces.FactoryObject): 

505 

506 def __init__( 

507 self, 

508 obj_id: str, 

509 notional: _Union[float, NotionalStructure], 

510 issue_date: _Union[date, datetime], 

511 maturity_date: _Union[date, datetime], 

512 pay_leg: _Union[IrFixedLegSpecification, IrFloatLegSpecification, IrOISLegSpecification], 

513 receive_leg: _Union[IrFixedLegSpecification, IrFloatLegSpecification, IrOISLegSpecification], 

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

515 calendar: _Union[_HolidayBase, str] = None, 

516 day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

517 business_day_convention: _Union[RollConvention, str] = RollConvention.FOLLOWING, 

518 issuer: str = None, 

519 securitization_level: _Union[SecuritizationLevel, str] = SecuritizationLevel.NONE, 

520 rating: _Union[Rating, str] = Rating.NONE, 

521 ): 

522 """Specification of the entire swap, encapsulating both the pay leg and the receive leg. 

523 

524 Args: 

525 obj_id (str): _description_ 

526 notional (_Union[float, NotionalStructure]): _description_ 

527 issue_date (_Union[date, datetime]): _description_ 

528 maturity_date (_Union[date, datetime]): _description_ 

529 pay_leg (_Union[IrFixedLegSpecification, IrFloatLegSpecification]): _description_ 

530 receive_leg (_Union[IrFixedLegSpecification, IrFloatLegSpecification]): _description_ 

531 currency (_Union[Currency, str], optional): _description_. Defaults to "EUR". 

532 calendar (_Union[_HolidayBase, str], optional): _description_. Defaults to None. 

533 day_count_convention (_Union[DayCounterType, str], optional): _description_. Defaults to DayCounterType.ThirtyU360. 

534 business_day_convention (_Union[RollConvention, str], optional): _description_. Defaults to RollConvention.FOLLOWING. 

535 issuer (str, optional): _description_. Defaults to None. 

536 securitization_level (_Union[SecuritizationLevel, str], optional): _description_. Defaults to SecuritizationLevel.NONE. 

537 rating (_Union[Rating, str], optional): _description_. Defaults to Rating.NONE. 

538 """ 

539 self.obj_id = obj_id 

540 if issuer is not None: 

541 self.issuer = issuer 

542 if securitization_level is not None: 

543 self.securitization_level = securitization_level 

544 self.issue_date = issue_date 

545 self.maturity_date = maturity_date 

546 self.currency = currency 

547 self.notional_structure = notional 

548 self.rating = Rating.to_string(rating) 

549 # validate dates 

550 self._validate_derived_issued_instrument() 

551 self.pay_leg = pay_leg 

552 self.receive_leg = receive_leg 

553 self.day_count_convention = day_count_convention 

554 self.business_day_convention = RollConvention.to_string(business_day_convention) 

555 if calendar is None: 

556 self.calendar = _ECB(years=range(issue_date.year, maturity_date.year + 1)) 

557 else: 

558 self.calendar = _string_to_calendar(calendar) 

559 

560 @staticmethod # TODO 

561 def _create_sample( 

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

563 ) -> _List["InterestRateSwapSpecification"]: 

564 if seed is not None: 

565 np.random.seed(seed) 

566 if ref_date is None: 

567 ref_date = datetime.now() 

568 else: 

569 ref_date = _date_to_datetime(ref_date) 

570 if issuers is None: 

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

572 result = [] 

573 if currencies is None: 

574 currencies = list(Currency) 

575 if sec_levels is None: 

576 sec_levels = list(SecuritizationLevel) 

577 for i in range(n_samples): 

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

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

580 result.append( 

581 InterestRateSwapSpecification( 

582 obj_id=f"IRS_{i}", 

583 issue_date=issue_date, 

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

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

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

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

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

589 ) 

590 ) 

591 return result 

592 

593 def _validate_derived_issued_instrument(self): 

594 self.__issue_date, self.__maturity_date = _check_start_before_end(self.__issue_date, self.__maturity_date) 

595 

596 def _to_dict(self) -> dict: 

597 result = { 

598 "obj_id": self.obj_id, 

599 "issuer": self.issuer, 

600 "securitization_level": self.securitization_level, 

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

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

603 "currency": self.currency, 

604 "notional": self.notional_structure, 

605 "rating": self.rating, 

606 "receive_leg": self.receive_leg, 

607 "pay_leg": self.pay_leg, 

608 "calendar": getattr(self.calendar, "name", self.calendar.__class__.__name__), 

609 "day_count_convention": self.day_count_convention, 

610 "business_day_convention": self.business_day_convention, 

611 } 

612 return result 

613 

614 # region properties 

615 @property 

616 def issuer(self) -> str: 

617 """ 

618 Getter for instrument's issuer. 

619 

620 Returns: 

621 str: Instrument's issuer. 

622 """ 

623 return self.__issuer 

624 

625 @issuer.setter 

626 def issuer(self, issuer: str): 

627 """ 

628 Setter for instrument's issuer. 

629 

630 Args: 

631 issuer(str): Issuer of the instrument. 

632 """ 

633 self.__issuer = issuer 

634 

635 @property 

636 def rating(self) -> str: 

637 return self.__rating 

638 

639 @rating.setter 

640 def rating(self, rating: _Union[Rating, str]) -> str: 

641 self.__rating = Rating.to_string(rating) 

642 

643 @property 

644 def securitization_level(self) -> str: 

645 """ 

646 Getter for instrument's securitisation level. 

647 

648 Returns: 

649 str: Instrument's securitisation level. 

650 """ 

651 return self.__securitization_level 

652 

653 @securitization_level.setter 

654 def securitization_level(self, securitisation_level: _Union[SecuritizationLevel, str]): 

655 self.__securitization_level = SecuritizationLevel.to_string(securitisation_level) 

656 

657 @property 

658 def issue_date(self) -> date: 

659 """ 

660 Getter for IR swap's issue date. 

661 

662 Returns: 

663 date: IR swap's issue date. 

664 """ 

665 return self.__issue_date 

666 

667 @issue_date.setter 

668 def issue_date(self, issue_date: _Union[datetime, date]): 

669 """ 

670 Setter for IR swap's issue date. 

671 

672 Args: 

673 issue_date (Union[datetime, date]): IR swap's issue date. 

674 """ 

675 self.__issue_date = _date_to_datetime(issue_date) 

676 

677 @property 

678 def maturity_date(self) -> date: 

679 """ 

680 Getter for IR swap's maturity date. 

681 

682 Returns: 

683 date: IR swap's maturity date. 

684 """ 

685 return self.__maturity_date 

686 

687 @maturity_date.setter 

688 def maturity_date(self, maturity_date: _Union[datetime, date]): 

689 """ 

690 Setter for IR swap's maturity date. 

691 

692 Args: 

693 maturity_date (Union[datetime, date]): IR swap's maturity date. 

694 """ 

695 self.__maturity_date = _date_to_datetime(maturity_date) 

696 

697 @property 

698 def currency(self) -> str: 

699 """ 

700 Getter for IR swap's currency. 

701 

702 Returns: 

703 str: IR swap's ISO 4217 currency code 

704 """ 

705 return self.__currency 

706 

707 @currency.setter 

708 def currency(self, currency: str): 

709 self.__currency = Currency.to_string(currency) 

710 

711 @property 

712 def notional_structure(self) -> NotionalStructure: 

713 """Return the notionals 

714 

715 Returns: 

716 NotionalStructure: class object detailing the notionals, start dates, ... 

717 """ 

718 return self._notional_structure 

719 

720 @notional_structure.setter 

721 def notional_structure(self, value: _Union[float, NotionalStructure]): 

722 """If only a float is given, assume a constant notional and create a ConstNotionalStructure. 

723 

724 Args: 

725 value (_Union[float, NotionalStructure]): _description_ 

726 """ 

727 if isinstance(value, (int, float)): 

728 self._notional_structure = ConstNotionalStructure(float(value)) 

729 else: 

730 self._notional_structure = value 

731 

732 def get_pay_leg(self): 

733 return self.pay_leg 

734 

735 def get_receive_leg(self): 

736 return self.receive_leg 

737 

738 def get_fixed_leg(self): 

739 """get the fixed leg (only possible for fixed vs. floating swaps -> throws otherwise)""" 

740 

741 pay_leg_is_fixed = self.get_pay_leg().leg_type == IrLegType.FIXED 

742 receive_leg_is_fixed = self.get_receive_leg().leg_type == IrLegType.FIXED 

743 

744 if pay_leg_is_fixed and not receive_leg_is_fixed: 

745 leg = self.get_pay_leg() 

746 elif receive_leg_is_fixed and not pay_leg_is_fixed: 

747 leg = self.get_receive_leg() 

748 else: 

749 raise ValueError("Swap is not comprised of one fixed leg and one float/OIS leg!") 

750 

751 return leg 

752 

753 def get_float_leg(self): 

754 """get the float leg (only possible for fixed vs. floating swaps -> throws otherwise)""" 

755 

756 pay_leg_is_float = self.get_pay_leg().leg_type in [IrLegType.FLOAT, IrLegType.OIS] 

757 receive_leg_is_float = self.get_receive_leg().leg_type in [IrLegType.FLOAT, IrLegType.OIS] 

758 

759 if pay_leg_is_float and not receive_leg_is_float: 

760 leg = self.get_pay_leg() 

761 elif receive_leg_is_float and not pay_leg_is_float: 

762 leg = self.get_receive_leg() 

763 else: 

764 raise ValueError("Swap is not comprised of one fixed leg and one float/OIS leg!") 

765 

766 return leg 

767 

768 def ins_type(self): 

769 """Return instrument type 

770 

771 Returns: 

772 Instrument: Interest Rate Swap 

773 """ 

774 return Instrument.IRS 

775 

776 # temp placeholder 

777 def get_end_date(self): 

778 return self.maturity_date 

779 

780 # endregion 

781 

782 

783class InterestRateBasisSwapSpecification(interfaces.FactoryObject): 

784 

785 def __init__( 

786 self, 

787 obj_id: str, 

788 notional: _Union[float, NotionalStructure], 

789 issue_date: _Union[date, datetime], 

790 maturity_date: _Union[date, datetime], 

791 pay_leg: IrFloatLegSpecification, 

792 receive_leg: IrFloatLegSpecification, 

793 spread_leg: IrFixedLegSpecification, 

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

795 calendar: _Union[_HolidayBase, str] = None, 

796 day_count_convention: _Union[DayCounterType, str] = DayCounterType.ThirtyU360, 

797 business_day_convention: _Union[RollConvention, str] = RollConvention.FOLLOWING, 

798 issuer: str = None, 

799 securitization_level: _Union[SecuritizationLevel, str] = SecuritizationLevel.NONE, 

800 rating: _Union[Rating, str] = Rating.NONE, 

801 ): 

802 """Specification of the entire swap, encapsulating both the pay leg and the receive leg. 

803 

804 Args: 

805 obj_id (str): _description_ 

806 notional (_Union[float, NotionalStructure]): _description_ 

807 issue_date (_Union[date, datetime]): _description_ 

808 maturity_date (_Union[date, datetime]): _description_ 

809 pay_leg (_Union[IrFixedLegSpecification, IrFloatLegSpecification]): _description_ 

810 receive_leg (_Union[IrFixedLegSpecification, IrFloatLegSpecification]): _description_ 

811 currency (_Union[Currency, str], optional): _description_. Defaults to "EUR". 

812 calendar (_Union[_HolidayBase, str], optional): _description_. Defaults to None. 

813 day_count_convention (_Union[DayCounterType, str], optional): _description_. Defaults to DayCounterType.ThirtyU360. 

814 business_day_convention (_Union[RollConvention, str], optional): _description_. Defaults to RollConvention.FOLLOWING. 

815 issuer (str, optional): _description_. Defaults to None. 

816 securitization_level (_Union[SecuritizationLevel, str], optional): _description_. Defaults to SecuritizationLevel.NONE. 

817 rating (_Union[Rating, str], optional): _description_. Defaults to Rating.NONE. 

818 """ 

819 self.obj_id = obj_id 

820 if issuer is not None: 

821 self.issuer = issuer 

822 if securitization_level is not None: 

823 self.securitization_level = securitization_level 

824 self.issue_date = issue_date 

825 self.maturity_date = maturity_date 

826 self.currency = currency 

827 self.notional_structure = notional 

828 self.rating = Rating.to_string(rating) 

829 # validate dates 

830 self._validate_derived_issued_instrument() 

831 self.pay_leg = pay_leg 

832 self.receive_leg = receive_leg 

833 self.spread_leg = spread_leg 

834 self.day_count_convention = day_count_convention 

835 self.business_day_convention = RollConvention.to_string(business_day_convention) 

836 if calendar is None: 

837 self.calendar = _ECB(years=range(issue_date.year, maturity_date.year + 1)) 

838 else: 

839 self.calendar = _string_to_calendar(calendar) 

840 

841 @staticmethod # TODO 

842 def _create_sample( 

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

844 ) -> _List["InterestRateBasisSwapSpecification"]: 

845 if seed is not None: 

846 np.random.seed(seed) 

847 if ref_date is None: 

848 ref_date = datetime.now() 

849 else: 

850 ref_date = _date_to_datetime(ref_date) 

851 if issuers is None: 

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

853 result = [] 

854 if currencies is None: 

855 currencies = list(Currency) 

856 if sec_levels is None: 

857 sec_levels = list(SecuritizationLevel) 

858 for i in range(n_samples): 

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

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

861 pay_leg = IrFloatLegSpecification() # TODO how to randomly generate? 

862 receive_leg = IrFloatLegSpecification() 

863 spread_leg = IrFixedLegSpecification() 

864 result.append( 

865 InterestRateBasisSwapSpecification( 

866 obj_id=f"IRS_{i}", 

867 issue_date=issue_date, 

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

869 pay_leg=pay_leg, 

870 receive_leg=receive_leg, 

871 spread_leg=spread_leg, 

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

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

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

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

876 ) 

877 ) 

878 return result 

879 

880 def _validate_derived_issued_instrument(self): 

881 self.__issue_date, self.__maturity_date = _check_start_before_end(self.__issue_date, self.__maturity_date) 

882 

883 def _to_dict(self) -> dict: 

884 result = { 

885 "obj_id": self.obj_id, 

886 "issuer": self.issuer, 

887 "securitization_level": self.securitization_level, 

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

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

890 "currency": self.currency, 

891 "notional": self.notional_structure, 

892 "rating": self.rating, 

893 "receive_leg": self.receive_leg, 

894 "pay_leg": self.pay_leg, 

895 "spread_leg": self.spread_leg, 

896 "calendar": getattr(self.calendar, "name", self.calendar.__class__.__name__), 

897 "day_count_convention": self.day_count_convention, 

898 "business_day_convention": self.business_day_convention, 

899 } 

900 return result 

901 

902 # region properties 

903 @property 

904 def issuer(self) -> str: 

905 """ 

906 Getter for instrument's issuer. 

907 

908 Returns: 

909 str: Instrument's issuer. 

910 """ 

911 return self.__issuer 

912 

913 @issuer.setter 

914 def issuer(self, issuer: str): 

915 """ 

916 Setter for instrument's issuer. 

917 

918 Args: 

919 issuer(str): Issuer of the instrument. 

920 """ 

921 self.__issuer = issuer 

922 

923 @property 

924 def rating(self) -> str: 

925 return self.__rating 

926 

927 @rating.setter 

928 def rating(self, rating: _Union[Rating, str]) -> str: 

929 self.__rating = Rating.to_string(rating) 

930 

931 @property 

932 def securitization_level(self) -> str: 

933 """ 

934 Getter for instrument's securitisation level. 

935 

936 Returns: 

937 str: Instrument's securitisation level. 

938 """ 

939 return self.__securitization_level 

940 

941 @securitization_level.setter 

942 def securitization_level(self, securitisation_level: _Union[SecuritizationLevel, str]): 

943 self.__securitization_level = SecuritizationLevel.to_string(securitisation_level) 

944 

945 @property 

946 def issue_date(self) -> date: 

947 """ 

948 Getter for IR swap's issue date. 

949 

950 Returns: 

951 date: IR swap's issue date. 

952 """ 

953 return self.__issue_date 

954 

955 @issue_date.setter 

956 def issue_date(self, issue_date: _Union[datetime, date]): 

957 """ 

958 Setter for IR swap's issue date. 

959 

960 Args: 

961 issue_date (Union[datetime, date]): IR swap's issue date. 

962 """ 

963 self.__issue_date = _date_to_datetime(issue_date) 

964 

965 @property 

966 def maturity_date(self) -> date: 

967 """ 

968 Getter for IR swap's maturity date. 

969 

970 Returns: 

971 date: IR swap's maturity date. 

972 """ 

973 return self.__maturity_date 

974 

975 @maturity_date.setter 

976 def maturity_date(self, maturity_date: _Union[datetime, date]): 

977 """ 

978 Setter for IR swap's maturity date. 

979 

980 Args: 

981 maturity_date (Union[datetime, date]): IR swap's maturity date. 

982 """ 

983 self.__maturity_date = _date_to_datetime(maturity_date) 

984 

985 @property 

986 def currency(self) -> str: 

987 """ 

988 Getter for IR swap's currency. 

989 

990 Returns: 

991 str: IR swap's ISO 4217 currency code 

992 """ 

993 return self.__currency 

994 

995 @currency.setter 

996 def currency(self, currency: str): 

997 self.__currency = Currency.to_string(currency) 

998 

999 @property 

1000 def notional_structure(self) -> NotionalStructure: 

1001 """Return the notionals 

1002 

1003 Returns: 

1004 NotionalStructure: class object detailing the notionals, start dates, ... 

1005 """ 

1006 return self._notional_structure 

1007 

1008 @notional_structure.setter 

1009 def notional_structure(self, value: _Union[float, NotionalStructure]): 

1010 """If only a float is given, assume a constant notional and create a ConstNotionalStructure. 

1011 

1012 Args: 

1013 value (_Union[float, NotionalStructure]): _description_ 

1014 """ 

1015 if isinstance(value, (int, float)): 

1016 self._notional_structure = ConstNotionalStructure(float(value)) 

1017 else: 

1018 self._notional_structure = value 

1019 

1020 def get_pay_leg(self): 

1021 return self.pay_leg 

1022 

1023 def get_receive_leg(self): 

1024 return self.receive_leg 

1025 

1026 def get_spread_leg(self): 

1027 return self.spread_leg 

1028 

1029 def get_fixed_leg(self): 

1030 """get the fixed leg (only possible for fixed vs. floating swaps -> throws otherwise)""" 

1031 

1032 pay_leg_is_fixed = self.get_pay_leg().leg_type == IrLegType.FIXED 

1033 receive_leg_is_fixed = self.get_receive_leg().leg_type == IrLegType.FIXED 

1034 

1035 if pay_leg_is_fixed and not receive_leg_is_fixed: 

1036 leg = self.get_pay_leg() 

1037 elif receive_leg_is_fixed and not pay_leg_is_fixed: 

1038 leg = self.get_receive_leg() 

1039 else: 

1040 raise ValueError("Swap is not comprised of one fixed leg and one float/OIS leg!") 

1041 

1042 return leg 

1043 

1044 def get_float_leg(self): 

1045 """get the float leg (only possible for fixed vs. floating swaps -> throws otherwise)""" 

1046 

1047 pay_leg_is_float = self.get_pay_leg().leg_type in [IrLegType.FLOAT, IrLegType.OIS] 

1048 receive_leg_is_float = self.get_receive_leg().leg_type in [IrLegType.FLOAT, IrLegType.OIS] 

1049 

1050 if pay_leg_is_float and not receive_leg_is_float: 

1051 leg = self.get_pay_leg() 

1052 elif receive_leg_is_float and not pay_leg_is_float: 

1053 leg = self.get_receive_leg() 

1054 else: 

1055 raise ValueError("Swap is not comprised of one fixed leg and one float/OIS leg!") 

1056 

1057 return leg 

1058 

1059 def ins_type(self): 

1060 """Return instrument type 

1061 

1062 Returns: 

1063 Instrument: Basis Swap 

1064 """ 

1065 return Instrument.BS 

1066 

1067 # temp placeholder 

1068 def get_end_date(self): 

1069 return self.maturity_date 

1070 

1071 # endregion