Coverage for rivapy/tools/datetools.py: 82%

265 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-05 14:27 +0000

1# -*- coding: utf-8 -*- 

2 

3from datetime import datetime, date 

4from dateutil.relativedelta import relativedelta 

5from calendar import monthrange 

6from typing import List as _List, Union as _Union, Callable 

7from holidays import \ 

8 HolidayBase as _HolidayBase, \ 

9 ECB as _ECB 

10from rivapy.tools.enums import RollConvention, DayCounterType 

11from rivapy.tools._validators import _string_to_calendar 

12import logging 

13 

14 

15# TODO: Switch to locally configured logger. 

16logger = logging.getLogger(__name__) 

17logger.setLevel(logging.INFO) 

18 

19class DayCounter: 

20 

21 def __init__(self, daycounter: _Union[str, DayCounterType]): 

22 self._dc = DayCounterType.to_string(daycounter) 

23 self._yf = DayCounter.get(self._dc) 

24 

25 def yf(self, d1: _Union[date, datetime], d2: _Union[_Union[date, datetime],_List[_Union[date, datetime]]]): 

26 try: 

27 result = [self._yf(d1, d2_) for d2_ in d2] 

28 return result 

29 except: 

30 return self._yf(d1,d2) 

31 

32 

33 @staticmethod 

34 def get(daycounter: _Union[str, DayCounterType])->Callable[[ _Union[date, datetime], _Union[date, datetime]], float]: 

35 dc = DayCounterType.to_string(daycounter) 

36 if dc == DayCounterType.Act365Fixed.value: 

37 return DayCounter.yf_Act365Fixed 

38 raise NotImplementedError(dc + ' not yet implemented.') 

39 

40 @staticmethod 

41 def yf_Act365Fixed(d1: _Union[date, datetime], d2: _Union[date, datetime])->float: 

42 return ((d2-d1).total_seconds()/(365.0*24*60*60)) 

43 

44 

45class Period: 

46 def __init__(self, 

47 years: int = 0, 

48 months: int = 0, 

49 days: int = 0): 

50 """ 

51 Time Period expressed in years, months and days. 

52 

53 Args: 

54 years (int, optional): Number of years in time period. Defaults to 0. 

55 months (int, optional): Number of months in time period. Defaults to 0. 

56 days (int, optional): Number of days in time period. Defaults to 0. 

57 """ 

58 self.years = years 

59 self.months = months 

60 self.days = days 

61 

62 @staticmethod 

63 def from_string(period: str): 

64 """Creates a Period from a string 

65 

66 Args: 

67 period (str): The string defining the period. The string must be defined by the number of days/months/years followed by one of the letters 'Y'/'M'/'D', i.e. '6M' means 6 months. 

68 

69 Returns: 

70 Period: The resulting period 

71 

72 Examples: 

73 .. code-block:: python 

74 

75 >>> p = Period('6M') # period of 6 months 

76 >>> p = Period('1Y') #period of 1 year 

77 """ 

78 period_length = int(period[:-1]) 

79 period_type = period[1] 

80 if period_type == 'Y': 

81 return Period(years=period_length) 

82 elif period_type == 'M': 

83 return Period(months = period_length) 

84 elif period_type == 'D': 

85 return Period(days=period_length) 

86 raise Exception(period + ' is not a valid period string. See documentation of tools.datetools.Period for deocumentation of valid strings.') 

87 @property 

88 def years(self) -> int: 

89 """ 

90 Getter for years of period. 

91 

92 Returns: 

93 int: Number of years for specified time period. 

94 """ 

95 return self.__years 

96 

97 @years.setter 

98 def years(self, years: int): 

99 """ 

100 Setter for years of period. 

101 

102 Args: 

103 years(int): Number of years. 

104 """ 

105 self.__years = years 

106 

107 @property 

108 def months(self) -> int: 

109 """ 

110 Getter for months of period. 

111 

112 Returns: 

113 int: Number of months for specified time period. 

114 """ 

115 return self.__months 

116 

117 @months.setter 

118 def months(self, months: int): 

119 """ 

120 Setter for months of period. 

121 

122 Args: 

123 months(int): Number of months. 

124 """ 

125 self.__months = months 

126 

127 @property 

128 def days(self) -> int: 

129 """ 

130 Getter for number of days in time period. 

131 

132 Returns: 

133 int: Number of days for specified time period. 

134 """ 

135 return self.__days 

136 

137 @days.setter 

138 def days(self, days: int): 

139 """ 

140 Setter for days of period. 

141 

142 Args: 

143 days(int): Number of days. 

144 """ 

145 self.__days = days 

146 

147 

148class Schedule: 

149 def __init__(self, 

150 start_day: _Union[date, datetime], 

151 end_day: _Union[date, datetime], 

152 time_period: _Union[Period, str], 

153 backwards: bool = True, 

154 stub: bool = False, 

155 business_day_convention: _Union[RollConvention, str] = RollConvention.MODIFIED_FOLLOWING, 

156 calendar: _Union[_HolidayBase, str] = None): 

