Skip to content

Commit 889f599

Browse files
Code refactoring
1 parent 85f899b commit 889f599

File tree

15 files changed

+1320
-64
lines changed

15 files changed

+1320
-64
lines changed

curo/daycount/convention.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""_summary_
2+
3+
Raises:
4+
NotImplementedError: _description_
5+
6+
Returns:
7+
_type_: _description_
8+
"""
9+
10+
import pandas as pd
11+
from daycount.day_count_factor import DayCountFactor
12+
from enums import DayCountOrigin
13+
14+
class Convention:
15+
"""_summary_
16+
"""
17+
def __init__(self, use_post_dates: bool = False, include_non_financing_flows: bool = False,
18+
use_xirr_method: bool = False):
19+
self.use_post_dates = use_post_dates
20+
self.include_non_financing_flows = include_non_financing_flows
21+
self.use_xirr_method = use_xirr_method
22+
23+
def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
24+
"""_summary_
25+
26+
Args:
27+
start (pd.Timestamp): _description_
28+
end (pd.Timestamp): _description_
29+
30+
Raises:
31+
NotImplementedError: _description_
32+
33+
Returns:
34+
DayCountFactor: _description_
35+
"""
36+
raise NotImplementedError
37+
38+
@property
39+
def day_count_origin(self) -> DayCountOrigin:
40+
"""_summary_
41+
42+
Returns:
43+
DayCountOrigin: _description_
44+
"""
45+
return DayCountOrigin.DRAWDOWN if self.use_xirr_method else DayCountOrigin.NEIGHBOUR

curo/daycount/day_count_factor.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""_summary_
2+
"""
3+
4+
from dataclasses import dataclass
5+
from typing import List, Optional
6+
7+
@dataclass
8+
class DayCountFactor:
9+
"""_summary_
10+
11+
Returns:
12+
_type_: _description_
13+
"""
14+
principal_factor: float
15+
fractional_adjustment: Optional[float]
16+
principal_operand_log: List[str]
17+
fractional_operand_log: Optional[List[str]]
18+
19+
@classmethod
20+
def us_appendix_j(cls, principal_factor: float,
21+
fractional_adjustment: float,
22+
principal_operand_log: List[str],
23+
fractional_operand_log: List[str]) -> 'DayCountFactor':
24+
"""_summary_
25+
26+
Args:
27+
principal_factor (float): _description_
28+
fractional_adjustment (float): _description_
29+
principal_operand_log (List[str]): _description_
30+
fractional_operand_log (List[str]): _description_
31+
32+
Returns:
33+
DayCountFactor: _description_
34+
"""
35+
return cls(principal_factor, fractional_adjustment, principal_operand_log,
36+
fractional_operand_log)
37+
38+
@property
39+
def total_factor(self) -> float:
40+
"""_summary_
41+
42+
Returns:
43+
float: _description_
44+
"""
45+
return self.principal_factor + (self.fractional_adjustment or 0.0)
46+
47+
@staticmethod
48+
def operands_to_string(numerator: int, denominator: int) -> str:
49+
"""_summary_
50+
51+
Args:
52+
numerator (int): _description_
53+
denominator (int): _description_
54+
55+
Returns:
56+
str: _description_
57+
"""
58+
return f"{numerator}/{denominator}"

