Coverage for rivapy / instruments / bond_specifications.py: 64%

519 statements  

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

1import abc 

2import numpy as np 

3from rivapy.instruments._logger import logger 

4from rivapy.instruments.components import ( 

5 AmortizationScheme, 

6 ZeroAmortizationScheme, 

7 Issuer, 

8 LinearAmortizationScheme, 

9 LinearNotionalStructure, 

10 VariableAmortizationScheme, 

11) 

12from rivapy.marketdata.fixing_table import FixingTable 

13import rivapy.tools.interfaces as interfaces 

14from scipy.optimize import brentq 

15 

16from collections import defaultdict 

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

18from dateutil.relativedelta import relativedelta 

19from rivapy.tools.enums import Currency, Rating, SecuritizationLevel, RollConvention, InterestRateIndex, get_index_by_alias 

20from rivapy.tools.datetools import _date_to_datetime, Schedule, Period, DayCounterType, DayCounter, _string_to_period 

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

22from rivapy.tools._validators import ( 

23 _check_positivity, 

24 _check_start_before_end, 

25 _string_to_calendar, 

26 _check_start_at_or_before_end, 

27 _check_non_negativity, 

28 _is_ascending_date_list, 

29) 

30from datetime import datetime, date, timedelta 

31from rivapy.tools.datetools import ( 

32 _term_to_period, 

33 calc_end_day, 

34 calc_start_day, 

35 roll_day, 

36 next_or_previous_business_day, 

37 is_business_day, 

38 RollRule, 

39 serialize_date, 

40) 

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

42 

43# placeholder 

44from rivapy.marketdata.curves import DiscountCurve 

45 

46 

47class BondBaseSpecification(interfaces.FactoryObject): 

48 """Base class for bond-like instrument specifications. 

49 

50 This class implements common properties shared by bonds, deposits and other 

51 deterministic cashflow instruments such as issue/maturity dates, notional 

52 handling, currency and basic validation. Subclasses should implement 

53 instrument-specific schedule and cashflow behaviour. 

54 """ 

55 

56 # ToDo: amend setter and property to handle float vs notional structure upon initialization, focus on Const and Linear, and variable with provided %-vector (as in FBG) 

57 # ToDo: amend setter and property to handle amortization scheme upon initialization 

58 # ToDo: adjust getCashFlows methods in derived classes accordingly 

59 def __init__( 

60 self, 

61 obj_id: str, 

62 issue_date: _Union[date, datetime], 

63 maturity_date: _Union[date, datetime], 

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

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

66 amortization_scheme: _Optional[_Union[str, AmortizationScheme]] = None, 

67 issuer: str = None, 

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

69 rating: _Union[Rating, str] = "NONE", 

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

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

72 roll_convention: _Union[RollRule, str] = "NONE", 

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

74 ): 

75 """Base bond specification. 

76 

77 Args: 

78 obj_id (str): (Preferably) Unique label of the bond, e.g. ISIN. 

79 issue_date (_Union[date, datetime]): Date of bond issuance. 

80 maturity_date (_Union[date, datetime]): Bond's maturity/expiry date. Must lie after the issue_date. 

81 currency (str, optional): Currency as alphabetic, Defaults to 'EUR'. 

82 notional (float, optional): Bond's notional/face value. Must be positive. Defaults to 100.0. 

83 issuer (str, optional): Name/id of issuer. Defaults to None. 

84 securitization_level (_Union[SecuritizationLevel, str], optional): Securitization level. Defaults to None. 

85 rating (_Union[Rating, str]): Paper rating. 

86 """ 

87 self.obj_id = obj_id 

88 if issuer is not None: 

89 self._issuer = issuer 

90 else: 

91 self._issuer = "Unknown" 

92 if securitization_level is not None: 

93 self._securitization_level = securitization_level 

94 self._issue_date = issue_date 

95 self._maturity_date = maturity_date 

96 self._currency = currency 

97 self._amortization_scheme = self.set_amortization_scheme(amortization_scheme) 

98 # pass the resolved amortization scheme (object) to set_notional_structure 

99 self._notional = self.set_notional_structure(notional, self._amortization_scheme) 

100 self._rating = Rating.to_string(rating) 

101 self._day_count_convention = day_count_convention 

102 self._business_day_convention = business_day_convention 

103 self._roll_convention = roll_convention 

104 self._calendar = calendar 

105 # validate dates 

106 self._validate_derived_issued_instrument() 

107 

108 def set_amortization_scheme(self, amortization_scheme) -> AmortizationScheme: 

109 """Resolve an amortization scheme descriptor into an AmortizationScheme. 

110 

111 Accepts one of: 

112 - None: returns a ZeroAmortizationScheme 

113 - str: resolves the identifier via AmortizationScheme._from_string 

114 - AmortizationScheme instance: returned unchanged 

115 

116 Args: 

117 amortization_scheme (None | str | AmortizationScheme): descriptor. 

118 

119 Returns: 

120 AmortizationScheme: concrete amortization scheme object. 

121 

122 Raises: 

123 ValueError: if the provided argument type is not supported. 

124 """ 

125 if amortization_scheme is None: 

126 return ZeroAmortizationScheme() 

127 elif isinstance(amortization_scheme, str): 

128 return AmortizationScheme._from_string(amortization_scheme.lower()) 

129 elif isinstance(amortization_scheme, AmortizationScheme): 

130 return amortization_scheme 

131 else: 

132 raise ValueError("Invalid amortization scheme provided.") 

133 

134 def set_notional_structure(self, notional, amortization_scheme) -> NotionalStructure: 

135 """Create or validate the notional structure for this instrument. 

136 

137 The function accepts numeric notionals (int/float) and converts them to 

138 a concrete NotionalStructure (constant, linear, or variable) depending 

139 on the provided amortization scheme. If a NotionalStructure instance is 

140 provided it is validated / passed through. 

141 

142 Args: 

143 notional (NotionalStructure | int | float): notional or notional descriptor. 

144 amortization_scheme (AmortizationScheme | None): resolved amortization scheme 

145 that controls which notional structure is appropriate. 

146 

147 Returns: 

148 NotionalStructure: instance representing the instrument notional. 

149 

150 Raises: 

151 ValueError: when inputs cannot be converted into a valid notional structure. 

152 """ 

153 if amortization_scheme is None: 

154 if isinstance(notional, _Union[int, float]): 

155 return ConstNotionalStructure(_check_positivity(notional)) 

156 elif isinstance(notional, NotionalStructure): 

157 return notional 

158 raise ValueError("Invalid notional structure provided.") 

159 elif isinstance(amortization_scheme, ZeroAmortizationScheme): 

160 # accept ints and floats for numeric notional values 

161 if isinstance(notional, (_Union[int, float])): 

162 return ConstNotionalStructure(_check_positivity(notional)) 

163 elif isinstance(notional, ConstNotionalStructure): 

164 return notional 

165 else: 

166 logger.warning("Amortization scheme is Const but notional is not ConstNotionalStructure. Using provided notional structure.") 

167 return notional 

168 elif isinstance(amortization_scheme, LinearAmortizationScheme): 

169 # accept ints and floats for numeric notional values 

170 if isinstance(notional, (_Union[int, float])): 

171 return LinearNotionalStructure(_check_positivity(notional)) 

172 elif isinstance(notional, ConstNotionalStructure): 

173 logger.warning("Amortization scheme is Linear but notional is ConstNotionalStructure. Converting to LinearNotionalStructure.") 

174 return LinearNotionalStructure(notional.get_amount(0)) 

175 elif isinstance(notional, (LinearNotionalStructure, VariableNotionalStructure)): 