157 """ 

158 A schedule is a list of dates, e.g. of coupon payments, fixings, etc., which is defined by its fist (= start 

159 day) and last (= end day) day, by its distance between two consecutive dates (= time period) and by the 

160 procedure for rolling out the schedule, more precisely by the direction (backwards/forwards) and the dealing 

161 with incomplete periods (stubs). Moreover, the schedule ensures to comply to business day conventions with 

162 respect to a specified holiday calendar. 

163 

164 Args: 

165 start_day (_Union[date, datetime]): Schedule's first day - beginning of the schedule. 

166 end_day (_Union[date, datetime]): Schedule's last day - end of the schedule. 

167 time_period (_Union[Period, str]): Time distance between two consecutive dates. 

168 backwards (bool, optional): Defines direction for rolling out the schedule. True means the schedule will be 

169 rolled out (backwards) from end day to start day. Defaults to True. 

170 stub (bool, optional): Defines if the first/last period is accepted (True), even though it is shorter than 

171 the others, or if it remaining days are added to the neighbouring period (False). 

172 Defaults to True. 

173 business_day_convention (_Union[RollConvention, str], optional): Set of rules defining the adjustment of 

174 days to ensure each date being a business 

175 day with respect to a given holiday 

176 calendar. Defaults to 

177 RollConvention.MODIFIED_FOLLOWING 

178 calendar (_Union[_HolidayBase, str], optional): Holiday calendar defining the bank holidays of a country or 

179 province (but not all non-business days as for example 

180 Saturdays and Sundays). 

181 Defaults (through constructor) to holidays.ECB 

182 (= Target2 calendar) between start_day and end_day. 

183 

184 Examples: 

185 

186 .. code-block:: python 

187  

188 >>> from datetime import date 

189 >>> from rivapy.tools import schedule 

190 >>> schedule = Schedule(date(2020, 8, 21), date(2021, 8, 21), Period(0, 3, 0), True, False, RollConvention.UNADJUSTED, holidays_de).generate_dates(False), 

191 [date(2020, 8, 21), date(2020, 11, 21), date(2021, 2, 21), date(2021, 5, 21), date(2021, 8, 21)]) 

192 """ 

193 self.start_day = start_day 

194 self.end_day = end_day 

195 self.time_period = time_period 

196 self.backwards = backwards 

197 self.stub = stub 

198 self.business_day_convention = business_day_convention 

199 self.calendar = calendar 

200 

201 @property 

202 def start_day(self): 

203 """ 

204 Getter for schedule's start date. 

205 

206 Returns: 

207 Start date of specified schedule. 

208 """ 

209 return self.__start_day 

210 

211 @start_day.setter 

212 def start_day(self, start_day: _Union[date, datetime]): 

213 self.__start_day = _date_to_datetime(start_day) 

214 

215 @property 

216 def end_day(self): 

217 """ 

218 Getter for schedule's end date. 

219 

220 Returns: 

221 End date of specified schedule. 

222 """ 

223 return self.__end_day 

224 

225 @end_day.setter 

226 def end_day(self, end_day: _Union[date, datetime]): 

227 self.__end_day = _date_to_datetime(end_day) 

228 

229 @property 

230 def time_period(self): 

231 """ 

232 Getter for schedule's time period. 

233 

234 Returns: 

235 Time period of specified schedule. 

236 """ 

237 return self.__time_period 

238 

239 @time_period.setter 

240 def time_period(self, time_period: _Union[Period, str]): 

241 self.__time_period = _term_to_period(time_period) 

242 

243 @property 

244 def backwards(self): 

245 """ 

246 Getter for schedule's roll out direction. 

247 

248 Returns: 

249 True, if rolled out from end day to start day. 

250 False, if rolled out from start day to end day. 

251 """ 

252 return self.__backwards 

253 

254 @backwards.setter 

255 def backwards(self, backwards: bool): 

256 self.__backwards = backwards 

257 

258 @property 

259 def stub(self): 

260 """ 

261 Getter for potential existence of short periods (stubs). 

262 

263 Returns: 

264 True, if a shorter period is allowed. 

265 False, if only a longer period is allowed. 

266 """ 

267 return self.__stub 

268 

269 @stub.setter 

270 def stub(self, stub: bool): 

271 self.__stub = stub 

272 

273 @property 

274 def business_day_convention(self): 

275 """ 

276 Getter for schedule's business day convention. 

277 

278 Returns: 

279 Business day convention of specified schedule. 

280 """ 

281 return self.__business_day_convention 

282 

283 @business_day_convention.setter 

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

285 self.__business_day_convention = RollConvention.to_string(business_day_convention) 

286 

287 @property 

288 def calendar(self): 

289 """ 

290 Getter for schedule's holiday calendar. 

291 

292 Returns: 

293 Holiday calendar of specified schedule. 

294 """ 

295 return self.__calendar 

296 

297 @calendar.setter 

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

299 if calendar is None: 

300 self.__calendar = _ECB(years=range(self.__start_day.year, self.__end_day.year + 1)) 

301 else: 

302 self.__calendar = _string_to_calendar(calendar) 

303 

304 @staticmethod 

305 def _roll_out(from_: _Union[date, datetime], to_: _Union[date, datetime], term: Period, backwards: bool, 

306 allow_stub: bool) -> _List[date]: 

