142 lines
3.9 KiB
Python
142 lines
3.9 KiB
Python
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")
|