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
« 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
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
43# placeholder
44from rivapy.marketdata.curves import DiscountCurve
47class BondBaseSpecification(interfaces.FactoryObject):
48 """Base class for bond-like instrument specifications.
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 """
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.
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()
108 def set_amortization_scheme(self, amortization_scheme) -> AmortizationScheme:
109 """Resolve an amortization scheme descriptor into an AmortizationScheme.
111 Accepts one of:
112 - None: returns a ZeroAmortizationScheme
113 - str: resolves the identifier via AmortizationScheme._from_string
114 - AmortizationScheme instance: returned unchanged
116 Args:
117 amortization_scheme (None | str | AmortizationScheme): descriptor.
119 Returns:
120 AmortizationScheme: concrete amortization scheme object.
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.")
134 def set_notional_structure(self, notional, amortization_scheme) -> NotionalStructure:
135 """Create or validate the notional structure for this instrument.
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.
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.
147 Returns:
148 NotionalStructure: instance representing the instrument notional.
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
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.
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.
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.
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
236 def _validate_derived_issued_instrument(self):
237 self._issue_date, self._maturity_date = _check_start_before_end(self._issue_date, self._maturity_date)
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
252 # region properties
254 @property
255 def issuer(self) -> str:
256 """
257 Getter for instrument's issuer.
259 Returns:
260 str: Instrument's issuer.
261 """
262 return self._issuer
264 @issuer.setter
265 def issuer(self, issuer: str):
266 """
267 Setter for instrument's issuer.
269 Args:
270 issuer(str): Issuer of the instrument.
271 """
272 self._issuer = issuer
274 @property
275 def rating(self) -> str:
276 return self._rating
278 @rating.setter
279 def rating(self, rating: _Union[Rating, str]) -> str:
280 self._rating = Rating.to_string(rating)
282 @property
283 def securitization_level(self) -> str:
284 """
285 Getter for instrument's securitisation level.
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
294 @securitization_level.setter
295 def securitization_level(self, securitisation_level: _Union[SecuritizationLevel, str]):
296 self._securitization_level = SecuritizationLevel.to_string(securitisation_level)
298 @property
299 def issue_date(self) -> date:
300 """
301 Getter for bond's issue date.
303 Returns:
304 date: Bond's issue date.
305 """
306 return self._issue_date
308 @issue_date.setter
309 def issue_date(self, issue_date: _Union[datetime, date]):
310 """
311 Setter for bond's issue date.
313 Args:
314 issue_date (Union[datetime, date]): Bond's issue date.
315 """
316 self._issue_date = _date_to_datetime(issue_date)
318 @property
319 def maturity_date(self) -> date:
320 """
321 Getter for bond's maturity date.
323 Returns:
324 date: Bond's maturity date.
325 """
326 return self._maturity_date
328 @maturity_date.setter
329 def maturity_date(self, maturity_date: _Union[datetime, date]):
330 """
331 Setter for bond's maturity date.
333 Args:
334 maturity_date (Union[datetime, date]): Bond's maturity date.
335 """
336 self._maturity_date = _date_to_datetime(maturity_date)
338 @property
339 def currency(self) -> str:
340 """
341 Getter for bond's currency.
343 Returns:
344 str: Bond's ISO 4217 currency code
345 """
346 return self._currency
348 @currency.setter
349 def currency(self, currency: str):
350 self._currency = Currency.to_string(currency)
352 @property
353 def notional(self) -> NotionalStructure:
354 """
355 Getter for bond's face value.
357 Returns:
358 float: Bond's face value.
359 """
360 return self._notional
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))
369 @property
370 def day_count_convention(self) -> str:
371 """
372 Getter for instruments's day count convention.
374 Returns:
375 str: instruments's day count convention.
376 """
377 return self._day_count_convention
379 @day_count_convention.setter
380 def day_count_convention(self, dcc: _Union[DayCounterType, str]):
381 self._day_count_convention = DayCounterType.to_string(dcc)
383 @property
384 def business_day_convention(self) -> str:
385 """
386 Getter for FRA's day count convention.
388 Returns:
389 str: FRA's day count convention.
390 """
391 return self._business_day_convention
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)
398 @property
399 def roll_convention(self) -> str:
400 """
401 Getter for the roll convention used for business day adjustment.
403 Returns:
404 str: The roll convention used for business day adjustment.
405 """
406 return self._roll_convention
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.
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)
418 @property
419 def calendar(self):
420 """
421 Getter for the calendar used for business day adjustment.
423 Returns:
424 The calendar used for business day adjustment.
425 """
426 return self._calendar
428 @calendar.setter
429 def calendar(self, calendar: _Union[_HolidayBase, str]):
430 """
431 Setter for the calendar used for business day adjustment.
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)
441 # endregion
443 def notional_amount(self, index: _Union[date, datetime, int] = None) -> float:
444 """Get the notional amount at a specific date.
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.
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)
461class DeterministicCashflowBondSpecification(BondBaseSpecification):
462 """Specification for instruments that produce deterministic cashflows.
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).
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`).
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 """
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.
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.
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)
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()
609 # region properties
611 @property
612 def coupon(self) -> float:
613 """
614 Getter for instrument's coupon.
616 Returns:
617 float: Instrument's coupon.
618 """
619 return self._coupon
621 @coupon.setter
622 def coupon(self, rate: float):
623 """
624 Setter for instrument's rate.
626 Args:
627 rate(float): interest rate of the instrument.
628 """
629 self._rate = rate
631 @property
632 def start_date(self) -> datetime.date:
633 """
634 Getter for deposit's start date.
636 Returns:
637 date: deposit's start date.
638 """
639 return self._start_date
641 @start_date.setter
642 def start_date(self, start_date: _Union[date, datetime]):
643 """
644 Setter for deposit's start date.
646 Args:
647 start_date (Union[datetime, date]): deposit's start date.
648 """
649 self._start_date = _date_to_datetime(start_date)
651 @property
652 def end_date(self) -> datetime:
653 """
654 Getter for deposit's end date.
656 Returns:
657 date: deposit's end date.
658 """
659 return self._end_date
661 @end_date.setter
662 def end_date(self, end_date: _Union[date, datetime]):
663 """
664 Setter for deposit's end date.
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)
673 @property
674 def frequency(self) -> Period:
675 """
676 Getter for instrument's payment frequency.
678 Returns:
679 Period: instrument's payment frequency.
680 """
681 return self._frequency
683 @frequency.setter
684 def frequency(self, frequency: _Union[Period, str]):
685 """
686 Setter for instrument's payment frequency.
688 Args:
689 frequency (Union[Period, str]): instrument's payment frequency.
690 """
691 self._frequency = _term_to_period(frequency)
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)
698 @issue_price.setter
699 def issue_price(self, issue_price: _Union[float, str]):
700 self._issue_price = _check_non_negativity(issue_price)
702 @property
703 def notional_exchange(self):
704 return self._notional_exchange
706 @notional_exchange.setter
707 def notional_exchange(self, notional_exchange: bool):
708 self._notional_exchange = notional_exchange
710 @property
711 def payment_days(self) -> int:
712 """
713 Getter for the number of payment days.
715 Returns:
716 int: Number of payment days.
717 """
718 return self._payment_days
720 @payment_days.setter
721 def payment_days(self, payment_days: int):
722 """
723 Setter for the number of payment days.
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
732 @property
733 def pays_in_arrears(self) -> bool:
734 """
735 Getter for the pays_in_arrears flag.
737 Returns:
738 bool: True if the instrument pays in arrears, False otherwise.
739 """
740 return self._pays_in_arrears
742 @pays_in_arrears.setter
743 def pays_in_arrears(self, pays_in_arrears: bool):
744 """
745 Setter for the pays_in_arrears flag.
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
754 @property
755 def coupon_type(self) -> str:
756 """
757 Getter for the coupon type of the instrument.
759 Returns:
760 str: The coupon type of the instrument.
761 """
762 return self._coupon_type
764 @coupon_type.setter
765 def coupon_type(self, coupon_type: str):
766 """
767 Setter for the coupon type of the instrument.
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
776 @property
777 def backwards(self) -> bool:
778 """
779 Getter for the backwards flag.
781 Returns:
782 bool: True if the schedule is generated backwards, False otherwise.
783 """
784 return self._backwards
786 @backwards.setter
787 def backwards(self, backwards: bool):
788 """
789 Setter for the backwards flag.
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
798 @property
799 def stub_type_is_Long(self) -> bool:
800 """
801 Getter for the stub type flag.
803 Returns:
804 bool: True if the stub type is long, False otherwise.
805 """
806 return self._stub_type_is_Long
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.
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
820 @property
821 def spot_days(self) -> int:
822 """
823 Getter for the number of spot days.
825 Returns:
826 int: Number of spot days.
827 """
828 return self._spot_days
830 @spot_days.setter
831 def spot_days(self, spot_days: int):
832 """
833 Setter for the number of spot days.
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
842 @property
843 def last_fixing(self) -> _Optional[float]:
844 """
845 Getter for the last fixing value.
847 Returns:
848 _Optional[float]: The last fixing value, or None if not set.
849 """
850 return self._last_fixing
852 @last_fixing.setter
853 def last_fixing(self, last_fixing: _Optional[float]):
854 """
855 Setter for the last fixing value.
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
864 @property
865 def nr_annual_payments(self) -> _Optional[float]:
866 """
867 Getter for the number of annual payments.
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
876 @nr_annual_payments.setter
877 def nr_annual_payments(self, value: _Optional[float]):
878 """
879 Setter for the number of annual payments.
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
888 @property
889 def schedule(self) -> Schedule:
890 """
891 Getter for the dates of the instrument.
893 Returns:
894 Schedule: The schedule of the instrument.
895 """
896 return self.get_schedule()
898 @schedule.setter
899 def schedule(self, schedule: Schedule):
900 """
901 Setter for the dates of the instrument.
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
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.
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 []
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.
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
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.
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 []
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.
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
990 @property
991 def index(self) -> float:
992 """
993 Getter for instrument's index.
995 Returns:
996 float: Instrument's index.
997 """
998 return self._index
1000 @index.setter
1001 def index(self, index: _Union[InterestRateIndex, str]):
1002 """
1003 Setter for instrument's index.
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
1012 @property
1013 def ir_index(self) -> InterestRateIndex:
1014 """
1015 Getter for instrument's interest rate index.
1017 Returns:
1018 InterestRateIndex: Instrument's interest rate index.
1019 """
1020 return self._ir_index
1022 @ir_index.setter
1023 def ir_index(self, ir_index: InterestRateIndex):
1024 """
1025 Setter for instrument's interest rate index.
1027 Args:
1028 ir_index (InterestRateIndex): Instrument's interest rate index.
1029 """
1030 self._ir_index = ir_index
1032 @property
1033 def adjust_start_date(self) -> bool:
1034 return self._adjust_start_date
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)
1043 @property
1044 def adjust_end_date(self) -> bool:
1045 return self._adjust_end_date
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)
1053 @property
1054 def adjust_schedule(self) -> bool:
1055 return self._adjust_schedule
1057 @adjust_schedule.setter
1058 def adjust_schedule(self, value: bool):
1059 self._adjust_schedule = value
1061 @property
1062 def adjust_accruals(self) -> bool:
1063 return self._adjust_accruals
1065 @adjust_accruals.setter
1066 def adjust_accruals(self, value: bool):
1067 self._adjust_accruals = value
1069 # endregion
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.")
1080 def get_schedule(self) -> Schedule:
1081 """Return a configured :class:`Schedule` for the instrument.
1083 The returned Schedule is constructed from the instrument's start/end
1084 dates, frequency/tenor, stub and roll conventions and calendar.
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 )
1100 def get_nr_annual_payments(self) -> float:
1101 """Compute the (approximate) number of annual payments implied by frequency.
1103 Returns:
1104 float: number of payments per year implied by the frequency. If
1105 frequency is not set 0.0 is returned.
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
1122 @abc.abstractmethod
1123 def _to_dict(self) -> dict:
1124 pass
1127class FixedRateBondSpecification(DeterministicCashflowBondSpecification):
1128 """Specification for fixed-rate bonds.
1130 Stores coupon, frequency and other fixed-rate specific settings and
1131 delegates schedule construction to the base class behaviour.
1132 """
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.
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 )
1200 @staticmethod
1201 def _create_sample(n_samples: int, seed: int = None):
1202 """Return a list of example FixedRateBondSpecification instances.
1204 Args:
1205 n_samples (int): Number of sample instances to create.
1206 seed (int, optional): RNG seed for reproducibility.
1208 Returns:
1209 List[FixedRateBondSpecification]: Example fixed-rate bonds.
1210 """
1211 result = []
1212 if seed is not None:
1213 np.random.seed(seed)
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
1239 def _to_dict(self) -> Dict:
1240 """Serialize the fixed-rate bond specification to a dictionary.
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
1266class ZeroBondSpecification(DeterministicCashflowBondSpecification):
1267 """Specification for zero-coupon bonds.
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 """
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.
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 )
1328 @staticmethod
1329 def _create_sample(n_samples: int, seed: int = None):
1330 """Return a list of example ZeroBondSpecification instances.
1332 Args:
1333 n_samples (int): Number of sample instances to create.
1334 seed (int, optional): RNG seed for reproducibility.
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
1363 def _to_dict(self) -> Dict:
1364 """Serialize the zero-coupon bond specification to a dictionary.
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
1385class FloatingRateBondSpecification(DeterministicCashflowBondSpecification):
1386 """Specification for floating-rate bonds.
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 """
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.
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.
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.
1448 Raises:
1449 ValueError: If neither index nor frequency is provided.
1450 """
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()
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 )
1509 @staticmethod
1510 def _create_sample(n_samples: int, seed: int = None):
1511 """Return a list of example FloatingRateBondSpecification instances.
1513 Args:
1514 n_samples (int): Number of sample instances to create.
1515 seed (int, optional): RNG seed for reproducibility.
1517 Returns:
1518 List[FloatingRateBondSpecification]: Example floating-rate bonds.
1519 """
1520 result = []
1521 if seed is not None:
1522 np.random.seed(seed)
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
1550 def _to_dict(self) -> Dict:
1551 """Serialize the floating-rate bond specification to a dictionary.
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
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())
1597if __name__ == "__main__":
1598 bonds_main()