307 """ 

308 Rolls out dates from from_ to to_ in the specified direction applying the given term under consideration of the 

309 specification for allowing shorter periods. 

310 

311 Args: 

312 from_ (_Union[date, datetime]): Beginning of the roll out mechanism. 

313 to_ (_Union[date, datetime]): End of the roll out mechanism. 

314 term (Period): Difference between rolled out dates. 

315 backwards (bool): Direction of roll out mechanism: backwards if True, forwards if False. 

316 allow_stub (bool): Defines if periods shorter than term are allowed. 

317 

318 Returns: 

319 Date schedule not yet adjusted to any business day convention. 

320 """ 

321 # convert datetime to date (if necessary): 

322 from_ = _date_to_datetime(from_) 

323 to_ = _date_to_datetime(to_) 

324 

325 # check input consistency: 

326 if (~backwards) & (from_ < to_): 

327 direction = +1 

328 elif backwards & (from_ > to_): 

329 direction = -1 

330 else: 

331 raise Exception("From-date '" + str(from_) + "' and to-date '" + str(to_) + 

332 "' are not consistent with roll direction (backwards = '" + str(backwards) + "')!") 

333 

334 # generates a list of dates ... 

335 dates = [] 

336 # ... for forward rolling case or backward rolling case ... 

337 while ((~backwards) & (from_ <= to_)) | (backwards & (to_ <= from_)): 

338 dates.append(from_) 

339 from_ += direction * relativedelta(years=term.years, months=term.months, days=term.days) 

340 # ... and compete list for fractional periods ... 

341 if dates[-1] != to_: 

342 # ... by adding stub or ... 

343 if allow_stub: 

344 dates.append(to_) 

345 # ... by extending last period. 

346 else: 

347 dates[-1] = to_ 

348 return dates 

349 

350 def generate_dates(self, ends_only: bool) -> _List[date]: 

351 """ 

352 Generate list of schedule days according to the schedule specification, in particular with regards to business 

353 day convention and calendar given. 

354 

355 Args: 

356 ends_only (bool): Flag to indicate if period beginnings shall be included, e.g. for defining accrual 

357 periods: True, if only period ends shall be included, e.g. for defining payment dates. 

358 

359 Returns: 

360 List[date]: List of schedule dates (including start and end date) adjusted to rolling convention. 

361 """ 

362 # roll out dates ignoring any business day issues 

363 if self.__backwards: 

364 schedule_dates = Schedule._roll_out(self.__end_day, self.__start_day, self.__time_period, 

365 True, self.__stub) 

366 schedule_dates.reverse() 

367 else: 

368 schedule_dates = Schedule._roll_out(self.__start_day, self.__end_day, self.__time_period, 

369 False, self.__stub) 

370 

371 # adjust according to business day convention 

372 rolled_schedule_dates = [roll_day(schedule_dates[0], self.__calendar, self.__business_day_convention, 

373 schedule_dates[0])] 

374 [rolled_schedule_dates.append(roll_day(schedule_dates[i], self.__calendar, self.__business_day_convention, 

375 rolled_schedule_dates[i - 1])) for i in range(1, len(schedule_dates))] 

376 

377 if ends_only: 

378 rolled_schedule_dates.pop(0) 

379 

380 logger.debug("Schedule dates successfully calculated from '" 

381 + str(self.__start_day) + "' to '" + str(self.__end_day) + "'.") 

382 return rolled_schedule_dates 

383 

384 

385 

386 

387# class PowerSchedule: 

388# def __init__(self, 

389# start_day: _Union[date, datetime], 

390# end_day: _Union[date, datetime], 

391# time_period: _Union[Period, str], 

392# backwards: bool = True, 

393# business_day_convention: _Union[RollConvention, str] = RollConvention.MODIFIED_FOLLOWING, 

394# calendar: _Union[_HolidayBase, str] = None): 

395# """ 

396 

397# Args: 

398# start_day (_Union[date, datetime]): Schedule's first day - beginning of the schedule. 

399# end_day (_Union[date, datetime]): Schedule's last day - end of the schedule. 

400# time_period (_Union[Period, str]): Time distance between two consecutive dates. 

401# backwards (bool, optional): Defines direction for rolling out the schedule. True means the schedule will be 

402# rolled out (backwards) from end day to start day. Defaults to True. 

403# stub (bool, optional): Defines if the first/last period is accepted (True), even though it is shorter than 

404# the others, or if it remaining days are added to the neighbouring period (False). 

405# Defaults to True. 

406# business_day_convention (_Union[RollConvention, str], optional): Set of rules defining the adjustment of 

407# days to ensure each date being a business 

408# day with respect to a given holiday 

409# calendar. Defaults to 

410# RollConvention.MODIFIED_FOLLOWING 

411# calendar (_Union[_HolidayBase, str], optional): Holiday calendar defining the bank holidays of a country or 

412# province (but not all non-business days as for example 

413# Saturdays and Sundays). 

414# Defaults (through constructor) to holidays.ECB 

415# (= Target2 calendar) between start_day and end_day. 

416 

417# Examples: 

418 

419# .. code-block:: python 

420 

421# >>> from datetime import date 

422# >>> from rivapy.tools import schedule 

423# >>> schedule = Schedule(date(2020, 8, 21), date(2021, 8, 21), Period(0, 3, 0), True, False, RollConvention.UNADJUSTED, holidays_de).generate_dates(False), 

424# [date(2020, 8, 21), date(2020, 11, 21), date(2021, 2, 21), date(2021, 5, 21), date(2021, 8, 21)]) 

