Bases: Convention
The EU Directive 2008/48/EC day count convention for Annual Percentage Rate of Charge (APRC).
Used for consumer credit agreements in EU member states. Computes time intervals backwards
from the cash flow date to the initial drawdown date, expressed as whole periods (years,
months, or weeks) plus remaining days divided by 365 or 366 (leap year).
For more details, see EU APR guidelines
(ANNEX 1, section 4.1.1). This document is no longer available online and is provided
here for reference.
Note
Replaced by Directive (EU) 2023/2225, but this implementation remains valid as it
expresses intervals as whole periods plus days, per Annex III, I. (c).
Parameters:
| Name |
Type |
Description |
Default |
time_period
|
DayCountTimePeriod
|
The interval for calculations ('year',
'month', 'week'). Defaults to 'month'.
|
MONTH
|
Source code in curo/daycount/eu_2008_48.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126 | class EU200848EC(Convention):
"""
The EU Directive 2008/48/EC day count convention for Annual Percentage Rate of Charge (APRC).
Used for consumer credit agreements in EU member states. Computes time intervals backwards
from the cash flow date to the initial drawdown date, expressed as whole periods (years,
months, or weeks) plus remaining days divided by 365 or 366 (leap year).
For more details, see [EU APR guidelines](../../assets/reference/eu_apr_guidelines_final.pdf)
(ANNEX 1, section 4.1.1). This document is no longer available online and is provided
here for reference.
Note:
Replaced by Directive (EU) 2023/2225, but this implementation remains valid as it
expresses intervals as whole periods plus days, per Annex III, I. (c).
Args:
time_period (DayCountTimePeriod, optional): The interval for calculations ('year',
'month', 'week'). Defaults to 'month'.
"""
def __init__(self, time_period: DayCountTimePeriod = DayCountTimePeriod.MONTH):
if time_period not in [
DayCountTimePeriod.YEAR,
DayCountTimePeriod.MONTH,
DayCountTimePeriod.WEEK
]:
raise ValueError("Only year, month, and week time periods are supported")
self.time_period = time_period
super().__init__(
use_post_dates=True,
include_non_financing_flows=True,
use_xirr_method=True
)
def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
"""
Computes the day count factor between two dates using the EU 2008/48/EC convention.
Calculates intervals backwards from the end date to the start date, expressed as whole
periods (years, months, or weeks) plus remaining days divided by 365 or 366.
Args:
start (pd.Timestamp): Initial drawdown date.
end (pd.Timestamp): Cash flow post date.
Returns:
DayCountFactor: The day count factor with year fraction and operands.
Raises:
ValueError: If end is before start.
"""
if end < start:
raise ValueError("end must be after start")
if end == start:
return DayCountFactor(primary_period_fraction=0.0, discount_factor_log=["0"])
whole_periods = 0
initial_drawdown = start
start_whole_period = end
operand_log = []
while True:
temp_date = None
if self.time_period == DayCountTimePeriod.YEAR:
temp_date = roll_month(start_whole_period, -12, end.day)
elif self.time_period == DayCountTimePeriod.MONTH:
temp_date = roll_month(start_whole_period, -1, end.day)
elif self.time_period == DayCountTimePeriod.WEEK:
temp_date = roll_day(start_whole_period, -7)
if not initial_drawdown > temp_date:
start_whole_period = temp_date
whole_periods += 1
else:
# Handle month-end cases for year/month periods
if self.time_period in [DayCountTimePeriod.YEAR, DayCountTimePeriod.MONTH]:
if self.time_period == DayCountTimePeriod.YEAR:
if (initial_drawdown.month == temp_date.month and
initial_drawdown.day == temp_date.day):
break
if (start.month == end.month and
has_month_end_day(start) and
has_month_end_day(end)):
start_whole_period = initial_drawdown
whole_periods += 1
elif self.time_period == DayCountTimePeriod.MONTH:
if initial_drawdown.day == temp_date.day:
break
if (initial_drawdown.day >= temp_date.day and
has_month_end_day(start) and
has_month_end_day(end)):
start_whole_period = initial_drawdown
whole_periods += 1
break
factor = 0.0
if whole_periods > 0:
factor = whole_periods / self.time_period.periods_in_year
operand_log.append(
DayCountFactor.operands_to_string(whole_periods, self.time_period.periods_in_year)
)
if not initial_drawdown > start_whole_period:
numerator = actual_days(initial_drawdown, start_whole_period)
start_den_period = roll_month(start_whole_period, -12, start_whole_period.day)
denominator = actual_days(
start_den_period,
start_whole_period) if numerator > 0 else self.time_period.periods_in_year
factor += numerator / denominator
if numerator > 0 or not operand_log:
operand_log.append(
DayCountFactor.operands_to_string(numerator, denominator)
)
return DayCountFactor(primary_period_fraction=factor, discount_factor_log=operand_log)
|
compute_factor(start, end)
Computes the day count factor between two dates using the EU 2008/48/EC convention.
Calculates intervals backwards from the end date to the start date, expressed as whole
periods (years, months, or weeks) plus remaining days divided by 365 or 366.
Parameters:
| Name |
Type |
Description |
Default |
start
|
Timestamp
|
|
required
|
end
|
Timestamp
|
|
required
|
Returns:
| Name | Type |
Description |
DayCountFactor |
DayCountFactor
|
The day count factor with year fraction and operands.
|
Raises:
| Type |
Description |
ValueError
|
|
Source code in curo/daycount/eu_2008_48.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126 | def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
"""
Computes the day count factor between two dates using the EU 2008/48/EC convention.
Calculates intervals backwards from the end date to the start date, expressed as whole
periods (years, months, or weeks) plus remaining days divided by 365 or 366.
Args:
start (pd.Timestamp): Initial drawdown date.
end (pd.Timestamp): Cash flow post date.
Returns:
DayCountFactor: The day count factor with year fraction and operands.
Raises:
ValueError: If end is before start.
"""
if end < start:
raise ValueError("end must be after start")
if end == start:
return DayCountFactor(primary_period_fraction=0.0, discount_factor_log=["0"])
whole_periods = 0
initial_drawdown = start
start_whole_period = end
operand_log = []
while True:
temp_date = None
if self.time_period == DayCountTimePeriod.YEAR:
temp_date = roll_month(start_whole_period, -12, end.day)
elif self.time_period == DayCountTimePeriod.MONTH:
temp_date = roll_month(start_whole_period, -1, end.day)
elif self.time_period == DayCountTimePeriod.WEEK:
temp_date = roll_day(start_whole_period, -7)
if not initial_drawdown > temp_date:
start_whole_period = temp_date
whole_periods += 1
else:
# Handle month-end cases for year/month periods
if self.time_period in [DayCountTimePeriod.YEAR, DayCountTimePeriod.MONTH]:
if self.time_period == DayCountTimePeriod.YEAR:
if (initial_drawdown.month == temp_date.month and
initial_drawdown.day == temp_date.day):
break
if (start.month == end.month and
has_month_end_day(start) and
has_month_end_day(end)):
start_whole_period = initial_drawdown
whole_periods += 1
elif self.time_period == DayCountTimePeriod.MONTH:
if initial_drawdown.day == temp_date.day:
break
if (initial_drawdown.day >= temp_date.day and
has_month_end_day(start) and
has_month_end_day(end)):
start_whole_period = initial_drawdown
whole_periods += 1
break
factor = 0.0
if whole_periods > 0:
factor = whole_periods / self.time_period.periods_in_year
operand_log.append(
DayCountFactor.operands_to_string(whole_periods, self.time_period.periods_in_year)
)
if not initial_drawdown > start_whole_period:
numerator = actual_days(initial_drawdown, start_whole_period)
start_den_period = roll_month(start_whole_period, -12, start_whole_period.day)
denominator = actual_days(
start_den_period,
start_whole_period) if numerator > 0 else self.time_period.periods_in_year
factor += numerator / denominator
if numerator > 0 or not operand_log:
operand_log.append(
DayCountFactor.operands_to_string(numerator, denominator)
)
return DayCountFactor(primary_period_fraction=factor, discount_factor_log=operand_log)
|