176 logger.warning( 

177 "Amortization scheme is Linear but notional is alredy LinearNotionalStructure or VariableNotionalStructure. Using provided notional structure." 

178 ) 

179 return notional 

180 elif isinstance(amortization_scheme, VariableAmortizationScheme): 

181 logger.warning("Variable amortization scheme is not implemented. Retuning notional as is.") 

182 if isinstance(notional, (_Union[int, float])): 

183 return ConstNotionalStructure(_check_positivity(notional)) 

184 else: 

185 return notional 

186 

187 @staticmethod 

188 def _create_sample( 

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

190 ) -> _List[dict]: 

191 """Create a small list of example bond specifications for testing. 

192 

193 This helper generates a list of dictionaries that mimic the kwargs used 

194 to construct bond specifications. It is intended for internal testing 

195 and examples only. 

196 

197 Args: 

198 n_samples (int): Number of sample entries to generate. 

199 seed (int, optional): RNG seed for reproducible samples. 

200 ref_date (date | datetime, optional): Reference date for issue/maturity generation. 

201 issuers (List[str], optional): Optional pool of issuer names to sample from. 

202 sec_levels (List[str], optional): Optional securitization levels to sample from. 

203 currencies (List[str], optional): Optional currencies to sample from. 

204 

205 Returns: 

206 List[dict]: List of parameter dictionaries usable to create bond specs. 

207 """ 

208 if seed is not None: 

209 np.random.seed(seed) 

210 if ref_date is None: 

211 ref_date = datetime.now() 

212 else: 

213 ref_date = _date_to_datetime(ref_date) 

214 if issuers is None: 

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

216 result = [] 

217 if currencies is None: 

218 currencies = list(Currency) 

219 if sec_levels is None: 

220 sec_levels = list(SecuritizationLevel) 

221 for _ in range(n_samples): 

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

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

224 result.append( 

225 { 

226 "issue_date": issue_date, 

227 "maturity_date": ref_date + timedelta(days=days), 

228 "currency": np.random.choice(currencies), 

229 "notional": np.random.choice([100.0, 1000.0, 10_000.0, 100_0000.0]), 

230 "issuer": np.random.choice(issuers), 

231 "securitization_level": np.random.choice(sec_levels), 

232 } 

233 ) 

234 return result 

235 

236 def _validate_derived_issued_instrument(self): 

237 self._issue_date, self._maturity_date = _check_start_before_end(self._issue_date, self._maturity_date) 

238 

239 def _to_dict(self) -> dict: 

240 result = { 

241 "obj_id": self.obj_id, 

242 "issuer": self.issuer, 

243 "securitization_level": self.securitization_level, 

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

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

246 "currency": self.currency, 

247 "notional": self.notional, 

248 "rating": self.rating, 

249 } 

250 return result 

251 

252 # region properties 

253 

254 @property 

255 def issuer(self) -> str: 

256 """ 

257 Getter for instrument's issuer. 

258 

259 Returns: 

260 str: Instrument's issuer. 

261 """ 

262 return self._issuer 

263 

264 @issuer.setter 

265 def issuer(self, issuer: str): 

266 """ 

267 Setter for instrument's issuer. 

268 

269 Args: 

270 issuer(str): Issuer of the instrument. 

271 """ 

272 self._issuer = issuer 

273 

274 @property 

275 def rating(self) -> str: 

276 return self._rating 

277 

278 @rating.setter 

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

280 self._rating = Rating.to_string(rating) 

281 

282 @property 

283 def securitization_level(self) -> str: 

284 """ 

285 Getter for instrument's securitisation level. 

286 

287 Returns: 

288 str: Instrument's securitisation level. 

289 """ 

290 if isinstance(self._securitization_level, SecuritizationLevel): 

291 return self._securitization_level.value 

292 return self._securitization_level 

293 

294 @securitization_level.setter 

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

296 self._securitization_level = SecuritizationLevel.to_string(securitisation_level) 

297 

298 @property 

299 def issue_date(self) -> date: 

300 """ 

301 Getter for bond's issue date. 

302 

303 Returns: 

304 date: Bond's issue date. 

305 """ 

306 return self._issue_date 

307 

308 @issue_date.setter 

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

310 """ 

311 Setter for bond's issue date. 

312 

313 Args: 

314 issue_date (Union[datetime, date]): Bond's issue date. 

315 """ 

316 self._issue_date = _date_to_datetime(issue_date) 

317 

318 @property 

319 def maturity_date(self) -> date: 

320 """ 

321 Getter for bond's maturity date. 

322 

323 Returns: 

324 date: Bond's maturity date. 

325 """ 

326 return self._maturity_date 

327 

328 @maturity_date.setter 

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

330 """ 

331 Setter for bond's maturity date. 

332 

333 Args: 

334 maturity_date (Union[datetime, date]): Bond's maturity date. 

335 """ 

336 self._maturity_date = _date_to_datetime(maturity_date) 

337 

338 @property 

339 def currency(self) -> str: 

340 """ 

341 Getter for bond's currency. 

342 

343 Returns: 

344 str: Bond's ISO 4217 currency code 

345 """ 

346 return self._currency 

347 

348 @currency.setter 

349 def currency(self, currency: str): 

350 self._currency = Currency.to_string(currency) 

351 

352 @property 

353 def notional(self) -> NotionalStructure: 

354 """ 

355 Getter for bond's face value. 

356 

357 Returns: 

358 float: Bond's face value. 

359 """ 

360 return self._notional 

361 

362 @notional.setter 

363 def notional(self, notional): 

364 if isinstance(notional, NotionalStructure): 

365 self._notional = notional 

366 else: 

367 self._notional = ConstNotionalStructure(_check_positivity(notional)) 

368 

369 @property 

370 def day_count_convention(self) -> str: 

371 """ 

372 Getter for instruments's day count convention. 

373 

374 Returns: 

375 str: instruments's day count convention. 

376 """ 

377 return self._day_count_convention 

378 

379 @day_count_convention.setter 

380 def day_count_convention(self, dcc: _Union[DayCounterType, str]): 

381 self._day_count_convention = DayCounterType.to_string(dcc) 

382 

383 @property 

384 def business_day_convention(self) -> str: 

385 """ 

386 Getter for FRA's day count convention. 

387 

388 Returns: 

389 str: FRA's day count convention. 

390 """ 

391 return self._business_day_convention 

392 

393 @business_day_convention.setter 

394 def business_day_convention(self, business_day_convention: _Union[RollConvention, str]): 

395 # business_day_convention represents a RollConvention; normalize accordingly 

396 self._business_day_convention = RollConvention.to_string(business_day_convention) 

397 

398 @property 

399 def roll_convention(self) -> str: 

400 """ 

401 Getter for the roll convention used for business day adjustment. 

402 

403 Returns: 

404 str: The roll convention used for business day adjustment. 

405 """ 

406 return self._roll_convention 

407 

408 @roll_convention.setter 

409 def roll_convention(self, roll_convention: _Union[RollRule, str]): 

410 """ 

411 Setter for the roll convention used for business day adjustment. 

412 

413 Args: 

414 roll_convention (_Union[RollRule, str]): The roll convention used for business day adjustment. 

415 """ 

416 self._roll_convention = RollRule.to_string(roll_convention) 

417 

418 @property 

419 def calendar(self): 

420 """ 

421 Getter for the calendar used for business day adjustment. 

422 

423 Returns: 

424 The calendar used for business day adjustment. 

425 """ 

426 return self._calendar 

427 

428 @calendar.setter 

429 def calendar(self, calendar: _Union[_HolidayBase, str]): 

430 """ 

431 Setter for the calendar used for business day adjustment. 

432 

433 Args: 

434 calendar (_Union[_HolidayBase, str]): The calendar used for business day adjustment. 

435 """ 