425# """ 

426# self.start_day = start_day 

427# self.end_day = end_day 

428# self.time_period = time_period 

429# self.backwards = backwards 

430# self.business_day_convention = business_day_convention 

431# self.calendar = calendar 

432 

433 

434 

435# @property 

436# def start_day(self): 

437# """ 

438# Getter for schedule's start date. 

439 

440# Returns: 

441# Start date of specified schedule. 

442# """ 

443# return self.__start_day 

444 

445# @start_day.setter 

446# def start_day(self, start_day: _Union[date, datetime]): 

447# self.__start_day = _date_to_datetime(start_day) 

448 

449# @property 

450# def end_day(self): 

451# """ 

452# Getter for schedule's end date. 

453 

454# Returns: 

455# End date of specified schedule. 

456# """ 

457# return self.__end_day 

458 

459# @end_day.setter 

460# def end_day(self, end_day: _Union[date, datetime]): 

461# self.__end_day = _date_to_datetime(end_day) 

462 

463# @property 

464# def time_period(self): 

465# """ 

466# Getter for schedule's time period. 

467 

468# Returns: 

469# Time period of specified schedule. 

470# """ 

471# return self.__time_period 

472 

473# @time_period.setter 

474# def time_period(self, time_period: _Union[Period, str]): 

475# self.__time_period = _term_to_period(time_period) 

476 

477# @property 

478# def backwards(self): 

479# """ 

480# Getter for schedule's roll out direction. 

481 

482# Returns: 

483# True, if rolled out from end day to start day. 

484# False, if rolled out from start day to end day. 

485# """ 

486# return self.__backwards 

487 

488# @backwards.setter 

489# def backwards(self, backwards: bool): 

490# self.__backwards = backwards 

491 

492# @property 

493# def stub(self): 

494# """ 

495# Getter for potential existence of short periods (stubs). 

496 

497# Returns: 

498# True, if a shorter period is allowed. 

499# False, if only a longer period is allowed. 

500# """ 

501# return self.__stub 

502 

503# @stub.setter 

504# def stub(self, stub: bool): 

505# self.__stub = stub 

506 

507# @property 

508# def business_day_convention(self): 

509# """ 

510# Getter for schedule's business day convention. 

511 

512# Returns: 

513# Business day convention of specified schedule. 

514# """ 

515# return self.__business_day_convention 

516 

517# @business_day_convention.setter 

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

519# self.__business_day_convention = RollConvention.to_string(business_day_convention) 

520 

521# @property 

522# def calendar(self): 

523# """ 

524# Getter for schedule's holiday calendar. 

525 

526# Returns: 

527# Holiday calendar of specified schedule. 

528# """ 

529# return self.__calendar 

530 

531# @calendar.setter 

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

533# if calendar is None: 

534# self.__calendar = _ECB(years=range(self.__start_day.year, self.__end_day.year + 1)) 

535# else: 

536# self.__calendar = _string_to_calendar(calendar) 

537 

538# @staticmethod 

539# def _roll_out(from_: _Union[date, datetime], to_: _Union[date, datetime], term: Period, backwards: bool, 

540# allow_stub: bool) -> _List[date]: 

541# """ 

542# Rolls out dates from from_ to to_ in the specified direction applying the given term under consideration of the 

543# specification for allowing shorter periods. 

544 

545# Args: 

546# from_ (_Union[date, datetime]): Beginning of the roll out mechanism. 

547# to_ (_Union[date, datetime]): End of the roll out mechanism. 

548# term (Period): Difference between rolled out dates. 

549# backwards (bool): Direction of roll out mechanism: backwards if True, forwards if False. 

550# allow_stub (bool): Defines if periods shorter than term are allowed. 

551 

552# Returns: 

553# Date schedule not yet adjusted to any business day convention. 

554# """ 

555# # convert datetime to date (if necessary): 

556# from_ = _date_to_datetime(from_) 

557# to_ = _date_to_datetime(to_) 

558 

559# # check input consistency: 

560# if (~backwards) & (from_ < to_): 

561# direction = +1 

562# elif backwards & (from_ > to_): 

563# direction = -1 

564# else: 

565# raise Exception("From-date '" + str(from_) + "' and to-date '" + str(to_) + 

566# "' are not consistent with roll direction (backwards = '" + str(backwards) + "')!") 

567 

568# # generates a list of dates ... 

569# dates = [] 

570# # ... for forward rolling case or backward rolling case ... 

571# while ((~backwards) & (from_ <= to_)) | (backwards & (to_ <= from_)): 

572# dates.append(from_) 

573# from_ += direction * relativedelta(years=term.years, months=term.months, days=term.days) 

574# # ... and compete list for fractional periods ... 

575# if dates[-1] != to_: 

576# # ... by adding stub or ... 

577# if allow_stub: 

578# dates.append(to_) 

579# # ... by extending last period. 

580# else: 

581# dates[-1] = to_ 

582# return dates 

583 

584# def generate_dates(self, ends_only: bool) -> _List[date]: 

585# """ 

586# Generate list of schedule days according to the schedule specification, in particular with regards to business 

587# day convention and calendar given. 

588 

589# Args: 

590# ends_only (bool): Flag to indicate if period beginnings shall be included, e.g. for defining accrual 

591# periods: True, if only period ends shall be included, e.g. for defining payment dates. 

