Files
tradon/modules/purchase_trade/finance_tools.py
2026-02-15 12:47:07 +01:00

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")