436 if isinstance(calendar, str) and calendar.upper() == "TARGET": 

437 self._calendar = _ECB() 

438 else: 

439 self._calendar = _string_to_calendar(calendar) 

440 

441 # endregion 

442 

443 def notional_amount(self, index: _Union[date, datetime, int] = None) -> float: 

444 """Get the notional amount at a specific date. 

445 

446 Args: 

447 index (_Union[date, datetime, int]): The index for which to get the notional amount, may be a date or an integer index. If None, returns the full notional structure. 

448 

449 Returns: 

450 float: The notional amount at the specified index. 

451 """ 

452 if index is not None: 

453 if isinstance(index, int): 

454 return self._notional.get_amount(index) 

455 else: 

456 return self._notional.get_amount_per_date(_date_to_datetime(index)) 

457 else: 

458 return self._notional.get_amount(index) 

459 

460 

461class DeterministicCashflowBondSpecification(BondBaseSpecification): 

462 """Specification for instruments that produce deterministic cashflows. 

463 

464 This class centralizes fields and behaviours common to instruments whose 

465 cashflows can be determined deterministically from the specification 

466 (for example fixed-rate bonds, floating-rate notes and zero-coupon bonds). 

467 

468 Responsibilities 

469 - Hold instrument conventions (frequency, day-count, business-day rules). 

470 - Manage notional / amortization schemes. 

471 - Create and adjust accrual/payment schedules (via :class:`Schedule` / :func:`roll_day`). 

472 

473 Notes 

474 - Subclasses typically call ``super().__init__(...)`` with their specific 

475 defaults (coupon, margin, index, etc.). 

476 - Dates are normalized to datetimes internally; callers can pass 

477 ``datetime`` or ``date`` objects. 

478 """ 

479 

480 def __init__( 

481 self, 

482 obj_id: str, 

483 issue_date: _Union[date, datetime], 

484 maturity_date: _Union[date, datetime], 

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

486 frequency: _Optional[_Union[Period, str]] = None, 

487 issue_price: _Optional[float] = None, 

488 ir_index: _Union[InterestRateIndex, str] = None, 

489 index: _Optional[_Union[InterestRateIndex, str]] = None, 

490 currency: _Union[Currency, str] = Currency.EUR, 

491 notional_exchange: bool = True, 

492 coupon: float = 0.0, 

493 margin: float = 0.0, 

494 amortization_scheme: _Optional[_Union[str, AmortizationScheme]] = None, 

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

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

497 roll_convention: _Union[RollRule, str] = "NONE", 

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

499 coupon_type: str = "fix", 

500 payment_days: int = 0, 

501 spot_days: int = 2, 

502 pays_in_arrears: bool = True, 

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

504 rating: _Union[Rating, str] = "NONE", 

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

506 backwards=True, 

507 stub_type_is_Long=True, 

508 last_fixing: _Optional[float] = None, 

509 fixings: _Optional[FixingTable] = None, 

510 adjust_start_date: bool = True, 

511 adjust_end_date: bool = False, 

512 adjust_schedule: bool = True, 

513 adjust_accruals: bool = True, 

514 ): 

515 """Create a deterministic cashflow bond specification. 

516 

517 Args: 

518 obj_id (str): Unique identifier for the object. 

519 issue_date (date | datetime): Issue date for the instrument. Corresponds to the ``start date´´ of the first accrual period if ``adjust_start_date`` is False. 

520 maturity_date (date | datetime): Maturity date for the instrument. Will be rolled to a business day acc. to business day convention. 

521 The unrolled maturity date corresponds to the ``end date´´ of the last accrual period if ``adjust_end_date`` is False. 

522 notional (NotionalStructure | float, optional): Notional or a notional structure. Defaults to 100.0. 

523 frequency (Period | str, optional): Payment frequency (e.g. '1Y', '6M'). When None, frequency may be derived from an index. 

524 issue_price (float, optional): Issue price for priced instruments. Defaults to None. 

525 ir_index (InterestRateIndex | str, optional): Internal index reference (enum or alias). 

526 index (InterestRateIndex | str, optional): External index alias used for fixings. 

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

528 notional_exchange (bool, optional): If True notional is exchanged at maturity. Defaults to True. 

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

530 margin (float, optional): Floating leg spread (for floaters). Defaults to 0.0. 

531 amortization_scheme (str | AmortizationScheme, optional): Amortization descriptor or object. 

532 day_count_convention (DayCounterType | str, optional): Day count convention. Defaults to 'ACT360'. 

533 business_day_convention (RollConvention | str, optional): Business-day adjustment rule. Defaults to 'ModifiedFollowing'. 

534 roll_convention (RollRule | str, optional): Roll convention for schedule generation. Defaults to 'NONE'. 

535 calendar (HolidayBase | str, optional): Holiday calendar used for adjustments. Defaults to ECB calendar. 

536 coupon_type (str, optional): 'fix'|'float'|'zero'. Defaults to 'fix'. 

537 payment_days (int, optional): Payment lag in days. Defaults to 0. 

538 spot_days (int, optional): Spot settlement days. Defaults to 2. 

539 pays_in_arrears (bool, optional): If True coupon is paid in arrears. Defaults to True. 

540 issuer (Issuer | str, optional): Issuer identifier. Defaults to None. 

541 rating (Rating | str, optional): Issuer or instrument rating. Defaults to 'NONE'. 

542 securitization_level (SecuritizationLevel | str, optional): Securitization level. Defaults to 'NONE'. 

543 backwards (bool, optional): Generate schedule backwards. Defaults to True. 

544 stub_type_is_Long (bool, optional): Use long stub when generating schedule. Defaults to True. 

545 last_fixing (float, optional): Last known fixing. Defaults to None. 

546 fixings (FixingTable, optional): Fixing table for historical fixings. Defaults to None. 

547 adjust_start_date (bool, optional): Adjust the start date to a business day acc. to business day convention. Defaults to True. 

548 adjust_end_date (bool, optional): Adjust the end date to a business day acc. to business day convention. Defaults to False. 

549 adjust_schedule (bool, optional): Adjust generated schedule dates to business days. Defaults to True. 

550 adjust_accruals (bool, optional): Adjust schedule dates to business days. Defaults to True. if ``adjust_schedule`` is True also accrual dates are adjusted. 

551 

552 Raises: 

553 ValueError: on invalid argument combinations or types (validated by :meth:`_validate`). 

554 """ 

555 super().__init__( 

556 obj_id, 

557 issue_date, 

558 _date_to_datetime(maturity_date), 

559 Currency.to_string(currency), 

560 notional, 

561 amortization_scheme, 

562 issuer, 

563 securitization_level, 

564 Rating.to_string(rating), 

565 day_count_convention, 

566 business_day_convention, 

567 roll_convention, 

568 calendar, 

569 ) 

570 if not is_business_day(issue_date, calendar) and adjust_start_date: 

571 self._start_date = roll_day(issue_date, calendar=calendar, business_day_convention=business_day_convention) 

572 else: 

573 self._start_date = issue_date 

574 if not is_business_day(maturity_date, calendar) and adjust_end_date: 

575 self._end_date = roll_day(maturity_date, calendar=calendar, business_day_convention=business_day_convention) 

576 else: 

577 self._end_date = maturity_date 

578 if not is_business_day(maturity_date, calendar): 

579 self._maturity_date = roll_day(maturity_date, calendar=calendar, business_day_convention=business_day_convention) 

580 

581 if issue_price is not None: 

582 self._issue_price = _check_non_negativity(issue_price) 

583 else: 

584 self._issue_price = None 

585 self._coupon = coupon 

586 self._margin = margin 

587 self._frequency = frequency 