592 

593# Returns: 

594# List[date]: List of schedule dates (including start and end date) adjusted to rolling convention. 

595# """ 

596# # roll out dates ignoring any business day issues 

597# if self.__backwards: 

598# schedule_dates = Schedule._roll_out(self.__end_day, self.__start_day, self.__time_period, 

599# True, self.__stub) 

600# schedule_dates.reverse() 

601# else: 

602# schedule_dates = Schedule._roll_out(self.__start_day, self.__end_day, self.__time_period, 

603# False, self.__stub) 

604 

605# # adjust according to business day convention 

606# rolled_schedule_dates = [roll_day(schedule_dates[0], self.__calendar, self.__business_day_convention, 

607# schedule_dates[0])] 

608# [rolled_schedule_dates.append(roll_day(schedule_dates[i], self.__calendar, self.__business_day_convention, 

609# rolled_schedule_dates[i - 1])) for i in range(1, len(schedule_dates))] 

610 

611# if ends_only: 

612# rolled_schedule_dates.pop(0) 

613 

614# logger.debug("Schedule dates successfully calculated from '" 

615# + str(self.__start_day) + "' to '" + str(self.__end_day) + "'.") 

616# return rolled_schedule_dates 

617 

618 

619 

620def _date_to_datetime(date_time: _Union[datetime, date] 

621 ) -> date: 

622 """ 

623 Converts a date to a datetime or leaves it unchanged if it is already of type datetime. 

624 

625 Args: 

626 date_time (_Union[datetime, date]): Date(time) to be converted. 

627 

628 Returns: 

629 date: (Potentially) Converted datetime. 

630 """ 

631 if isinstance(date_time, datetime): 

632 return date_time 

633 elif isinstance(date_time, date): 

634 return datetime.combine(date_time, datetime.min.time()) 

635 else: 

636 raise TypeError("'" + str(date_time) + "' must be of type datetime or date!") 

637 

638 

639def _datetime_to_date_list(date_times: _Union[_List[datetime], _List[date]] 

640 ) -> _List[date]: 

641 """ 

642 Converts types of date list from datetime to date or leaves it unchanged if they are already of type date. 

643 

644 Args: 

645 date_times (_Union[List[datetime], List[date]]): List of date(time)s to be converted. 

646 

647 Returns: 

648 List[date]: List of (potentially) converted date(time)s. 

649 """ 

650 if isinstance(date_times, list): 

651 return [_date_to_datetime(date_time) for date_time in date_times] 

652 else: 

653 raise TypeError("'" + str(date_times) + "' must be a list of type datetime or date!") 

654 

655 

656def _string_to_period(term: str 

657 ) -> Period: 

658 """ 

659 Converts terms, e.g. 1D, 3M, and 5Y, into periods, i.e. Period(0, 0, 1), Period(0, 3, 0), and Period(5, 0, 0), 

660 respectively. 

661 

662 Args: 

663 term (str): Term to be converted into a period. 

664 

665 Returns: 

666 Period: Period corresponding to the term specified. 

667 """ 

668 unit = term[-1] 

669 measure = int(term[:-1]) 

670 

671 if unit.upper() == 'D': 

672 period = Period(0, 0, measure) 

673 elif unit.upper() == 'M': 

674 period = Period(0, measure, 0) 

675 elif unit.upper() == 'Y': 

676 period = Period(measure, 0, 0) 

677 else: 

678 raise Exception("Unknown term! Please use: 'D', 'M', or 'Y'.") 

679 

680 return period 

681 

682 

683def _term_to_period(term: _Union[Period, str] 

684 ) -> Period: 

685 """ 

686 Converts a term provided as period or string into period format if necessary. 

687 

688 Args: 

689 term (_Union[Period, str]): Tenor to be converted if provided as string. 

690 

691 Returns: 

692 Period: Tenor (potentially converted) in(to) period format. 

693 """ 

694 if isinstance(term, Period): 

695 return term 

696 elif isinstance(term, str): 

697 return _string_to_period(term) 

698 else: 

699 raise TypeError("The term '" + str(term) + "' must be provided as Period or string!") 

700 

701 

702def calc_end_day(start_day: _Union[date, datetime], 

703 term: str, 

704 business_day_convention: _Union[RollConvention, str] = None, 

705 calendar: _Union[_HolidayBase, str] = None 

706 ) -> date: 

707 """ 

708 Derives the end date of a time period based on the start day the the term given as string, e.g. 1D, 3M, or 5Y. 

709 If business day convention and corresponding calendar are provided the end date is additionally rolled accordingly. 

710 

711 Args: 

712 start_day (_Union[date, datetime): Beginning of the time period with length term. 

713 term (str): Term defining the period from start to end date. 

714 business_day_convention (_Union[RollConvention, str], optional): Set of rules defining how to adjust 

715 non-business days. Defaults to None. 

716 calendar (_Union[_HolidayBase, str], optional): Holiday calender defining non-business days 

717 (but not Saturdays and Sundays). 

718 Defaults to None. 

719 

720 Returns: 

721 date: End date potentially adjusted according to the specified business day convention with respect to the given 

722 calendar. 

723 """ 

724 start_date = _date_to_datetime(start_day) 

725 period = _term_to_period(term) 

726 end_date = start_date + relativedelta(years=period.years, months=period.months, days=period.days) 