curo/daycount/us_30_360.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""_summary_
2+
3+
Returns:
4+
_type_: _description_
5+
"""
6+
7+
import pandas as pd
8+
from convention import Convention
9+
from day_count_factor import DayCountFactor
10+
11+
12+
class US30360(Convention):
13+
"""_summary_
14+
15+
Args:
16+
Convention (_type_): _description_
17+
"""
18+
def __init__(self, use_post_dates: bool = True,
19+
include_non_financing_flows: bool = False,
20+
use_xirr_method: bool = False):
21+
super().__init__(use_post_dates=use_post_dates,
22+
include_non_financing_flows=include_non_financing_flows,
23+
use_xirr_method=use_xirr_method)
24+
25+
def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
26+
dd1, mm1, yyyy1 = start.day, start.month, start.year
27+
dd2, mm2, yyyy2 = end.day, end.month, end.year
28+
29+
z = 30 if dd1 == 31 else dd1
30+
dt1 = 360 * yyyy1 + 30 * mm1 + z
31+
32+
if dd2 == 31 and (dd1 == 30 or dd1 == 31):
33+
z = 30
34+
elif dd2 == 31 and dd1 < 30:
35+
z = dd2
36+
else:
37+
z = dd2
38+
dt2 = 360 * yyyy2 + 30 * mm2 + z
39+
40+
numerator = abs(dt2 - dt1)
41+
factor = numerator / 360
42+
43+
return DayCountFactor(
44+
principal_factor=factor,
45+
fractional_adjustment=None,
46+
principal_operand_log=[DayCountFactor.operands_to_string(numerator, 360)],
47+
fractional_operand_log=None
48+
)

curo/daycount/us_30u_360.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Docstring for daycount.us_30u_360
3+
"""
4+
5+
import pandas as pd
6+
from convention import Convention
7+
from day_count_factor import DayCountFactor
8+
from utils import is_leap_year
9+
10+
class US30U360(Convention):
11+
"""_summary_
12+
13+
Args:
14+
Convention (_type_): _description_
15+
"""
16+
def __init__(self, use_post_dates: bool = True,
17+
include_non_financing_flows: bool = False,
18+
use_xirr_method: bool = False):
19+
super().__init__(use_post_dates=use_post_dates,
20+
include_non_financing_flows=include_non_financing_flows,
21+
use_xirr_method=use_xirr_method)
22+
23+
def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
24+
dt1 = start.replace(day=30 if start.day == 31 else start.day)
25+
dt2 = end.replace(day=30 if end.day == 31 else end.day)
26+
days_diff = self._day_difference(dt1, dt2)
27+
days = ((dt2.year - dt1.year) * 360) + ((dt2.month - dt1.month) * 30) + days_diff
28+
29+
numerator = abs(days)
30+
factor = numerator / 360
31+
32+
return DayCountFactor(
33+
principal_factor=factor,
34+
fractional_adjustment=None,
35+
principal_operand_log=[DayCountFactor.operands_to_string(numerator, 360)],
36+
fractional_operand_log=None
37+
)
38+
39+
def _day_difference(self, d1: pd.Timestamp, d2: pd.Timestamp) -> int:
40+
is_d1_last_day = self._is_last_day_of_month(d1)
41+
is_d2_last_day = self._is_last_day_of_month(d2)
42+
43+
if is_d1_last_day and is_d2_last_day:
44+
return d2.day + (d1.day - d2.day) - d1.day
45+
if is_d1_last_day and d1.month == 2 and d1.day != d2.day:
46+
if not is_leap_year(d1.year) and d2.day == 29:
47+
return d2.day - (d1.day + (29 - d1.day))
48+
return d2.day - (d1.day + (30 - d1.day))
49+
if is_d2_last_day and d2.month == 2 and d2.day != d1.day:
50+
if not is_leap_year(d2.year) and d1.day == 29:
51+
return (d2.day + (29 - d2.day)) - d1.day
52+
return (d2.day + (30 - d2.day)) - d1.day
53+
return d2.day - d1.day
54+
55+
def _is_last_day_of_month(self, date: pd.Timestamp) -> bool:
56+
next_month = date + pd.offsets.MonthBegin(1)
57+
end_of_month = next_month - pd.Timedelta(days=1)
58+
return (date.year == end_of_month.year and
59+
date.month == end_of_month.month and
60+
date.day == end_of_month.day)