588 self._ir_index = ir_index 

589 self._index = index 

590 self._coupon_type = coupon_type 

591 self._notional_exchange = notional_exchange 

592 self._payment_days = payment_days 

593 self._spot_days = spot_days 

594 self._pays_in_arrears = pays_in_arrears 

595 self._backwards = backwards 

596 self._stub_type_is_Long = stub_type_is_Long 

597 self._last_fixing = last_fixing 

598 self._fixings = fixings 

599 self._schedule = None 

600 self._nr_annual_payments = None 

601 self._dates = None 

602 self._accrual_dates = None 

603 self._adjust_start_date = adjust_start_date 

604 self._adjust_end_date = adjust_end_date 

605 self._adjust_schedule = adjust_schedule 

606 self._adjust_accruals = adjust_accruals 

607 self._validate() 

608 

609 # region properties 

610 

611 @property 

612 def coupon(self) -> float: 

613 """ 

614 Getter for instrument's coupon. 

615 

616 Returns: 

617 float: Instrument's coupon. 

618 """ 

619 return self._coupon 

620 

621 @coupon.setter 

622 def coupon(self, rate: float): 

623 """ 

624 Setter for instrument's rate. 

625 

626 Args: 

627 rate(float): interest rate of the instrument. 

628 """ 

629 self._rate = rate 

630 

631 @property 

632 def start_date(self) -> datetime.date: 

633 """ 

634 Getter for deposit's start date. 

635 

636 Returns: 

637 date: deposit's start date. 

638 """ 

639 return self._start_date 

640 

641 @start_date.setter 

642 def start_date(self, start_date: _Union[date, datetime]): 

643 """ 

644 Setter for deposit's start date. 

645 

646 Args: 

647 start_date (Union[datetime, date]): deposit's start date. 

648 """ 

649 self._start_date = _date_to_datetime(start_date) 

650 

651 @property 

652 def end_date(self) -> datetime: 

653 """ 

654 Getter for deposit's end date. 

655 

656 Returns: 

657 date: deposit's end date. 

658 """ 

659 return self._end_date 

660 

661 @end_date.setter 

662 def end_date(self, end_date: _Union[date, datetime]): 

663 """ 

664 Setter for deposit's end date. 

665 

666 Args: 

667 end_date (Union[datetime, date]): deposit's end date. 

668 """ 

669 if not isinstance(end_date, (date, datetime)): 

670 raise TypeError("end_date must be a datetime or date object.") 

671 self._end_date = _date_to_datetime(end_date) 

672 

673 @property 

674 def frequency(self) -> Period: 

675 """ 

676 Getter for instrument's payment frequency. 

677 

678 Returns: 

679 Period: instrument's payment frequency. 

680 """ 

681 return self._frequency 

682 

683 @frequency.setter 

684 def frequency(self, frequency: _Union[Period, str]): 

685 """ 

686 Setter for instrument's payment frequency. 

687 

688 Args: 

689 frequency (Union[Period, str]): instrument's payment frequency. 

690 """ 

691 self._frequency = _term_to_period(frequency) 

692 

693 @property 

694 def issue_price(self) -> _Optional[float]: 

695 """The bond's issue price as a float.""" 

696 return getattr(self, "_issue_price", None) 

697 

698 @issue_price.setter 

699 def issue_price(self, issue_price: _Union[float, str]): 

700 self._issue_price = _check_non_negativity(issue_price) 

701 

702 @property 

703 def notional_exchange(self): 

704 return self._notional_exchange 

705 

706 @notional_exchange.setter 

707 def notional_exchange(self, notional_exchange: bool): 

708 self._notional_exchange = notional_exchange 

709 

710 @property 

711 def payment_days(self) -> int: 

712 """ 

713 Getter for the number of payment days. 

714 

715 Returns: 

716 int: Number of payment days. 

717 """ 

718 return self._payment_days 

719 

720 @payment_days.setter 

721 def payment_days(self, payment_days: int): 

722 """ 

723 Setter for the number of payment days. 

724 

725 Args: 

726 payment_days (int): Number of payment days. 

727 """ 

728 if not isinstance(payment_days, int) or payment_days < 0: 

729 raise ValueError("payment days must be a non-negative integer.") 

730 self._payment_days = payment_days 

731 

732 @property 

733 def pays_in_arrears(self) -> bool: 

734 """ 

735 Getter for the pays_in_arrears flag. 

736 

737 Returns: 

738 bool: True if the instrument pays in arrears, False otherwise. 

739 """ 

740 return self._pays_in_arrears 

741 

742 @pays_in_arrears.setter 

743 def pays_in_arrears(self, pays_in_arrears: bool): 

744 """ 

745 Setter for the pays_in_arrears flag. 

746 

747 Args: 

748 pays_in_arrears (bool): True if the instrument pays in arrears, False otherwise. 

749 """ 

750 if not isinstance(pays_in_arrears, bool): 

751 raise ValueError("pays_in_arrears must be a boolean value.") 

752 self._pays_in_arrears = pays_in_arrears 

753 

754 @property 

755 def coupon_type(self) -> str: 

756 """ 

757 Getter for the coupon type of the instrument. 

758 

759 Returns: 

760 str: The coupon type of the instrument. 

761 """ 

762 return self._coupon_type 

763 

764 @coupon_type.setter 

765 def coupon_type(self, coupon_type: str): 

766 """ 

767 Setter for the coupon type of the instrument. 

768 

769 Args: 

770 coupon_type (str): The coupon type of the instrument. 

771 """ 

772 if not isinstance(coupon_type, str): 

773 raise ValueError("Coupon type must be a string.") 

774 self._coupon_type = coupon_type 

775 

776 @property 

777 def backwards(self) -> bool: 

778 """ 

779 Getter for the backwards flag. 

780 

781 Returns: 

782 bool: True if the schedule is generated backwards, False otherwise. 

783 """ 

784 return self._backwards 

785 

786 @backwards.setter 

787 def backwards(self, backwards: bool): 

788 """ 

789 Setter for the backwards flag. 

790 

791 Args: 

792 backwards (bool): True if the schedule is generated backwards, False otherwise. 

793 """ 

794 if not isinstance(backwards, bool): 

795 raise ValueError("Backwards must be a boolean value.") 

796 self._backwards = backwards 

797 

798 @property 

799 def stub_type_is_Long(self) -> bool: 

800 """ 

801 Getter for the stub type flag. 

802 

803 Returns: 

804 bool: True if the stub type is long, False otherwise. 

805 """ 

806 return self._stub_type_is_Long 

807 

808 @stub_type_is_Long.setter 

809 def stub_type_is_Long(self, stub_type_is_Long: bool): 

810 """ 

811 Setter for the stub type flag. 

812 

813 Args: 

814 stub_type_is_Long (bool): True if the stub type is long, False otherwise. 

815 """ 

816 if not isinstance(stub_type_is_Long, bool): 

817 raise ValueError("Stub type must be a boolean value.") 

818 self._stub_type_is_Long = stub_type_is_Long 

819 

820 @property 

821 def spot_days(self) -> int: 

822 """ 

823 Getter for the number of spot days. 

824 

825 Returns: 

826 int: Number of spot days. 

827 """ 

828 return self._spot_days 

829 

830 @spot_days.setter 

831 def spot_days(self, spot_days: int): 

832 """ 

833 Setter for the number of spot days. 

834 

835 Args: 

836 spot_days (int): Number of spot days. 

837 """ 

838 if not isinstance(spot_days, int) or spot_days < 0: 

839 raise ValueError("Spot days must be a non-negative integer.") 

840 self._spot_days = spot_days 

841 

842 @property 

843 def last_fixing(self) -> _Optional[float]: 