727 if (business_day_convention is not None) & (calendar is not None): 

728 end_date = roll_day(end_date, calendar, business_day_convention, start_date) 

729 

730 return end_date 

731 

732 

733def calc_start_day(end_day: _Union[date, datetime], 

734 term: str, 

735 business_day_convention: _Union[RollConvention, str] = None, 

736 calendar: _Union[_HolidayBase, str] = None 

737 ) -> date: 

738 """ 

739 Derives the start date of a time period based on the end day the the term given as string, e.g. 1D, 3M, or 5Y. 

740 If business day convention and corresponding calendar are provided the start date is additionally rolled 

741 accordingly. 

742 

743 Args: 

744 end_day (_Union[date, datetime): End of the time period with length term. 

745 term (str): Term defining the period from start to end date. 

746 business_day_convention (_Union[RollConvention, str], optional): Set of rules defining how to adjust 

747 non-business days. Defaults to None. 

748 calendar (_Union[_HolidayBase, str], optional): Holiday calender defining non-business days 

749 (but not Saturdays and Sundays). 

750 Defaults to None. 

751 Returns: 

752 date: Start date potentially adjusted according to the specified business day convention with respect to the 

753 given calendar. 

754 """ 

755 end_date = _date_to_datetime(end_day) 

756 period = _term_to_period(term) 

757 start_date = end_date - relativedelta(years=period.years, months=period.months, days=period.days) 

758 if (business_day_convention is not None) & (calendar is not None): 

759 start_date = roll_day(start_date, calendar, business_day_convention) 

760 

761 return start_date 

762 

763 

764def last_day_of_month(day: _Union[date, datetime] 

765 ) -> date: 

766 """ 

767 Derives last day of the month corresponding to the given day. 

768 

769 Args: 

770 day (_Union[date, datetime]): Day defining month and year for derivation of month's last day. 

771 

772 Returns: 

773 date: Date of last day of the corresponding month. 

774 """ 

775 return date(day.year, day.month, monthrange(day.year, day.month)[1]) 

776 

777 

778def is_last_day_of_month(day: _Union[date, datetime] 

779 ) -> bool: 

780 """ 

781 Checks if a given day is the last day of the corresponding month. 

782 

783 Args: 

784 day (_Union[date, datetime]): Day to be checked. 

785 

786 Returns: 

787 bool: True, if day is last day of the month, False otherwise. 

788 """ 

789 return _date_to_datetime(day) == last_day_of_month(day) 

790 

791 

792def is_business_day(day: _Union[date, datetime], 

793 calendar: _Union[_HolidayBase, str] 

794 ) -> bool: 

795 """ 

796 Checks if a given day is a business day in a given calendar. 

797 

798 Args: 

799 day (_Union[date, datetime]): Day to be checked. 

800 calendar (_Union[_HolidayBase, str]): List of holidays defined by the given calendar. 

801 

802 Returns: 

803 bool: True, if day is a business day, False otherwise. 

804 """ 

805 # TODO: adjust for countries with weekend not on Saturday/Sunday (http://worldmap.workingdays.org/) 

806 return (day.isoweekday() < 6) & (day not in _string_to_calendar(calendar)) 

807 

808 

809def last_business_day_of_month(day: _Union[date, datetime], 

810 calendar: _Union[_HolidayBase, str] 

811 ) -> date: 

812 """ 

813 Derives the last business day of a month corresponding to a given day based on the holidays set in the calendar. 

814 

815 Args: 

816 day (_Union[date, datetime]): Day defining month and year for deriving the month's last business day. 

817 calendar (_Union[_HolidayBase, str]): List of holidays defined by the given calendar. 

818 

819 Returns: 

820 date: Date of last business day of the corresponding month. 

821 """ 

822 check_day = date(day.year, day.month, monthrange(day.year, day.month)[1]) 

823 while not (is_business_day(check_day, calendar)): 

824 check_day -= relativedelta(days=1) 

825 return check_day 

826 

827 

828def is_last_business_day_of_month(day: _Union[date, datetime], 

829 calendar: _Union[_HolidayBase, str] 

830 ) -> bool: 

831 """ 

832 Checks it the given day is the last business day of the corresponding month. 

833 

834 Args: 

835 day (_Union[date, datetime]): day to be checked 

836 calendar (_Union[_HolidayBase, str]): list of holidays defined by the given calendar 

837 

838 Returns: 

839 bool: True if day is last business day of the corresponding month, False otherwise. 

840 """ 

841 return _date_to_datetime(day) == last_business_day_of_month(day, calendar) 

842 

843 

844def nearest_business_day(day: _Union[date, datetime], 

845 calendar: _Union[_HolidayBase, str], 

846 following_first: bool = True 

847 ) -> date: 

848 """ 

849 Derives nearest business day from given day for a given calendar. If there are equally near days preceding and 

850 following the flag following_first determines if the following day is preferred to the preceding one. 

851 

852 Args: 

853 day (_Union[date, datetime]): Day for which the nearest business day is to be found.  

854 calendar (_Union[_HolidayBase, str]): List of holidays given by calendar. 

855 following_first (bool): Flag for deciding if following days are preferred to an equally near preceding day. 

856 Default value is True. 

857 

858 Returns: 

859 date: Nearest business day to given day according to given calendar. 

860 """ 

