rangekeeper 0.7.9__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/PKG-INFO +1 -1
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/Rangekeeper.egg-info/PKG-INFO +1 -1
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/Rangekeeper.egg-info/SOURCES.txt +0 -1
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/pyproject.toml +1 -1
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/flux.py +20 -0
- rangekeeper-0.8.0/rangekeeper/formula/financial.py +404 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/tests/test_formulas.py +191 -69
- rangekeeper-0.8.0/tests/test_measures.py +102 -0
- rangekeeper-0.7.9/rangekeeper/conftest.py +0 -2
- rangekeeper-0.7.9/rangekeeper/formula/financial.py +0 -234
- rangekeeper-0.7.9/tests/test_measures.py +0 -108
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/README.md +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/Rangekeeper.egg-info/dependency_links.txt +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/Rangekeeper.egg-info/requires.txt +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/Rangekeeper.egg-info/top_level.txt +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/__init__.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/api.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/distribution.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/duration.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/dynamics/__init__.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/dynamics/black_swan.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/dynamics/cyclicality.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/dynamics/market.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/dynamics/noise.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/dynamics/trend.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/dynamics/volatility.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/extrapolation.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/format.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/formula/__init__.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/graph.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/measure.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/policy.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/projection.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/segmentation.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/rangekeeper/space.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/setup.cfg +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/tests/test_api.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/tests/test_dynamics.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/tests/test_graph.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/tests/test_models.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/tests/test_modules.py +0 -0
- {rangekeeper-0.7.9 → rangekeeper-0.8.0}/tests/test_projections.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rangekeeper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
|
|
5
5
|
Author-email: Daniel Fink <danfink@mit.edu>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rangekeeper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
|
|
5
5
|
Author-email: Daniel Fink <danfink@mit.edu>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "Rangekeeper"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Daniel Fink", email = "danfink@mit.edu" }
|
|
@@ -330,6 +330,26 @@ class Flow:
|
|
|
330
330
|
amounts=self.movements.to_list(),
|
|
331
331
|
)
|
|
332
332
|
|
|
333
|
+
def diff(
|
|
334
|
+
self,
|
|
335
|
+
with_previous: bool = True,
|
|
336
|
+
name: str = None,
|
|
337
|
+
) -> Flow:
|
|
338
|
+
"""
|
|
339
|
+
Returns a Flow with the deltas between each sequential movement
|
|
340
|
+
"""
|
|
341
|
+
if not with_previous:
|
|
342
|
+
return self.__class__(
|
|
343
|
+
movements=self.movements.copy(deep=True).diff(periods=-1),
|
|
344
|
+
units=self.units,
|
|
345
|
+
name=name if name is not None else self.name + " (diff)",
|
|
346
|
+
)
|
|
347
|
+
return self.__class__(
|
|
348
|
+
movements=self.movements.copy(deep=True).diff(),
|
|
349
|
+
units=self.units,
|
|
350
|
+
name=name if name is not None else self.name + " (diff)",
|
|
351
|
+
)
|
|
352
|
+
|
|
333
353
|
def resample(
|
|
334
354
|
self,
|
|
335
355
|
frequency: rk.duration.Type,
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import numba
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import pint
|
|
10
|
+
|
|
11
|
+
import rangekeeper as rk
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Account:
|
|
15
|
+
startings: rk.flux.Flow
|
|
16
|
+
endings: rk.flux.Flow
|
|
17
|
+
overdraft: rk.flux.Flow
|
|
18
|
+
interest: rk.flux.Flow
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
# @numba.jit
|
|
22
|
+
def _calculate(
|
|
23
|
+
starting: float,
|
|
24
|
+
transactions: numba.typed.List,
|
|
25
|
+
rates: numba.typed.List,
|
|
26
|
+
type: str,
|
|
27
|
+
arrears: bool = False,
|
|
28
|
+
) -> (
|
|
29
|
+
numba.typed.List,
|
|
30
|
+
numba.typed.List,
|
|
31
|
+
numba.typed.List,
|
|
32
|
+
numba.typed.List,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Calculate the balance of a financial account given a starting balance and a series of transactions.
|
|
36
|
+
Returns a tuple of (startings, endings, overdraft, interest) as `numba.typed.List`s
|
|
37
|
+
"""
|
|
38
|
+
startings = numba.typed.List.empty_list(numba.float64)
|
|
39
|
+
endings = numba.typed.List.empty_list(numba.float64)
|
|
40
|
+
overdrafts = numba.typed.List.empty_list(numba.float64)
|
|
41
|
+
interests = numba.typed.List.empty_list(numba.float64)
|
|
42
|
+
|
|
43
|
+
for i in range(len(transactions)):
|
|
44
|
+
if i == 0:
|
|
45
|
+
principal = starting + (0 if arrears else transactions[i])
|
|
46
|
+
startings.append(np.float64(starting) if starting > 0 else 0)
|
|
47
|
+
else:
|
|
48
|
+
startings.append(endings[-1])
|
|
49
|
+
principal = (
|
|
50
|
+
overdrafts[-1] + startings[i] + (0 if arrears else transactions[i])
|
|
51
|
+
)
|
|
52
|
+
# overdrafts.append(overdrafts[-1])
|
|
53
|
+
|
|
54
|
+
# principal = overdrafts[-1] + startings[i] + transactions[i]
|
|
55
|
+
if type == "simple":
|
|
56
|
+
interest = principal * rates[i] if principal > 0 else 0
|
|
57
|
+
principal = principal
|
|
58
|
+
|
|
59
|
+
elif type == "compound":
|
|
60
|
+
interest = principal * rates[i] if principal > 0 else 0
|
|
61
|
+
principal = principal + interest
|
|
62
|
+
# endings.append(principal + interest)
|
|
63
|
+
|
|
64
|
+
elif type == "capitalized":
|
|
65
|
+
# Since we are capitalizing interest, the amount (draw) must include interest to pay on the principal.
|
|
66
|
+
# Derived from i = r * (P + i)
|
|
67
|
+
interest = (
|
|
68
|
+
(principal * rates[i]) / (1 - rates[i]) if principal > 0 else 0
|
|
69
|
+
)
|
|
70
|
+
principal = principal + interest
|
|
71
|
+
# endings.append(principal + interest)
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
raise ValueError(f"Invalid instrument: {type}")
|
|
75
|
+
|
|
76
|
+
if arrears:
|
|
77
|
+
principal += transactions[i]
|
|
78
|
+
|
|
79
|
+
if principal < 0:
|
|
80
|
+
overdrafts.append(principal)
|
|
81
|
+
endings.append(0)
|
|
82
|
+
else:
|
|
83
|
+
overdrafts.append(0)
|
|
84
|
+
endings.append(principal)
|
|
85
|
+
|
|
86
|
+
interests.append(interest)
|
|
87
|
+
|
|
88
|
+
return (startings, endings, overdrafts, interests)
|
|
89
|
+
|
|
90
|
+
class Type(enum.Enum):
|
|
91
|
+
SIMPLE = "simple"
|
|
92
|
+
COMPOUND = "compound"
|
|
93
|
+
CAPITALIZED = "capitalized"
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
starting: float,
|
|
98
|
+
transactions: rk.flux.Flow,
|
|
99
|
+
frequency: rk.duration.Type,
|
|
100
|
+
rate: Union[float, rk.flux.Flow] = 0.0,
|
|
101
|
+
type: Type = Type.SIMPLE,
|
|
102
|
+
arrears: bool = False,
|
|
103
|
+
name: str = "",
|
|
104
|
+
):
|
|
105
|
+
transactions = transactions.resample(frequency=frequency)
|
|
106
|
+
name = f"{name.strip()} " # Add space to end of name
|
|
107
|
+
|
|
108
|
+
if isinstance(rate, float):
|
|
109
|
+
rate = rk.flux.Flow.from_projection(
|
|
110
|
+
name=f"{name}Interest Rates",
|
|
111
|
+
value=rate,
|
|
112
|
+
proj=rk.projection.Extrapolation(
|
|
113
|
+
form=rk.extrapolation.Recurring(),
|
|
114
|
+
sequence=rk.duration.Sequence.from_datestamps(
|
|
115
|
+
datestamps=transactions.movements.index,
|
|
116
|
+
frequency=frequency,
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
assert len(rate.movements.index) == len(
|
|
122
|
+
transactions.movements.index
|
|
123
|
+
), "Rate must be a Flow with the same number of periods as transactions."
|
|
124
|
+
|
|
125
|
+
startings, endings, overdraft, interest = self._calculate(
|
|
126
|
+
starting=starting,
|
|
127
|
+
transactions=numba.typed.List(transactions.movements.to_list()),
|
|
128
|
+
rates=numba.typed.List(rate.movements.to_list()),
|
|
129
|
+
type=type.value,
|
|
130
|
+
arrears=arrears,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self.name = f"{name}Account"
|
|
134
|
+
|
|
135
|
+
self.startings = rk.flux.Flow(
|
|
136
|
+
movements=pd.Series(
|
|
137
|
+
startings,
|
|
138
|
+
index=transactions.movements.index,
|
|
139
|
+
),
|
|
140
|
+
units=transactions.units,
|
|
141
|
+
name=f"{name}Start Balance",
|
|
142
|
+
)
|
|
143
|
+
self.endings = rk.flux.Flow(
|
|
144
|
+
movements=pd.Series(
|
|
145
|
+
endings,
|
|
146
|
+
index=transactions.movements.index,
|
|
147
|
+
),
|
|
148
|
+
units=transactions.units,
|
|
149
|
+
name=f"{name}End Balance",
|
|
150
|
+
)
|
|
151
|
+
self.overdraft = rk.flux.Flow(
|
|
152
|
+
movements=pd.Series(
|
|
153
|
+
overdraft,
|
|
154
|
+
index=transactions.movements.index,
|
|
155
|
+
),
|
|
156
|
+
units=transactions.units,
|
|
157
|
+
name=f"{name}Overdraft",
|
|
158
|
+
)
|
|
159
|
+
self.interest = rk.flux.Flow(
|
|
160
|
+
movements=pd.Series(
|
|
161
|
+
interest,
|
|
162
|
+
index=transactions.movements.index,
|
|
163
|
+
),
|
|
164
|
+
units=transactions.units,
|
|
165
|
+
name=f"{name}Interest Amounts",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def diff(self, name: str = None) -> rk.flux.Flow:
|
|
169
|
+
"""
|
|
170
|
+
Calculate the difference between the start and end balances.
|
|
171
|
+
"""
|
|
172
|
+
name = self.name if name is None else name
|
|
173
|
+
return rk.flux.Flow(
|
|
174
|
+
movements=self.endings.movements - self.startings.movements,
|
|
175
|
+
units=self.startings.units,
|
|
176
|
+
name=f"{self.startings.name} (diff)",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# class Balance:
|
|
181
|
+
# startings: rk.flux.Flow
|
|
182
|
+
# endings: rk.flux.Flow
|
|
183
|
+
# overdraft: rk.flux.Flow
|
|
184
|
+
#
|
|
185
|
+
# def __init__(
|
|
186
|
+
# self,
|
|
187
|
+
# starting: float,
|
|
188
|
+
# transactions: rk.flux.Flow,
|
|
189
|
+
# frequency: rk.duration.Type,
|
|
190
|
+
# name: str = "",
|
|
191
|
+
# ):
|
|
192
|
+
# """
|
|
193
|
+
# Calculate the balance of a financial account given a starting balance and a series of transactions.
|
|
194
|
+
# Returns an object with the starting and ending balances, as well as the overdraft amount, as `Flow`s.
|
|
195
|
+
# Overdraft is the portion of transactions occurring while the balance is non-positive.
|
|
196
|
+
# """
|
|
197
|
+
#
|
|
198
|
+
# transactions = transactions.resample(frequency=frequency)
|
|
199
|
+
#
|
|
200
|
+
# result = self._calculate_balance(
|
|
201
|
+
# startings=numba.typed.List([np.float64(starting)]),
|
|
202
|
+
# transactions=numba.typed.List(transactions.movements.to_list()),
|
|
203
|
+
# )
|
|
204
|
+
#
|
|
205
|
+
# self.startings = rk.flux.Flow(
|
|
206
|
+
# movements=pd.Series(
|
|
207
|
+
# result[0],
|
|
208
|
+
# index=transactions.movements.index,
|
|
209
|
+
# ),
|
|
210
|
+
# units=transactions.units,
|
|
211
|
+
# name=f"{name} Start Balance",
|
|
212
|
+
# )
|
|
213
|
+
#
|
|
214
|
+
# self.endings = rk.flux.Flow(
|
|
215
|
+
# movements=pd.Series(
|
|
216
|
+
# result[1],
|
|
217
|
+
# index=transactions.movements.index,
|
|
218
|
+
# ),
|
|
219
|
+
# units=transactions.units,
|
|
220
|
+
# name=f"{name} End Balance",
|
|
221
|
+
# )
|
|
222
|
+
#
|
|
223
|
+
# self.overdraft = rk.flux.Flow(
|
|
224
|
+
# movements=pd.Series(
|
|
225
|
+
# result[2],
|
|
226
|
+
# index=transactions.movements.index,
|
|
227
|
+
# ),
|
|
228
|
+
# units=transactions.units,
|
|
229
|
+
# name=f"{name} Overdraft",
|
|
230
|
+
# )
|
|
231
|
+
#
|
|
232
|
+
# @classmethod
|
|
233
|
+
# def from_flows(
|
|
234
|
+
# cls,
|
|
235
|
+
# startings: rk.flux.Flow,
|
|
236
|
+
# endings: rk.flux.Flow,
|
|
237
|
+
# frequency: rk.duration.Type,
|
|
238
|
+
# name: str = "",
|
|
239
|
+
# ):
|
|
240
|
+
# name = startings.name if name is None else name
|
|
241
|
+
# if startings.units != endings.units:
|
|
242
|
+
# raise ValueError("The units of the starting and ending flows must match.")
|
|
243
|
+
# units = startings.units
|
|
244
|
+
#
|
|
245
|
+
# startings = startings.resample(frequency=frequency)
|
|
246
|
+
# endings = endings.resample(frequency=frequency)
|
|
247
|
+
#
|
|
248
|
+
# transactions = rk.flux.Flow(
|
|
249
|
+
# movements=endings.movements - startings.movements,
|
|
250
|
+
# units=units,
|
|
251
|
+
# )
|
|
252
|
+
# starting = startings.movements.iloc[0]
|
|
253
|
+
#
|
|
254
|
+
# return cls(
|
|
255
|
+
# starting=starting,
|
|
256
|
+
# transactions=transactions,
|
|
257
|
+
# frequency=frequency,
|
|
258
|
+
# name=name,
|
|
259
|
+
# )
|
|
260
|
+
#
|
|
261
|
+
# @staticmethod
|
|
262
|
+
# @numba.jit
|
|
263
|
+
# def _calculate_balance(
|
|
264
|
+
# startings: numba.typed.List,
|
|
265
|
+
# transactions: numba.typed.List,
|
|
266
|
+
# ) -> (
|
|
267
|
+
# numba.typed.List,
|
|
268
|
+
# numba.typed.List,
|
|
269
|
+
# numba.typed.List,
|
|
270
|
+
# ):
|
|
271
|
+
#
|
|
272
|
+
# endings = numba.typed.List.empty_list(numba.float64)
|
|
273
|
+
# overdrafts = numba.typed.List.empty_list(numba.float64)
|
|
274
|
+
# for i in range(len(transactions)):
|
|
275
|
+
# ending = float(startings[-1] + transactions[i])
|
|
276
|
+
#
|
|
277
|
+
# overdrafts.append(ending if ending < 0 else 0)
|
|
278
|
+
# endings.append(ending if ending > 0 else 0)
|
|
279
|
+
# startings.append(endings[-1])
|
|
280
|
+
#
|
|
281
|
+
# return (
|
|
282
|
+
# startings[:-1],
|
|
283
|
+
# endings,
|
|
284
|
+
# overdrafts,
|
|
285
|
+
# )
|
|
286
|
+
#
|
|
287
|
+
#
|
|
288
|
+
# class Interest:
|
|
289
|
+
# amounts: rk.flux.Flow
|
|
290
|
+
# balance: Balance
|
|
291
|
+
#
|
|
292
|
+
# class Type(enum.Enum):
|
|
293
|
+
# INTEREST_ONLY = "interest_only"
|
|
294
|
+
# COMPOUND = "compound"
|
|
295
|
+
# CAPITALIZED = "capitalized"
|
|
296
|
+
#
|
|
297
|
+
# def __init__(
|
|
298
|
+
# self,
|
|
299
|
+
# rate: float | rk.flux.Flow,
|
|
300
|
+
# transactions: rk.flux.Flow,
|
|
301
|
+
# frequency: rk.duration.Type,
|
|
302
|
+
# type: Type = Type.COMPOUND,
|
|
303
|
+
# ):
|
|
304
|
+
# """
|
|
305
|
+
# Calculate interest expense on a loan or other interest-bearing liability,
|
|
306
|
+
# described by a series of transactions.
|
|
307
|
+
# Assumes interest is compounded on a positive, outstanding balance per period
|
|
308
|
+
# (i.e. draws are positive, payments are negative).
|
|
309
|
+
# Make sure the rate is consistent with the frequency.
|
|
310
|
+
# """
|
|
311
|
+
#
|
|
312
|
+
# transactions = transactions.resample(frequency=frequency)
|
|
313
|
+
#
|
|
314
|
+
# if isinstance(rate, float):
|
|
315
|
+
# rate = rk.flux.Flow.from_projection(
|
|
316
|
+
# name="Interest Rates",
|
|
317
|
+
# value=rate,
|
|
318
|
+
# proj=rk.projection.Extrapolation(
|
|
319
|
+
# form=rk.extrapolation.Recurring(),
|
|
320
|
+
# sequence=rk.duration.Sequence.from_datestamps(
|
|
321
|
+
# datestamps=transactions.movements.index, frequency=frequency
|
|
322
|
+
# ),
|
|
323
|
+
# ),
|
|
324
|
+
# )
|
|
325
|
+
# else:
|
|
326
|
+
# raise NotImplementedError("Only float rates are supported currently.")
|
|
327
|
+
#
|
|
328
|
+
# if len(transactions.movements) != len(rate.movements):
|
|
329
|
+
# raise ValueError(
|
|
330
|
+
# "The number of transactions must match the number of rates."
|
|
331
|
+
# )
|
|
332
|
+
#
|
|
333
|
+
# startings, endings, amounts = self._calc_interest(
|
|
334
|
+
# transactions=numba.typed.List(transactions.movements.to_list()),
|
|
335
|
+
# rates=numba.typed.List(rate.movements.to_list()),
|
|
336
|
+
# type=type.value,
|
|
337
|
+
# )
|
|
338
|
+
#
|
|
339
|
+
# startings = rk.flux.Flow(
|
|
340
|
+
# movements=pd.Series(startings, index=transactions.movements.index),
|
|
341
|
+
# units=transactions.units,
|
|
342
|
+
# )
|
|
343
|
+
# endings = rk.flux.Flow(
|
|
344
|
+
# movements=pd.Series(endings, index=transactions.movements.index),
|
|
345
|
+
# units=transactions.units,
|
|
346
|
+
# )
|
|
347
|
+
#
|
|
348
|
+
# self.balance = rk.formula.financial.Balance.from_flows(
|
|
349
|
+
# startings=startings,
|
|
350
|
+
# endings=endings,
|
|
351
|
+
# frequency=frequency,
|
|
352
|
+
# )
|
|
353
|
+
#
|
|
354
|
+
# self.amounts = rk.flux.Flow(
|
|
355
|
+
# movements=pd.Series(amounts, index=transactions.movements.index),
|
|
356
|
+
# units=transactions.units,
|
|
357
|
+
# name="Interest",
|
|
358
|
+
# )
|
|
359
|
+
#
|
|
360
|
+
# @staticmethod
|
|
361
|
+
# @numba.jit
|
|
362
|
+
# def _calc_interest(
|
|
363
|
+
# transactions: numba.typed.List,
|
|
364
|
+
# rates: numba.typed.List,
|
|
365
|
+
# type: str,
|
|
366
|
+
# ) -> (
|
|
367
|
+
# numba.typed.List,
|
|
368
|
+
# numba.typed.List,
|
|
369
|
+
# numba.typed.List,
|
|
370
|
+
# ):
|
|
371
|
+
#
|
|
372
|
+
# startings = numba.typed.List.empty_list(numba.float64)
|
|
373
|
+
# endings = numba.typed.List.empty_list(numba.float64)
|
|
374
|
+
# amounts = numba.typed.List.empty_list(numba.float64)
|
|
375
|
+
#
|
|
376
|
+
# for i in range(len(transactions)):
|
|
377
|
+
# if i == 0:
|
|
378
|
+
# startings.append(0)
|
|
379
|
+
# else:
|
|
380
|
+
# startings.append(endings[-1])
|
|
381
|
+
#
|
|
382
|
+
# principal = startings[i] + transactions[i]
|
|
383
|
+
# if type == "interest_only":
|
|
384
|
+
# interest = principal * rates[i] if principal > 0 else 0
|
|
385
|
+
# endings.append(principal)
|
|
386
|
+
#
|
|
387
|
+
# elif type == "compound":
|
|
388
|
+
# interest = principal * rates[i] if principal > 0 else 0
|
|
389
|
+
# endings.append(principal + interest)
|
|
390
|
+
#
|
|
391
|
+
# elif type == "capitalized":
|
|
392
|
+
# # Since we are capitalizing interest, the amount (draw) must include interest to pay on the principal.
|
|
393
|
+
# # Derived from i = r * (P + i)
|
|
394
|
+
# interest = (
|
|
395
|
+
# (principal * rates[i]) / (1 - rates[i]) if principal > 0 else 0
|
|
396
|
+
# )
|
|
397
|
+
# endings.append(principal + interest)
|
|
398
|
+
#
|
|
399
|
+
# else:
|
|
400
|
+
# raise ValueError(f"Invalid instrument: {type}")
|
|
401
|
+
#
|
|
402
|
+
# amounts.append(interest)
|
|
403
|
+
#
|
|
404
|
+
# return (startings, endings, amounts)
|