844 """ 

845 Getter for the last fixing value. 

846 

847 Returns: 

848 _Optional[float]: The last fixing value, or None if not set. 

849 """ 

850 return self._last_fixing 

851 

852 @last_fixing.setter 

853 def last_fixing(self, last_fixing: _Optional[float]): 

854 """ 

855 Setter for the last fixing value. 

856 

857 Args: 

858 last_fixing (_Optional[float]): The last fixing value, or None if not set. 

859 """ 

860 if last_fixing is not None and not isinstance(last_fixing, (float, int)): 

861 raise ValueError("Last fixing must be a float or None.") 

862 self._last_fixing = float(last_fixing) if last_fixing is not None else None 

863 

864 @property 

865 def nr_annual_payments(self) -> _Optional[float]: 

866 """ 

867 Getter for the number of annual payments. 

868 

869 Returns: 

870 _Optional[float]: The number of annual payments, or None if frequency is not set. 

871 """ 

872 if self._nr_annual_payments is None: 

873 self._nr_annual_payments = self.get_nr_annual_payments() 

874 return self._nr_annual_payments 

875 

876 @nr_annual_payments.setter 

877 def nr_annual_payments(self, value: _Optional[float]): 

878 """ 

879 Setter for the number of annual payments. 

880 

881 Args: 

882 value (_Optional[float]): The number of annual payments, or None if frequency is not set. 

883 """ 

884 if value is not None and (not isinstance(value, (float, int)) or value <= 0): 

885 raise ValueError("Number of annual payments must be a positive float or None.") 

886 self._nr_annual_payments = float(value) if value is not None else None 

887 

888 @property 

889 def schedule(self) -> Schedule: 

890 """ 

891 Getter for the dates of the instrument. 

892 

893 Returns: 

894 Schedule: The schedule of the instrument. 

895 """ 

896 return self.get_schedule() 

897 

898 @schedule.setter 

899 def schedule(self, schedule: Schedule): 

900 """ 

901 Setter for the dates of the instrument. 

902 

903 Args: 

904 schedule (Schedule): The schedule of the instrument. 

905 """ 

906 if not isinstance(schedule, Schedule): 

907 raise ValueError("Schedule must be a Schedule object.") 

908 self._schedule = schedule 

909 

910 @property 

911 def dates(self) -> _List[datetime]: 

912 """ 

913 Getter for the dates of the instrument that mark start and end dates of the accrual periods. 

914 

915 Returns: 

916 _List[datetime]: The dates of the instrument. 

917 """ 

918 if self._dates is None: 

919 # Try to get schedule, fallback to empty list if not possible 

920 try: 

921 schedule = self._schedule if self._schedule is not None else self.get_schedule() 

922 if schedule is not None: 

923 if self._adjust_schedule == False: 

924 self._dates = schedule._roll_out( 

925 from_=self._start_date if not self._backwards else self._end_date, 

926 to_=self._end_date if not self._backwards else self._start_date, 

927 term=_term_to_period(self._frequency), 

928 long_stub=self._stub_type_is_Long, 

929 backwards=self._backwards, 

930 roll_convention_=self._roll_convention, 

931 ) 

932 else: 

933 self._dates = schedule.generate_dates(False) 

934 if self._adjust_accruals: 

935 rolled = [roll_day(d, self._calendar, self._business_day_convention) for d in self._dates] 

936 else: 

937 rolled = self._dates 

938 self._accrual_dates = rolled 

939 if isinstance(self._notional, LinearNotionalStructure): 

940 self._notional.n_steps = len(self._dates) 

941 self._notional._notional = list( 

942 np.linspace(self._notional.start_notional, self._notional.end_notional, self._notional.n_steps) 

943 ) 

944 self._notional.start_date = rolled[:-1] 

945 self._notional.end_date = rolled[1:] 

946 else: 

947 self._dates = [] 

948 self._accrual_dates = [] 

949 except Exception as e: 

950 # Optionally log the error here 

951 self._dates = [] 

952 return self._dates if self._dates is not None else [] 

953 

954 @dates.setter 

955 def dates(self, dates: _List[datetime]): 

956 """ 

957 Setter for the dates of the instrument that mark start and end dates of the accrual periods. 

958 

959 Args: 

960 dates (_List[datetime]): The dates of the instrument. 

961 """ 

962 if not _is_ascending_date_list(dates): 

963 raise ValueError("Dates must be a list of ascending datetime objects.") 

964 self._dates = dates 

965 

966 @property 

967 def accrual_dates(self) -> _List[datetime]: 

968 """ 

969 Getter for the accrual dates of the instrument that mark start and end dates of the accrual periods. 

970 

971 Returns: 

972 _List[datetime]: The accrual dates of the instrument. 

973 """ 

974 if self._accrual_dates is None: 

975 _ = self.dates # Trigger dates property to populate accrual_dates 

976 return self._accrual_dates if self._accrual_dates is not None else [] 

977 

978 @accrual_dates.setter 

979 def accrual_dates(self, accrual_dates: _List[datetime]): 

980 """ 

981 Setter for the accrual dates of the instrument that mark start and end dates of the accrual periods. 

982 

983 Args: 

984 accrual_dates (_List[datetime]): The accrual dates of the instrument. 

985 """ 

986 if not _is_ascending_date_list(accrual_dates): 

987 raise ValueError("Accrual dates must be a list of ascending datetime objects.") 

988 self._accrual_dates = accrual_dates 

989 

990 @property 

991 def index(self) -> float: 

992 """ 

993 Getter for instrument's index. 

994 

995 Returns: 

996 float: Instrument's index. 

997 """ 

998 return self._index 

999 

1000 @index.setter 

1001 def index(self, index: _Union[InterestRateIndex, str]): 

1002 """ 

1003 Setter for instrument's index. 

1004 

1005 Args: 

1006 index (_Union[InterestRateIndex, str]): instrument's index. 

1007 """ 

1008 self._index = index 

1009 self._ir_index = index if isinstance(index, InterestRateIndex) else get_index_by_alias(index) 

1010 self._frequency = self._ir_index.value.tenor 

1011 

1012 @property 

1013 def ir_index(self) -> InterestRateIndex: 

1014 """ 

1015 Getter for instrument's interest rate index. 

1016 

1017 Returns: 

1018 InterestRateIndex: Instrument's interest rate index. 

1019 """ 

1020 return self._ir_index 

1021 

1022 @ir_index.setter 

1023 def ir_index(self, ir_index: InterestRateIndex): 

1024 """ 

1025 Setter for instrument's interest rate index. 

1026 

1027 Args: 

1028 ir_index (InterestRateIndex): Instrument's interest rate index. 

1029 """ 

1030 self._ir_index = ir_index 

1031 

1032 @property 

1033 def adjust_start_date(self) -> bool: 

1034 return self._adjust_start_date 

1035 

1036 @adjust_start_date.setter 

1037 def adjust_start_date(self, value: bool): 

1038 self._adjust_start_date = value 

1039 if not is_business_day(self._issue_date, self._calendar) and self._adjust_start_date: 

1040 # fix typo: use _issue_date (datetime) not _issuedate 

1041 self._start_date = roll_day(self._issue_date, calendar=self._calendar, business_day_convention=self._business_day_convention) 

1042 

1043 @property 

1044 def adjust_end_date(self) -> bool: 

1045 return self._adjust_end_date 

1046 

1047 @adjust_end_date.setter 

1048 def adjust_end_date(self, value: bool): 

1049 self._adjust_end_date = value 

1050 if not is_business_day(self._maturity_date, self._calendar) and self._adjust_end_date: 

1051 self._end_date = roll_day(self._maturity_date, calendar=self._calendar, business_day_convention=self._business_day_convention) 