861 distance = 0 

862 if following_first: 

863 direction = -1 

864 else: 

865 direction = +1 

866 

867 day = _date_to_datetime(day) 

868 while not is_business_day(day, calendar): 

869 distance += 1 

870 direction *= -1 

871 day += direction * relativedelta(days=distance) 

872 return day 

873 

874 

875def nearest_last_business_day_of_month(day: _Union[date, datetime], 

876 calendar: _Union[_HolidayBase, str], 

877 following_first: bool = True 

878 ) -> date: 

879 """ 

880 Derives nearest last business day of a month from given day for a given calendar. If there are equally near days 

881 preceding and following the flag following_first determines if the following day is preferred to the preceding one. 

882 

883 Args: 

884 day (_Union[date, datetime]): Day for which the nearest last business day of the month is to be found. 

885 calendar (_Union[_HolidayBase, str]): List of holidays given by calendar. 

886 following_first (bool, optional): Flag for deciding if following days are preferred to an equally near preceding 

887 day. Defaults to True. 

888 

889 Returns: 

890 date: Nearest last business day of a month to given day according to given calendar. 

891 """ 

892 distance = 0 

893 if following_first: 

894 direction = -1 

895 else: 

896 direction = +1 

897 

898 day = _date_to_datetime(day) 

899 while not is_last_business_day_of_month(day, calendar): 

900 distance += 1 

901 direction *= -1 

902 day += direction * relativedelta(days=distance) 

903 return day 

904 

905 

906def next_or_previous_business_day(day: _Union[date, datetime], 

907 calendar: _Union[_HolidayBase, str], 

908 following_first: bool 

909 ) -> date: 

910 """ 

911 Derives the preceding or following business day to a given day according to a given calendar depending on the flag 

912 following_first. If the day is already a business day the function directly returns the day. 

913 

914 Args: 

915 day (_Union[date, datetime]): Day for which the preceding or following business day is to be found. 

916 calendar (_HolidayBase): List of holidays defined by the calendar. 

917 following_first (bool): Flag to determine in the following (True) or preceding (False) business day is to be 

918 found. 

919 

920 Returns: 

921 date: Preceding or following business day, respectively, or day itself if it is a business day. 

922 """ 

923 if following_first: 

924 direction = +1 

925 else: 

926 direction = -1 

927 

928 day = _date_to_datetime(day) 

929 while not is_business_day(day, calendar): 

930 day += direction * relativedelta(days=1) 

931 

932 return day 

933 

934 

935def following(day: _Union[date, datetime], 

936 calendar: _Union[_HolidayBase, str] 

937 ) -> date: 

938 """ 

939 Derives the (potentially) adjusted business day according to the business day convention 'Following' for a specified 

940 day with respect to a specific calendar: The adjusted date is the following good business day. 

941 

942 Args: 

943 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day. 

944 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends. 

945 

946 Returns: 

947 date: Adjusted business day according to the roll convention 'Following' with respect to calendar if the day is 

948 not already a business day. Otherwise the (unadjusted) day is returned. 

949 """ 

950 return next_or_previous_business_day(day, calendar, True) 

951 

952 

953def preceding(day: _Union[date, datetime], 

954 calendar: _Union[_HolidayBase, str] 

955 ) -> date: 

956 """ 

957 Derives the (potentially) adjusted business day according to the business day convention 'Preceding' for a specified 

958 day with respect to a specific calendar: The adjusted date is the preceding good business day. 

959 

960 Args: 

961 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day. 

962 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends. 

963 

964 Returns: 

965 date: Adjusted business day according to the roll convention 'Preceding' with respect to calendar if the day is 

966 not already a business day. Otherwise the (unadjusted) day is returned. 

967 """ 

968 return next_or_previous_business_day(day, calendar, False) 

969 

970 

971def modified_following(day: _Union[date, datetime], 

972 calendar: _Union[_HolidayBase, str] 

973 ) -> date: 

974 """ 

975 Derives the (potentially) adjusted business day according to the business day convention 'Modified Following' for a 

976 specified day with respect to a specific calendar: The adjusted date is the following good business day unless the 

977 day is in the next calendar month, in which case the adjusted date is the preceding good business day. 

978 

979 Args: 

980 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day. 

981 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends. 

982 

983 Returns: 

984 date: Adjusted business day according to the roll convention 'Modified Following' with respect to calendar if 

985 the day is not already a business day. Otherwise the (unadjusted) day is returned. 

986 """ 

987 next_day = next_or_previous_business_day(day, calendar, True) 

988 if next_day.month > day.month: 

989 return preceding(day, calendar) 

990 else: 

991 return next_day 

992 

993 

994def modified_following_eom(day: _Union[date, datetime], 

995 calendar: _Union[_HolidayBase, str], 

996 start_day: _Union[date, datetime] 

997 ) -> date: 

998 """ 

999 Derives the (potentially) adjusted business day according to the business day convention 'End of Month' for a 

1000 specified day with respect to a specific calendar: Where the start date of a period is on the final business day of 

1001 a particular calendar month, the end date is on the final business day of the end month (not necessarily the 

1002 corresponding date in the end month). 

1003 

1004 Args: 

1005 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day. 

1006 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends. 

1007 start_day (_Union[date, datetime]): Day at which the period under consideration begins. 

1008 

1009 Returns: 

1010 date: Adjusted business day according to the roll convention 'End of Month' with respect to calendar. 

1011 """ 

