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.
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/PKG-INFO +5 -6
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/README.md +4 -5
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/PKG-INFO +5 -6
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/SOURCES.txt +0 -1
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/pyproject.toml +1 -1
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/flux.py +21 -1
- rangekeeper-0.8.0/rangekeeper/formula/financial.py +404 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_formulas.py +191 -61
- rangekeeper-0.8.0/tests/test_measures.py +102 -0
- rangekeeper-0.7.8/rangekeeper/conftest.py +0 -2
- rangekeeper-0.7.8/rangekeeper/formula/financial.py +0 -232
- rangekeeper-0.7.8/tests/test_measures.py +0 -108
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/dependency_links.txt +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/requires.txt +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/Rangekeeper.egg-info/top_level.txt +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/__init__.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/api.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/distribution.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/duration.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/__init__.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/black_swan.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/cyclicality.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/market.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/noise.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/trend.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/dynamics/volatility.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/extrapolation.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/format.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/formula/__init__.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/graph.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/measure.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/policy.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/projection.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/segmentation.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/rangekeeper/space.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/setup.cfg +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_api.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_dynamics.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_graph.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_models.py +0 -0
- {rangekeeper-0.7.8 → rangekeeper-0.8.0}/tests/test_modules.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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`
|
|
@@ -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,
|
|
@@ -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)
|