1052 

1053 @property 

1054 def adjust_schedule(self) -> bool: 

1055 return self._adjust_schedule 

1056 

1057 @adjust_schedule.setter 

1058 def adjust_schedule(self, value: bool): 

1059 self._adjust_schedule = value 

1060 

1061 @property 

1062 def adjust_accruals(self) -> bool: 

1063 return self._adjust_accruals 

1064 

1065 @adjust_accruals.setter 

1066 def adjust_accruals(self, value: bool): 

1067 self._adjust_accruals = value 

1068 

1069 # endregion 

1070 

1071 def _validate(self): 

1072 """Validates the parameters of the instrument.""" 

1073 _check_start_before_end(self._start_date, self._end_date) 

1074 # _check_start_at_or_before_end(self._end_date, self._maturity_date) # TODO special case modified following BCC 

1075 _check_non_negativity(self._payment_days) 

1076 _check_non_negativity(self._spot_days) 

1077 if not isinstance(self._calendar, (_HolidayBase, str)): 

1078 raise ValueError("Calendar must be a HolidayBase or string.") 

1079 

1080 def get_schedule(self) -> Schedule: 

1081 """Return a configured :class:`Schedule` for the instrument. 

1082 

1083 The returned Schedule is constructed from the instrument's start/end 

1084 dates, frequency/tenor, stub and roll conventions and calendar. 

1085 

1086 Returns: 

1087 Schedule: schedule object configured for this instrument. 

1088 """ 

1089 return Schedule( 

1090 start_day=self._start_date, 

1091 end_day=self._end_date, 

1092 time_period=_string_to_period(self._frequency), 

1093 backwards=self._backwards, 

1094 stub_type_is_Long=self._stub_type_is_Long, 

1095 business_day_convention=self._business_day_convention, 

1096 roll_convention=self._roll_convention, 

1097 calendar=self._calendar, 

1098 ) 

1099 

1100 def get_nr_annual_payments(self) -> float: 

1101 """Compute the (approximate) number of annual payments implied by frequency. 

1102 

1103 Returns: 

1104 float: number of payments per year implied by the frequency. If 

1105 frequency is not set 0.0 is returned. 

1106 

1107 Raises: 

1108 ValueError: if the frequency resolves to a non-positive period. 

1109 """ 

1110 if self._frequency is None: 

1111 logger.warning("Frequency is not set. Returning 0.") 

1112 return 0.0 

1113 freq = _string_to_period(self._frequency) 

1114 if freq.years > 0 or freq.months > 0 or freq.days > 0: 

1115 nr = 12.0 / (freq.years * 12 + freq.months + freq.days * 12 / 365.0) 

1116 else: 

1117 raise ValueError("Frequency must be positive.") 

1118 if nr.is_integer() is False: 

1119 logger.warning("Number of annual payments is not a whole number but a decimal.") 

1120 return nr 

1121 

1122 @abc.abstractmethod 

1123 def _to_dict(self) -> dict: 

1124 pass 

1125 

1126 

1127class FixedRateBondSpecification(DeterministicCashflowBondSpecification): 

1128 """Specification for fixed-rate bonds. 

1129 

1130 Stores coupon, frequency and other fixed-rate specific settings and 

1131 delegates schedule construction to the base class behaviour. 

1132 """ 

1133 

1134 def __init__( 

1135 self, 

1136 obj_id: str, 

1137 notional: _Union[NotionalStructure, float], 

1138 currency: _Union[Currency, str], 

1139 issue_date: _Union[date, datetime], 

1140 maturity_date: _Union[date, datetime], 

1141 coupon: float, 

1142 frequency: _Union[Period, str], 

1143 amortization_scheme: _Optional[_Union[str, AmortizationScheme]] = None, 

1144 business_day_convention: RollConvention = "ModifiedFollowing", 

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

1146 securitization_level: _Optional[_Union[SecuritizationLevel, str]] = "NONE", 

1147 rating: _Optional[_Union[Rating, str]] = "NONE", 

1148 day_count_convention: _Union[DayCounterType, str] = "ActActICMA", 

1149 spot_days: int = 2, 

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

1151 stub_type_is_Long: bool = True, 

1152 adjust_start_date: bool = True, 

1153 adjust_end_date: bool = False, 

1154 ): 

1155 """Create a fixed-rate bond specification. 

1156 

1157 Args: 

1158 obj_id (str): Unique identifier for the bond. 

1159 notional (NotionalStructure | float): Notional or notional structure. 

1160 currency (Currency | str): Currency code or enum. 

1161 issue_date (date | datetime): Issue date of the bond. 

1162 maturity_date (date | datetime): Maturity date of the bond. 

1163 coupon (float): Fixed coupon rate (decimal, e.g. 0.03 for 3%). 

1164 frequency (Period | str): Payment frequency (tenor) for coupons. 

1165 amortization_scheme (str | AmortizationScheme, optional): Amortization descriptor or object. 

1166 business_day_convention (RollConvention | str, optional): Business day convention used for schedule adjustments. 

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

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

1169 rating (Rating | str, optional): Instrument rating. 

1170 day_count_convention (DayCounterType | str, optional): Day count convention for accruals. 

1171 spot_days (int, optional): Spot settlement days. Defaults to 2. 

1172 calendar (HolidayBase | str, optional): Calendar used for business-day adjustments. 

1173 stub_type_is_Long (bool, optional): Use long stub when generating schedule. Defaults to True. 

1174 adjust_start_date (bool, optional): Adjust start date to business day. Defaults to True. 

1175 adjust_end_date (bool, optional): Adjust end date to business day. Defaults to False. 

1176 """ 

1177 super().__init__( 

1178 obj_id=obj_id, 

1179 spot_days=spot_days, 

1180 issue_date=issue_date, 

1181 maturity_date=maturity_date, 

1182 notional=notional, 

1183 amortization_scheme=amortization_scheme, 

1184 currency=currency, 

1185 coupon=coupon, 

1186 coupon_type="fix", 

1187 frequency=frequency, 

1188 day_count_convention=day_count_convention, 

1189 business_day_convention=business_day_convention, 

1190 payment_days=0, 

1191 stub_type_is_Long=stub_type_is_Long, 

1192 issuer=issuer, 

1193 rating=rating, 

1194 securitization_level=securitization_level, 

1195 calendar=calendar, 

1196 adjust_start_date=adjust_start_date, 

1197 adjust_end_date=adjust_end_date, 

1198 ) 

1199 

1200 @staticmethod 

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

1202 """Return a list of example FixedRateBondSpecification instances. 

1203 

1204 Args: 

1205 n_samples (int): Number of sample instances to create. 

1206 seed (int, optional): RNG seed for reproducibility. 

1207 

1208 Returns: 

1209 List[FixedRateBondSpecification]: Example fixed-rate bonds. 

1210 """ 

1211 result = [] 

1212 if seed is not None: 

1213 np.random.seed(seed) 

1214 

1215 issue_date = datetime(2025, 1, 1) 

1216 maturity_date = datetime(2027, 1, 1) 

1217 notional = 100.0 

1218 currency = Currency.EUR 

1219 securitization_level = SecuritizationLevel.SUBORDINATED 

1220 daycounter = DayCounterType.ACT_ACT 

1221 for i in range(n_samples): 

1222 coupon = np.random.choice([0.0, 0.01, 0.03, 0.05]) 

1223 period = np.random.choice(["1Y", "6M", "3M"]) 

1224 result.append( 

1225 FixedRateBondSpecification( 

1226 obj_id=f"ID_{i}", 

1227 notional=notional, 

1228 frequency=period, 

1229 currency=currency, 

1230 issue_date=issue_date, 

1231 maturity_date=maturity_date, 

1232 coupon=coupon, 

1233 securitization_level=securitization_level, 

1234 day_count_convention=daycounter, 

1235 ) 

1236 ) 

