rangekeeper 0.7.8__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.
Files changed (42) hide show
  1. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/PKG-INFO +5 -6
  2. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/README.md +4 -5
  3. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/PKG-INFO +5 -6
  4. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/SOURCES.txt +0 -1
  5. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/pyproject.toml +1 -1
  6. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/flux.py +21 -1
  7. rangekeeper-0.8.0/rangekeeper/formula/financial.py +404 -0
  8. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_formulas.py +191 -61
  9. rangekeeper-0.8.0/tests/test_measures.py +102 -0
  10. rangekeeper-0.7.8/rangekeeper/conftest.py +0 -2
  11. rangekeeper-0.7.8/rangekeeper/formula/financial.py +0 -232
  12. rangekeeper-0.7.8/tests/test_measures.py +0 -108
  13. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/dependency_links.txt +0 -0
  14. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/requires.txt +0 -0
  15. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/top_level.txt +0 -0
  16. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/__init__.py +0 -0
  17. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/api.py +0 -0
  18. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/distribution.py +0 -0
  19. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/duration.py +0 -0
  20. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/__init__.py +0 -0
  21. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/black_swan.py +0 -0
  22. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/cyclicality.py +0 -0
  23. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/market.py +0 -0
  24. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/noise.py +0 -0
  25. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/trend.py +0 -0
  26. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/volatility.py +0 -0
  27. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/extrapolation.py +0 -0
  28. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/format.py +0 -0
  29. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/formula/__init__.py +0 -0
  30. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/graph.py +0 -0
  31. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/measure.py +0 -0
  32. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/policy.py +0 -0
  33. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/projection.py +0 -0
  34. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/segmentation.py +0 -0
  35. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/space.py +0 -0
  36. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/setup.cfg +0 -0
  37. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_api.py +0 -0
  38. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_dynamics.py +0 -0
  39. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_graph.py +0 -0
  40. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_models.py +0 -0
  41. {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_modules.py +0 -0
  42. {rangekeeper-0.7.8 → 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.7.8
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
@@ -43,11 +43,10 @@ environment and dependency management:
43
43
  3. Create a virtual environment: `uv venv .venv`
44
44
  4. Activate the virtual environment: `source .venv/bin/activate`
45
45
  5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
46
- 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
47
- (https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
46
+ 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv](https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
48
47
 
49
48
  ### Publishing
50
- 1. First, remove any previously built packages: `rm -rf dist/*`
49
+ 1. First, remove any previously built packages: `rm -rf dist/`
51
50
  2. Build the package: `uv build`
52
- 3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
53
- '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
51
+ 3. Then, publish it to PyPI with your `UV_PUBLISH_TOKEN` recorded in the .env file: `export $(grep -v
52
+ '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN`
@@ -16,11 +16,10 @@ environment and dependency management:
16
16
  3. Create a virtual environment: `uv venv .venv`
17
17
  4. Activate the virtual environment: `source .venv/bin/activate`
18
18
  5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
19
- 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
20
- (https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
19
+ 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv](https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
21
20
 
22
21
  ### Publishing
23
- 1. First, remove any previously built packages: `rm -rf dist/*`
22
+ 1. First, remove any previously built packages: `rm -rf dist/`
24
23
  2. Build the package: `uv build`
25
- 3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
26
- '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
24
+ 3. Then, publish it to PyPI with your `UV_PUBLISH_TOKEN` recorded in the .env file: `export $(grep -v
25
+ '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rangekeeper
3
- Version: 0.7.8
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
@@ -43,11 +43,10 @@ environment and dependency management:
43
43
  3. Create a virtual environment: `uv venv .venv`
44
44
  4. Activate the virtual environment: `source .venv/bin/activate`
45
45
  5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
46
- 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
47
- (https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
46
+ 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv](https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
48
47
 
49
48
  ### Publishing
50
- 1. First, remove any previously built packages: `rm -rf dist/*`
49
+ 1. First, remove any previously built packages: `rm -rf dist/`
51
50
  2. Build the package: `uv build`
52
- 3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
53
- '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
51
+ 3. Then, publish it to PyPI with your `UV_PUBLISH_TOKEN` recorded in the .env file: `export $(grep -v
52
+ '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN`
@@ -7,7 +7,6 @@ Rangekeeper.egg-info/requires.txt
7
7
  Rangekeeper.egg-info/top_level.txt
8
8
  rangekeeper/__init__.py
9
9
  rangekeeper/api.py
10
- rangekeeper/conftest.py
11
10
  rangekeeper/distribution.py
12
11
  rangekeeper/duration.py
13
12
  rangekeeper/extrapolation.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Rangekeeper"
3
- version = "0.7.8"
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,
@@ -840,7 +860,7 @@ class Stream:
840
860
 
841
861
  def extend(
842
862
  self,
843
- flows: [Flow],
863
+ flows: List[Flow],
844
864
  ):
845
865
  """
846
866
  Returns a new Stream with additional Flows
@@ -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)