1012 if isinstance(start_day, date) | isinstance(start_day, datetime): 

1013 if is_last_business_day_of_month(start_day, calendar): 

1014 return nearest_last_business_day_of_month(day, calendar) 

1015 else: 

1016 return modified_following(day, calendar) 

1017 else: 

1018 raise Exception('The roll convention ' + str(RollConvention.MODIFIED_FOLLOWING_EOM) 

1019 + ' cannot be evaluated without a start_day') 

1020 

1021 

1022def modified_following_bimonthly(day: _Union[date, datetime], 

1023 calendar: _Union[_HolidayBase, str] 

1024 ) -> date: 

1025 """ 

1026 Derives the (potentially) adjusted business day according to the business day convention 'Modified Following 

1027 Bimonthly' for a specified day with respect to a specific calendar: The adjusted date is the following good business 

1028 day unless that day crosses the mid-month (15th) or end of a month, in which case the adjusted date is the preceding 

1029 good business day. 

1030 

1031 Args: 

1032 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day. 

1033 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends. 

1034 

1035 Returns: 

1036 date: Adjusted business day according to the roll convention 'Modified Following Bimonthly' with respect to 

1037 calendar if the day is not already a business day. Otherwise the (unadjusted) day is returned. 

1038 """ 

1039 next_day = next_or_previous_business_day(day, calendar, True) 

1040 if (next_day.month > day.month) | ((next_day.day > 15) & (day.day <= 15)): 

1041 return preceding(day, calendar) 

1042 else: 

1043 return next_day 

1044 

1045 

1046def modified_preceding(day: _Union[date, datetime], 

1047 calendar: _Union[_HolidayBase, str] 

1048 ) -> date: 

1049 """ 

1050 Derives the (potentially) adjusted business day according to the business day convention 'Modified Preceding' for a 

1051 specified day with respect to a specific calendar: The adjusted date is the preceding good business day unless the 

1052 day is in the previous calendar month, in which case the adjusted date is the following good business day. 

1053 

1054 Args: 

1055 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day. 

1056 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends. 

1057 

1058 Returns: 

1059 date: Adjusted business day according to the roll convention 'Modified Preceding' with respect to calendar if 

1060 the day is not already a business day. Otherwise the (unadjusted) day is returned. 

1061 """ 

1062 prev_day = next_or_previous_business_day(day, calendar, False) 

1063 if prev_day.month < day.month: 

1064 return following(day, calendar) 

1065 else: 

1066 return prev_day 

1067 

1068 

1069# to be used in the switcher (identical argument list) 

1070def unadjusted(day: _Union[date, datetime], 

1071 _ 

1072 ) -> date: 

1073 """ 

1074 Leaves the day unchanged independent from the fact if it is already a business day. 

1075 

1076 Args: 

1077 day (_Union[date, datetime]): Day to be adjusted according to the roll convention. 

1078 _: Placeholder for calendar argument. 

1079 

1080 Returns: 

1081 date: Unadjusted day. 

1082 """ 

1083 return _date_to_datetime(day) 

1084 

1085 

1086def roll_day(day: _Union[date, datetime], 

1087 calendar: _Union[_HolidayBase, str], 

1088 business_day_convention: _Union[RollConvention, str], 

1089 start_day: _Union[date, datetime] = None 

1090 ) -> date: 

1091 """ 

1092 Adjusts a given day according to the specified business day convention with respect to a given calendar or if the 

1093 given day falls on a Saturday or Sunday. For some roll conventions not only the (end) day to be adjusted but also 

1094 the start day of a period is relevant for the adjustment of the given (end) day. 

1095 

1096 Args: 

1097 day (_Union[date, datetime]): Day to be adjusted if it is a non-business day. 

1098 calendar (_Union[_HolidayBase, str]): Holiday calendar defining non-business days (but not weekends). 

1099 business_day_convention (_Union[RollConvention, str]): Set of rules defining how to adjust non-business days. 

1100 start_day (_Union[date, datetime], optional): Period's start day that may influence the rolling of the end day. 

1101 Defaults to None. 

1102 

1103 Returns: 

1104 date: Adjusted day. 

1105 """ 

1106 roll_convention = RollConvention.to_string(business_day_convention) 

1107 #if start_day is not None: 

1108 # start_day = _date_to_datetime(start_day) 

1109 

1110 switcher = { 

1111 'Unadjusted': unadjusted, 

1112 'Following': following, 

1113 'ModifiedFollowing': modified_following, 

1114 'ModifiedFollowingEOM': modified_following_eom, 

1115 'ModifiedFollowingBimonthly': modified_following_bimonthly, 

1116 'Nearest': nearest_business_day, 

1117 'Preceding': preceding, 

1118 'ModifiedPreceding': modified_preceding 

1119 } 

1120 # Get the appropriate roll function from switcher dictionary 

1121 roll_func = switcher.get(roll_convention, lambda: "Business day convention '" + str(business_day_convention) 

1122 + "' is not known!") 

1123 try: 

1124 result = roll_func(day, calendar) 

1125 except TypeError: 

1126 result = roll_func(day, calendar, start_day) 

1127 

1128 return result