1237 return result 

1238 

1239 def _to_dict(self) -> Dict: 

1240 """Serialize the fixed-rate bond specification to a dictionary. 

1241 

1242 Returns: 

1243 Dict: JSON-serializable representation of the specification. 

1244 """ 

1245 dict = { 

1246 "obj_id": self.obj_id, 

1247 "issuer": self._issuer, 

1248 "securitization_level": self._securitization_level, 

1249 "issue_date": serialize_date(self._issue_date), 

1250 "maturity_date": serialize_date(self._maturity_date), 

1251 "currency": self._currency, 

1252 "notional": self._notional, 

1253 "rating": self._rating, 

1254 "frequency": self._frequency, 

1255 "day_count_convention": self._day_count_convention, 

1256 "business_day_convention": self._business_day_convention, 

1257 "coupon": self._coupon, 

1258 "spot_days": self._spot_days, 

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

1260 "adjust_start_date": self._adjust_start_date, 

1261 "adjust_end_date": self._adjust_end_date, 

1262 } 

1263 return dict 

1264 

1265 

1266class ZeroBondSpecification(DeterministicCashflowBondSpecification): 

1267 """Specification for zero-coupon bonds. 

1268 

1269 Zero bonds have a single payout at maturity; this class wires the base 

1270 behaviour to use coupon_type 'zero' and appropriate notional handling. 

1271 """ 

1272 

1273 def __init__( 

1274 self, 

1275 obj_id: str, 

1276 notional: float, 

1277 currency: _Union[Currency, str], 

1278 issue_date: _Union[date, datetime], 

1279 maturity_date: _Union[date, datetime], 

1280 amortization_scheme: _Optional[_Union[str, AmortizationScheme]] = None, 

1281 issue_price: float = 100.0, 

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

1283 business_day_convention: RollConvention = "ModifiedFollowing", 

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

1285 securitization_level: _Optional[_Union[SecuritizationLevel, str]] = "NONE", 

1286 rating: _Optional[_Union[Rating, str]] = "NONE", 

1287 adjust_start_date: bool = True, 

1288 adjust_end_date: bool = True, 

1289 ): 

1290 """Create a zero-coupon bond specification. 

1291 

1292 Args: 

1293 obj_id (str): Unique identifier for the bond. 

1294 notional (float): Notional amount. 

1295 currency (Currency | str): Currency code or enum. 

1296 issue_date (date | datetime): Issue date. 

1297 maturity_date (date | datetime): Maturity date. 

1298 amortization_scheme (str | AmortizationScheme, optional): Amortization descriptor or object. 

1299 issue_price (float, optional): Issue price. Defaults to 100.0. 

1300 calendar (HolidayBase | str, optional): Holiday calendar used for adjustments. 

1301 business_day_convention (RollConvention | str, optional): Business-day adjustment convention. 

1302 issuer (Issuer | str, optional): Issuer id. 

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

1304 rating (Rating | str, optional): Instrument rating. 

1305 adjust_start_date (bool, optional): Adjust start date to business day. Defaults to True. 

1306 adjust_end_date (bool, optional): Adjust end date to business day. Defaults to True. 

1307 """ 

1308 if not is_business_day(maturity_date, calendar): 

1309 maturity_date = roll_day(maturity_date, calendar=calendar, business_day_convention=business_day_convention) 

1310 super().__init__( 

1311 obj_id=obj_id, 

1312 issue_date=issue_date, 

1313 maturity_date=maturity_date, 

1314 notional=notional, 

1315 amortization_scheme=amortization_scheme, 

1316 issue_price=issue_price, 

1317 currency=currency, 

1318 business_day_convention=business_day_convention, 

1319 coupon_type="zero", 

1320 issuer=issuer, 

1321 rating=rating, 

1322 securitization_level=securitization_level, 

1323 calendar=calendar, 

1324 adjust_start_date=adjust_start_date, 

1325 adjust_end_date=adjust_end_date, 

1326 ) 

1327 

1328 @staticmethod 

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

1330 """Return a list of example ZeroBondSpecification instances. 

1331 

1332 Args: 

1333 n_samples (int): Number of sample instances to create. 

1334 seed (int, optional): RNG seed for reproducibility. 

1335 

1336 Returns: 

1337 List[ZeroBondSpecification]: Example zero-coupon bonds. 

1338 """ 

1339 result = [] 

1340 if seed is not None: 

1341 np.random.seed(seed) 

1342 issue_date = datetime(2025, 1, 1) 

1343 maturity_date = datetime(2027, 1, 1) 

1344 notional = 100.0 

1345 currency = Currency.EUR 

1346 securitization_level = SecuritizationLevel.SUBORDINATED 

1347 for i in range(n_samples): 

1348 issue_price = np.random.choice([90.0, 95.0, 99.0]) 

1349 m = np.random.choice([0, 12, 6, 3]) 

1350 result.append( 

1351 ZeroBondSpecification( 

1352 obj_id=f"ID_{i}", 

1353 notional=notional, 

1354 issue_price=issue_price, 

1355 currency=currency, 

1356 issue_date=issue_date, 

1357 maturity_date=maturity_date + relativedelta(months=m), 

1358 securitization_level=securitization_level, 

1359 ) 

1360 ) 

1361 return result 

1362 

1363 def _to_dict(self) -> Dict: 

1364 """Serialize the zero-coupon bond specification to a dictionary. 

1365 

1366 Returns: 

1367 Dict: JSON-serializable representation of the specification. 

1368 """ 

1369 dict = { 

1370 "obj_id": self.obj_id, 

1371 "issuer": self._issuer, 

1372 "securitization_level": self._securitization_level, 

1373 "issue_date": serialize_date(self._issue_date), 

1374 "maturity_date": serialize_date(self._maturity_date), 

1375 "currency": self._currency, 

1376 "notional": self._notional, 

1377 "issue_price": self._issue_price, 

1378 "rating": self._rating, 

1379 "business_day_convention": self._business_day_convention, 

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

1381 } 

1382 return dict 

1383 

1384 

1385class FloatingRateBondSpecification(DeterministicCashflowBondSpecification): 

1386 """Specification for floating-rate bonds. 

1387 

1388 Supports providing the floating index via enum, string alias or explicit 

1389 index object. The class derives payment frequency from the index when 

1390 available and wires fixings/first fixing date handling used by the pricer. 

1391 """ 

1392 

1393 def __init__( 

1394 self, 

1395 obj_id: str, 

1396 notional: _Union[NotionalStructure, float], 

1397 currency: _Union[Currency, str], 

1398 issue_date: _Union[date, datetime], 

1399 maturity_date: _Union[date, datetime], 

1400 margin: float, 

1401 frequency: _Optional[_Union[Period, str]] = None, 

1402 amortization_scheme: _Optional[_Union[str, AmortizationScheme]] = None, 

1403 index: _Optional[_Union[InterestRateIndex, str]] = None, 

1404 business_day_convention: _Optional[RollConvention] = None, 

1405 day_count_convention: _Optional[DayCounterType] = None, 

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

1407 securitization_level: _Optional[_Union[SecuritizationLevel, str]] = "NONE", 

1408 rating: _Optional[_Union[Rating, str]] = "NONE", 

1409 fixings: _Optional[FixingTable] = None, 

1410 spot_days: int = 2, 

1411 calendar: _Optional[_Union[_HolidayBase, str]] = None, 

1412 stub_type_is_Long: bool = True, 

1413 adjust_start_date: bool = True, 

1414 adjust_end_date: bool = False, 

1415 adjust_schedule: bool = False, 

1416 adjust_accruals: bool = True, 

1417 ): 

