Skip to content

Actual/ISDA

Bases: Convention

Implements the Actual/Actual (ISDA) day count convention.

Counts the actual number of days between two dates, including the end date for each year segment, and divides by 365 (non-leap year) or 366 (leap year). For multi-year periods, splits the calculation by year, summing fractions for each year. Suitable for compound interest calculations and XIRR when use_xirr_method is True.

Parameters:

Name Type Description Default
use_post_dates bool

If True, uses cash flow post dates for day counts; if False, uses value dates. Defaults to True.

True
include_non_financing_flows bool

If True, includes non-financing cash flows (e.g., fees) in factor computations; if False, excludes them. Defaults to False.

False
use_xirr_method bool

If True, uses the XIRR method, setting day count origin to DRAWDOWN; if False, uses NEIGHBOUR. Defaults to False.

False

Examples:

>>> dc = ActualISDA()
>>> factor = dc.compute_factor(
...     pd.Timestamp('2020-01-28', tz='UTC'),
...     pd.Timestamp('2020-02-28', tz='UTC')
... )
>>> print(factor)
f = 31/366 = 0.08469945
Source code in curo/daycount/actual_isda.py
 10
 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
class ActualISDA(Convention):
    """
    Implements the Actual/Actual (ISDA) day count convention.

    Counts the actual number of days between two dates, including the end date for each
    year segment, and divides by 365 (non-leap year) or 366 (leap year). For multi-year
    periods, splits the calculation by year, summing fractions for each year. Suitable
    for compound interest calculations and XIRR when `use_xirr_method` is True.

    Args:
        use_post_dates: If True, uses cash flow post dates for day counts; if False, uses
            value dates. Defaults to True.
        include_non_financing_flows: If True, includes non-financing cash flows (e.g., fees)
            in factor computations; if False, excludes them. Defaults to False.
        use_xirr_method: If True, uses the XIRR method, setting day count origin to
            DRAWDOWN; if False, uses NEIGHBOUR. Defaults to False.

    Examples:
        >>> dc = ActualISDA()
        >>> factor = dc.compute_factor(
        ...     pd.Timestamp('2020-01-28', tz='UTC'),
        ...     pd.Timestamp('2020-02-28', tz='UTC')
        ... )
        >>> print(factor)
        f = 31/366 = 0.08469945
    """
    def __init__(
        self,
        use_post_dates: bool = True,
        include_non_financing_flows: bool = False,
        use_xirr_method: bool = False
    ):
        super().__init__(
            use_post_dates=use_post_dates,
            include_non_financing_flows=include_non_financing_flows,
            use_xirr_method=use_xirr_method
        )

    def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
        """
        Computes the year fraction between two dates using Actual/Actual (ISDA).

        Args:
            start: The earlier date (pd.Timestamp).
            end: The later date (pd.Timestamp).

        Returns:
            DayCountFactor: The year fraction with operand log.

        Raises:
            ValueError: If `end` is before `start`.

        Examples:
            >>> dc = ActualISDA()
            >>> factor = dc.compute_factor(
            ...     pd.Timestamp('2020-01-28', tz='UTC'),
            ...     pd.Timestamp('2020-02-28', tz='UTC')
            ... )
            >>> factor.primary_period_fraction
            0.08469945355191257
            >>> factor.discount_factor_log
            ['31/366']
        """
        if end < start:
            raise ValueError("end must be after start")
        if end == start:
            return DayCountFactor(primary_period_fraction=0.0, discount_factor_log=["0/365"])

        start_year = start.year
        end_year = end.year
        factor = 0.0

        if start_year == end_year:
            # Same year: use single denominator (365 or 366)
            days = (end - start).days
            denominator = 366 if calendar.isleap(start_year) else 365
            factor = days / denominator
            return DayCountFactor(
                primary_period_fraction=factor,
                discount_factor_log=[f"{days}/{denominator}"]
            )

        # Multi-year: split by year
        discount_factor_log = []
        current_date = start
        current_year = start_year

        while current_year != end_year:
            year_end = pd.Timestamp(f"{current_year}-12-31", tz="UTC")
            days = (year_end - current_date).days + 1 if year_end >= current_date else 0
            denominator = 366 if calendar.isleap(current_year) else 365
            factor += days / denominator
            discount_factor_log.append(f"{days}/{denominator}")
            current_date = year_end + pd.Timedelta(days=1)  # Move to Jan 1 next year
            current_year += 1

        # Final partial year
        days = (end - current_date).days if end >= current_date else 0
        denominator = 366 if calendar.isleap(end_year) else 365
        if days > 0:
            factor += days / denominator
            discount_factor_log.append(f"{days}/{denominator}")

        return DayCountFactor(
            primary_period_fraction=factor,
            discount_factor_log=discount_factor_log
        )

compute_factor(start, end)

Computes the year fraction between two dates using Actual/Actual (ISDA).

Parameters:

Name Type Description Default
start Timestamp

The earlier date (pd.Timestamp).

required
end Timestamp

The later date (pd.Timestamp).

required

Returns:

Name Type Description
DayCountFactor DayCountFactor

The year fraction with operand log.

Raises:

Type Description
ValueError

If end is before start.

Examples:

>>> dc = ActualISDA()
>>> factor = dc.compute_factor(
...     pd.Timestamp('2020-01-28', tz='UTC'),
...     pd.Timestamp('2020-02-28', tz='UTC')
... )
>>> factor.primary_period_fraction
0.08469945355191257
>>> factor.discount_factor_log
['31/366']
Source code in curo/daycount/actual_isda.py
 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
def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
    """
    Computes the year fraction between two dates using Actual/Actual (ISDA).

    Args:
        start: The earlier date (pd.Timestamp).
        end: The later date (pd.Timestamp).

    Returns:
        DayCountFactor: The year fraction with operand log.

    Raises:
        ValueError: If `end` is before `start`.

    Examples:
        >>> dc = ActualISDA()
        >>> factor = dc.compute_factor(
        ...     pd.Timestamp('2020-01-28', tz='UTC'),
        ...     pd.Timestamp('2020-02-28', tz='UTC')
        ... )
        >>> factor.primary_period_fraction
        0.08469945355191257
        >>> factor.discount_factor_log
        ['31/366']
    """
    if end < start:
        raise ValueError("end must be after start")
    if end == start:
        return DayCountFactor(primary_period_fraction=0.0, discount_factor_log=["0/365"])

    start_year = start.year
    end_year = end.year
    factor = 0.0

    if start_year == end_year:
        # Same year: use single denominator (365 or 366)
        days = (end - start).days
        denominator = 366 if calendar.isleap(start_year) else 365
        factor = days / denominator
        return DayCountFactor(
            primary_period_fraction=factor,
            discount_factor_log=[f"{days}/{denominator}"]
        )

    # Multi-year: split by year
    discount_factor_log = []
    current_date = start
    current_year = start_year

    while current_year != end_year:
        year_end = pd.Timestamp(f"{current_year}-12-31", tz="UTC")
        days = (year_end - current_date).days + 1 if year_end >= current_date else 0
        denominator = 366 if calendar.isleap(current_year) else 365
        factor += days / denominator
        discount_factor_log.append(f"{days}/{denominator}")
        current_date = year_end + pd.Timedelta(days=1)  # Move to Jan 1 next year
        current_year += 1

    # Final partial year
    days = (end - current_date).days if end >= current_date else 0
    denominator = 366 if calendar.isleap(end_year) else 365
    if days > 0:
        factor += days / denominator
        discount_factor_log.append(f"{days}/{denominator}")

    return DayCountFactor(
        primary_period_fraction=factor,
        discount_factor_log=discount_factor_log
    )