from decimal import Decimal, getcontext from datetime import date from calendar import isleap getcontext().prec = 28 class DayCount: @staticmethod def year_fraction(start_date, end_date, convention): if end_date <= start_date: return Decimal('0') if convention == 'ACT/360': return Decimal((end_date - start_date).days) / Decimal(360) elif convention in ('ACT/365', 'ACT/365F'): return Decimal((end_date - start_date).days) / Decimal(365) elif convention == 'ACT/ACT_ISDA': return DayCount._act_act_isda(start_date, end_date) elif convention == '30/360_US': return DayCount._30_360_us(start_date, end_date) elif convention == '30E/360': return DayCount._30e_360(start_date, end_date) elif convention == '30E/360_ISDA': return DayCount._30e_360_isda(start_date, end_date) else: raise ValueError(f"Unsupported convention {convention}") # ---------- IMPLEMENTATIONS ---------- @staticmethod def _act_act_isda(start_date, end_date): total = Decimal('0') current = start_date while current < end_date: year_end = date(current.year, 12, 31) period_end = min(year_end.replace(day=31) + (date(current.year + 1, 1, 1) - year_end), end_date) days_in_period = (period_end - current).days days_in_year = 366 if isleap(current.year) else 365 total += Decimal(days_in_period) / Decimal(days_in_year) current = period_end return total @staticmethod def _30_360_us(d1, d2): d1_day = 30 if d1.day == 31 else d1.day if d1.day in (30, 31) and d2.day == 31: d2_day = 30 else: d2_day = d2.day days = ((d2.year - d1.year) * 360 + (d2.month - d1.month) * 30 + (d2_day - d1_day)) return Decimal(days) / Decimal(360) @staticmethod def _30e_360(d1, d2): d1_day = min(d1.day, 30) d2_day = min(d2.day, 30) days = ((d2.year - d1.year) * 360 + (d2.month - d1.month) * 30 + (d2_day - d1_day)) return Decimal(days) / Decimal(360) @staticmethod def _30e_360_isda(d1, d2): d1_day = 30 if d1.day == 31 else d1.day d2_day = 30 if d2.day == 31 else d2.day days = ((d2.year - d1.year) * 360 + (d2.month - d1.month) * 30 + (d2_day - d1_day)) return Decimal(days) / Decimal(360) class InterestCalculator: @staticmethod def calculate( start_date, end_date, rate, rate_type='annual', # 'annual' or 'monthly' convention='ACT/360', compounding='simple', # simple, annual, monthly, continuous ): """ Retourne le facteur d'intérêt (pas le montant). """ if not start_date or not end_date: return Decimal('0') if end_date <= start_date: return Decimal('0') rate = Decimal(str(rate)) # Conversion en taux annuel si besoin if rate_type == 'monthly': annual_rate = rate * Decimal(12) else: annual_rate = rate yf = DayCount.year_fraction(start_date, end_date, convention) if compounding == 'simple': return annual_rate * yf elif compounding == 'annual': return (Decimal(1) + annual_rate) ** yf - Decimal(1) elif compounding == 'monthly': monthly_rate = annual_rate / Decimal(12) months = yf * Decimal(12) return (Decimal(1) + monthly_rate) ** months - Decimal(1) elif compounding == 'continuous': from math import exp return Decimal(exp(float(annual_rate * yf))) - Decimal(1) else: raise ValueError("Unsupported compounding mode")