1418 """Create a floating-rate bond specification. 

1419 

1420 Either ``index`` or ``frequency`` must be provided. If ``index`` is 

1421 supplied and contains convention information, frequency, calendar and 

1422 day-count are inferred from it unless explicitly overridden. 

1423 

1424 Args: 

1425 obj_id (str): Unique identifier for the bond. 

1426 notional (NotionalStructure | float): Notional or notional structure. 

1427 currency (Currency | str): Currency code or enum. 

1428 issue_date (date | datetime): Issue date. 

1429 maturity_date (date | datetime): Maturity date. 

1430 margin (float): Spread added to the floating index (decimal). 

1431 frequency (Period | str, optional): Payment frequency. May be inferred from index. 

1432 amortization_scheme (str | AmortizationScheme, optional): Amortization descriptor or object. 

1433 index (InterestRateIndex | str, optional): Index alias or enum used for fixings. 

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

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

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

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

1438 rating (Rating | str, optional): Instrument rating. 

1439 fixings (FixingTable, optional): Fixing table for historical fixings. 

1440 spot_days (int, optional): Spot settlement days. Defaults to 2. 

1441 calendar (HolidayBase | str, optional): Holiday calendar. May be inferred from index. 

1442 stub_type_is_Long (bool, optional): Use long stub when generating schedule. Defaults to True. 

1443 adjust_start_date (bool, optional): Adjust start date to business day. Defaults to True. 

1444 adjust_end_date (bool, optional): Adjust end date to business day. Defaults to False. 

1445 adjust_schedule (bool, optional): Adjust schedule dates to business days. Defaults to False. 

1446 adjust_accruals (bool, optional): Adjust accrual dates to business days. Defaults to True. 

1447 

1448 Raises: 

1449 ValueError: If neither index nor frequency is provided. 

1450 """ 

1451 

1452 if index is None and frequency is None: 

1453 raise ValueError("Either index or frequency must be provided for a floating rate bond.") 

1454 elif index is not None: 

1455 if isinstance(index, str): 

1456 # get_index_by_alias will raise if alias unknown 

1457 ir_index = get_index_by_alias(index) 

1458 else: 

1459 ir_index = index 

1460 # if not explicitly provided, extract conventions from index 

1461 if business_day_convention is None: 

1462 business_day_convention = ir_index.value.business_day_convention 

1463 if day_count_convention is None: 

1464 day_count_convention = ir_index.value.day_count_convention 

1465 if frequency is None: 

1466 frequency = ir_index.value.tenor 

1467 if calendar is None: 

1468 if ir_index.value.calendar.upper() == "TARGET": 

1469 calendar = _ECB() 

1470 else: 

1471 calendar = ir_index.value.calendar 

1472 else: 

1473 # no index info given, rely on provided frequency or use default conventions 

1474 frequency = frequency 

1475 ir_index = None 

1476 business_day_convention = "ModifiedFollowing" if business_day_convention is None else business_day_convention 

1477 day_count_convention = "ACT360" if day_count_convention is None else day_count_convention 

1478 calendar = calendar if calendar is not None else _ECB() 

1479 

1480 super().__init__( 

1481 obj_id=obj_id, 

1482 fixings=fixings, 

1483 spot_days=spot_days, 

1484 issue_date=issue_date, 

1485 maturity_date=maturity_date, 

1486 notional=notional, 

1487 amortization_scheme=amortization_scheme, 

1488 currency=currency, 

1489 margin=margin, 

1490 coupon_type="float", 

1491 frequency=frequency, 

1492 index=index, 

1493 ir_index=ir_index, 

1494 day_count_convention=day_count_convention, 

1495 business_day_convention=business_day_convention, 

1496 notional_exchange=True, 

1497 payment_days=0, 

1498 stub_type_is_Long=stub_type_is_Long, 

1499 issuer=issuer, 

1500 rating=rating, 

1501 securitization_level=securitization_level, 

1502 calendar=calendar, 

1503 adjust_start_date=adjust_start_date, 

1504 adjust_end_date=adjust_end_date, 

1505 adjust_schedule=adjust_schedule, 

1506 adjust_accruals=adjust_accruals, 

1507 ) 

1508 

1509 @staticmethod 

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

1511 """Return a list of example FloatingRateBondSpecification instances. 

1512 

1513 Args: 

1514 n_samples (int): Number of sample instances to create. 

1515 seed (int, optional): RNG seed for reproducibility. 

1516 

1517 Returns: 

1518 List[FloatingRateBondSpecification]: Example floating-rate bonds. 

1519 """ 

1520 result = [] 

1521 if seed is not None: 

1522 np.random.seed(seed) 

1523 

1524 issue_date = datetime(2025, 1, 1) 

1525 maturity_date = datetime(2027, 1, 1) 

1526 notional = 100.0 

1527 currency = Currency.EUR 

1528 fixings = FixingTable() 

1529 securitization_level = SecuritizationLevel.SUBORDINATED 

1530 daycounter = "ACT_ACT" 

1531 for i in range(n_samples): 

1532 margin = np.random.choice([0.0, 1, 3, 5]) 

1533 period = np.random.choice(["1Y", "6M", "3M"]) 

1534 result.append( 

1535 FloatingRateBondSpecification( 

1536 obj_id=f"ID_{i}", 

1537 notional=notional, 

1538 frequency=period, 

1539 currency=currency, 

1540 issue_date=issue_date, 

1541 maturity_date=maturity_date, 

1542 margin=margin, 

1543 securitization_level=securitization_level, 

1544 day_count_convention=daycounter, 

1545 fixings=fixings, 

1546 ) 

1547 ) 

1548 return result 

1549 

1550 def _to_dict(self) -> Dict: 

1551 """Serialize the floating-rate bond specification to a dictionary. 

1552 

1553 Returns: 

1554 Dict: JSON-serializable representation of the specification. 

1555 """ 

1556 dict = { 

1557 "obj_id": self.obj_id, 

1558 "issuer": self._issuer, 

1559 "securitization_level": self._securitization_level, 

1560 "issue_date": serialize_date(self._issue_date), 

1561 "maturity_date": serialize_date(self._maturity_date), 

1562 "currency": self._currency, 

1563 "notional": self._notional, 

1564 "rating": self._rating, 

1565 "frequency": self._frequency, 

1566 "day_count_convention": self._day_count_convention, 

1567 "business_day_convention": self._business_day_convention, 

1568 "fixings": self._fixings._to_dict() if isinstance(self._fixings, FixingTable) else self._fixings, 

1569 "ir_index": self._ir_index, 

1570 "index": self._index, 

1571 "margin": self._margin, 

1572 "spot_days": self._spot_days, 

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

1574 "adjust_start_date": self._adjust_start_date, 

1575 "adjust_end_date": self._adjust_end_date, 

1576 } 

1577 return dict 

1578 

1579 

1580def bonds_main(): 

1581 # zero coupon bond 

1582 zero_coupon_bond = ZeroBondSpecification( 

1583 obj_id="US500769CH58", 

1584 issue_price=85.0, 

1585 issue_date=datetime(2007, 6, 29), 

1586 maturity_date=datetime(2037, 6, 29), 

1587 currency="USD", 

1588 notional=1000, 

1589 issuer="KfW", 

1590 securitization_level=SecuritizationLevel.SENIOR_UNSECURED, 

1591 ) 

1592 # print("Zero Coupon Bond Specification:") 

1593 # print(zero_coupon_bond._to_dict()) 

1594 # print(zero_coupon_bond.notional_amount()) 

1595 

1596 

1597if __name__ == "__main__": 

1598 bonds_main()