curo/daycount/us_appendix_j.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""_summary_
2+
3+
Raises:
4+
ValueError: _description_
5+
6+
Returns:
7+
_type_: _description_
8+
"""
9+
10+
from typing import List
11+
import pandas as pd
12+
from daycount.convention import Convention
13+
from daycount.day_count_factor import DayCountFactor
14+
from enums import DayCountTimePeriod
15+
from utils import roll_day, roll_month, has_month_end_day
16+
17+
18+
class USAppendixJ(Convention):
19+
"""_summary_
20+
21+
Args:
22+
Convention (_type_): _description_
23+
"""
24+
def __init__(self, time_period: DayCountTimePeriod = DayCountTimePeriod.MONTH):
25+
"""_summary_
26+
27+
Args:
28+
time_period (DayCountTimePeriod, optional): _description_.
29+
Defaults to DayCountTimePeriod.MONTH.
30+
"""
31+
super().__init__(use_post_dates=True,
32+
include_non_financing_flows=True,
33+
use_xirr_method=True)
34+
self.time_period = time_period
35+
36+
def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFactor:
37+
"""_summary_
38+
39+
Args:
40+
start (pd.Timestamp): _description_
41+
end (pd.Timestamp): _description_
42+
43+
Raises:
44+
ValueError: _description_
45+
46+
Returns:
47+
DayCountFactor: _description_
48+
"""
49+
whole_periods = 0
50+
initial_drawdown = start
51+
start_whole_period = end
52+
principal_operand_log: List[str] = []
53+
fractional_operand_log: List[str] = []
54+
55+
if self.time_period == DayCountTimePeriod.DAY:
56+
days = (start_whole_period - initial_drawdown).days
57+
fractional_adjustment = days / 365.0
58+
fractional_operand_log.append(DayCountFactor.operands_to_string(days, 365))
59+
return DayCountFactor.us_appendix_j(
60+
principal_factor=0.0,
61+
fractional_adjustment=fractional_adjustment,
62+
principal_operand_log=principal_operand_log,
63+
fractional_operand_log=fractional_operand_log
64+
)
65+
66+
while True:
67+
temp_date = start_whole_period
68+
if self.time_period == DayCountTimePeriod.YEAR:
69+
temp_date = roll_month(start_whole_period, -12, end.day)
70+
elif self.time_period == DayCountTimePeriod.HALF_YEAR:
71+
temp_date = roll_month(start_whole_period, -6, end.day)
72+
elif self.time_period == DayCountTimePeriod.QUARTER:
73+
temp_date = roll_month(start_whole_period, -3, end.day)
74+
elif self.time_period == DayCountTimePeriod.MONTH:
75+
temp_date = roll_month(start_whole_period, -1, end.day)
76+
elif self.time_period == DayCountTimePeriod.FORTNIGHT:
77+
temp_date = roll_day(start_whole_period, -14)
78+
elif self.time_period == DayCountTimePeriod.WEEK:
79+
temp_date = roll_day(start_whole_period, -7)
80+
else:
81+
raise ValueError("Unsupported time period")
82+
83+
if not initial_drawdown > temp_date:
84+
start_whole_period = temp_date
85+
whole_periods += 1
86+
else:
87+
if self.time_period in [DayCountTimePeriod.YEAR, DayCountTimePeriod.HALF_YEAR,
88+
DayCountTimePeriod.QUARTER, DayCountTimePeriod.MONTH]:
89+
if initial_drawdown.day == temp_date.day:
90+
break
91+
if (initial_drawdown.month == temp_date.month and
92+
has_month_end_day(initial_drawdown) and
93+
has_month_end_day(end)):
94+
if initial_drawdown.day <= temp_date.day or has_month_end_day(end):
95+
start_whole_period = initial_drawdown
96+
whole_periods += 1
97+
break
98+
99+
principal_factor = float(whole_periods) if whole_periods > 0 else 0.0
100+
if whole_periods > 0:
101+
principal_operand_log.append(str(whole_periods))
102+
103+
fractional_adjustment = 0.0
104+
if not initial_drawdown > start_whole_period:
105+
days = (start_whole_period - initial_drawdown).days
106+
denominator = {
107+
DayCountTimePeriod.YEAR: 365,
108+
DayCountTimePeriod.HALF_YEAR: 180,
109+
DayCountTimePeriod.QUARTER: 90,
110+
DayCountTimePeriod.MONTH: 30,
111+
DayCountTimePeriod.FORTNIGHT: 15,
112+
DayCountTimePeriod.WEEK: 7
113+
}.get(self.time_period, 30)
114+
115+
if days > 0:
116+
fractional_adjustment = days / denominator
117+
fractional_operand_log.append(
118+
DayCountFactor.operands_to_string(days, denominator)
119+
)
120+
121+
return DayCountFactor.us_appendix_j(
122+
principal_factor=principal_factor,
123+
fractional_adjustment=fractional_adjustment,
124+
principal_operand_log=principal_operand_log,
125+
fractional_operand_log=fractional_operand_log
126+
)

curo/example.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Docstring for examples.example
3+
"""
4+
5+
import pandas as pd
6+
#from curo import Calculator, SeriesAdvance
7+
#, SeriesPayment
8+
from calculator import Calculator
9+
from enums import Frequency, Mode
10+
from series import SeriesAdvance, SeriesPayment
11+
12+
calculator = Calculator()
13+
14+
# Step 2: Define the advance, payment, and/or charge series
15+
#calculator.add(
16+
# series=SeriesAdvance(number_of=2, value=10000)
17+
#)
18+
19+
series_payment = SeriesPayment(
20+
number_of=3,
21+
# post_date_from=pd.Timestamp('2025-01-01', tz='UTC'),
22+
)
23+
24+
pd_dataframe = series_payment.to_cash_flows(pd.Timestamp('2025-01-27', tz='UTC'))
25+
26+
print(pd_dataframe.to_records)
27+
28+
29+
# calculator.add(
30+
# SeriesPayment(
31+
# numberOf: 6,
32+
# label: 'Instalment',
33+
# value: null,
34+
# mode: Mode.arrear,
35+
# ),
36+
# )
37+
38+
# // 3. Calculate the unknown cash flow value
39+
# final valueResult = await calculator.solveValue(
40+
# dayCount: const US30360(),
41+
# interestRate: 0.0825,
42+
# );

0 commit comments

Comments
 (0)