rangekeeper 0.7.2.post4__tar.gz → 0.7.4__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.2.post4 → rangekeeper-0.7.4}/PKG-INFO +7 -10
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/pyproject.toml +8 -11
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/flux.py +1 -1
- rangekeeper-0.7.4/rangekeeper/formula/__init__.py +1 -0
- rangekeeper-0.7.2.post4/rangekeeper/formula.py → rangekeeper-0.7.4/rangekeeper/formula/financial.py +112 -101
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/span.py +3 -3
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/README.md +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/__init__.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/api.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/conftest.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/distribution.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/duration.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/__init__.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/black_swan.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/cyclicality.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/market.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/noise.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/trend.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/volatility.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/extrapolation.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/format.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/graph.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/measure.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/policy.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/projection.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/segmentation.py +0 -0
- {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/space.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: Rangekeeper
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
|
|
5
5
|
License: MPL-2.0
|
|
6
6
|
Author: Daniel Fink
|
|
@@ -12,23 +12,20 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: aenum (>=3.1,<4.0)
|
|
15
|
-
Requires-Dist: matplotlib (>=3.
|
|
16
|
-
Requires-Dist: mpld3 (>=0.5,<0.6)
|
|
15
|
+
Requires-Dist: matplotlib (>=3.10,<4.0)
|
|
17
16
|
Requires-Dist: multiprocess (>=0.70,<0.71)
|
|
18
17
|
Requires-Dist: networkx (>=3.3,<4.0)
|
|
19
|
-
Requires-Dist: numba (>=0.
|
|
18
|
+
Requires-Dist: numba (>=0.61,<0.62)
|
|
20
19
|
Requires-Dist: numpy-financial (>=1.0,<2.0)
|
|
21
|
-
Requires-Dist: pandas (>=2,<3)
|
|
20
|
+
Requires-Dist: pandas (>=2.2,<3.0)
|
|
22
21
|
Requires-Dist: pint (>=0.24,<0.25)
|
|
23
|
-
Requires-Dist: plotly (>=
|
|
22
|
+
Requires-Dist: plotly (>=6.0,<7.0)
|
|
24
23
|
Requires-Dist: py-moneyed (>=3.0,<4.0)
|
|
25
|
-
Requires-Dist: pyarrow (>=18.1,<19.0)
|
|
26
24
|
Requires-Dist: pyvis (>=0.3,<0.4)
|
|
27
25
|
Requires-Dist: pyxirr (>=0.10,<0.11)
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist: scipy (>=1.13,<2.0)
|
|
26
|
+
Requires-Dist: scipy (>=1.14,<2.0)
|
|
30
27
|
Requires-Dist: seaborn (>=0.13,<0.14)
|
|
31
|
-
Requires-Dist: specklepy (>=2.
|
|
28
|
+
Requires-Dist: specklepy (>=2.21,<3.0)
|
|
32
29
|
Requires-Dist: tabulate (>=0.9,<0.10)
|
|
33
30
|
Description-Content-Type: text/markdown
|
|
34
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "Rangekeeper"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.4"
|
|
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 = ["Daniel Fink <danfink@mit.edu>"]
|
|
6
6
|
license = "MPL-2.0"
|
|
@@ -8,28 +8,25 @@ readme = "README.md"
|
|
|
8
8
|
|
|
9
9
|
[tool.poetry.dependencies]
|
|
10
10
|
python = ">=3.10, <3.13"
|
|
11
|
-
matplotlib = "^3.
|
|
12
|
-
pandas = "^2"
|
|
11
|
+
matplotlib = "^3.10"
|
|
12
|
+
pandas = "^2.2"
|
|
13
13
|
pint = "^0.24"
|
|
14
14
|
pyxirr = "^0.10"
|
|
15
|
-
scipy = "^1.
|
|
15
|
+
scipy = "^1.14"
|
|
16
16
|
networkx = "^3.3"
|
|
17
17
|
py-moneyed = "^3.0"
|
|
18
18
|
tabulate = "^0.9"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
numba = "^0.60"
|
|
19
|
+
specklepy = "^2.21"
|
|
20
|
+
numba = "^0.61"
|
|
22
21
|
multiprocess = "^0.70"
|
|
23
22
|
seaborn = "^0.13"
|
|
24
23
|
pyvis = "^0.3"
|
|
25
|
-
plotly = "^
|
|
26
|
-
mpld3 = "^0.5"
|
|
27
|
-
pyarrow = "^18.1"
|
|
24
|
+
plotly = "^6.0"
|
|
28
25
|
aenum = "^3.1"
|
|
29
26
|
numpy-financial = "^1.0"
|
|
30
27
|
|
|
31
28
|
[tool.poetry.dev-dependencies]
|
|
32
|
-
pytest = "^8.
|
|
29
|
+
pytest = "^8.3"
|
|
33
30
|
|
|
34
31
|
[build-system]
|
|
35
32
|
requires = ["setuptools", "poetry-core>=1.0.0"]
|
|
@@ -293,7 +293,7 @@ class Flow:
|
|
|
293
293
|
self,
|
|
294
294
|
frequency: rk.duration.Type) -> Flow:
|
|
295
295
|
"""
|
|
296
|
-
Returns a Flow with movements summed to specified frequency of
|
|
296
|
+
Returns a Flow with movements summed to specified frequency of periods
|
|
297
297
|
"""
|
|
298
298
|
return rk.flux.Flow(
|
|
299
299
|
movements=self.movements.copy(deep=True).resample(rule=rk.duration.Type.period(frequency)).sum(),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import financial as financial
|
rangekeeper-0.7.2.post4/rangekeeper/formula.py → rangekeeper-0.7.4/rangekeeper/formula/financial.py
RENAMED
|
@@ -1,29 +1,104 @@
|
|
|
1
|
-
from typing import Callable, Optional
|
|
2
1
|
import enum
|
|
2
|
+
import dataclasses
|
|
3
3
|
import numpy as np
|
|
4
|
-
from numba import jit
|
|
5
4
|
import numba
|
|
6
5
|
import pandas as pd
|
|
7
|
-
import scipy.optimize as opt
|
|
8
6
|
|
|
9
7
|
import rangekeeper as rk
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
class
|
|
10
|
+
class Balance:
|
|
11
|
+
startings: rk.flux.Flow
|
|
12
|
+
endings: rk.flux.Flow
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
starting: float,
|
|
17
|
+
transactions: rk.flux.Flow,
|
|
18
|
+
frequency: rk.duration.Type,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Calculate the balance of a financial account given a starting balance and a series of transactions.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
transactions = transactions.resample(frequency=frequency)
|
|
25
|
+
|
|
26
|
+
result = self._calculate_balance(
|
|
27
|
+
startings=numba.typed.List([np.float64(starting)]),
|
|
28
|
+
transactions=numba.typed.List(transactions.movements.to_list()))
|
|
29
|
+
|
|
30
|
+
self.startings=rk.flux.Flow(
|
|
31
|
+
movements=pd.Series(result[0], index=transactions.movements.index),
|
|
32
|
+
units=transactions.units,
|
|
33
|
+
name='Start Balance')
|
|
34
|
+
|
|
35
|
+
self.endings=rk.flux.Flow(
|
|
36
|
+
movements=pd.Series(result[1], index=transactions.movements.index),
|
|
37
|
+
units=transactions.units,
|
|
38
|
+
name='End Balance')
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_flows(
|
|
42
|
+
cls,
|
|
43
|
+
startings: rk.flux.Flow,
|
|
44
|
+
endings: rk.flux.Flow,
|
|
45
|
+
):
|
|
46
|
+
balance = super().__new__(cls)
|
|
47
|
+
balance.startings = startings
|
|
48
|
+
balance.endings = endings
|
|
49
|
+
|
|
50
|
+
return balance
|
|
51
|
+
|
|
52
|
+
def overdraft(
|
|
53
|
+
self,
|
|
54
|
+
name: str = None,
|
|
55
|
+
) -> rk.flux.Flow:
|
|
56
|
+
"""
|
|
57
|
+
Identifies the portion of transactions occurring while the balance is negative.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
starts = self.startings.movements.where(self.startings.movements < 0).fillna(0)
|
|
61
|
+
ends = self.endings.movements.where(self.endings.movements < 0).fillna(0)
|
|
62
|
+
|
|
63
|
+
overdrafts = ends - starts
|
|
64
|
+
overdrafts = overdrafts.where(overdrafts < 0).dropna()
|
|
65
|
+
|
|
66
|
+
return rk.flux.Flow(
|
|
67
|
+
movements=overdrafts,
|
|
68
|
+
units=self.startings.units,
|
|
69
|
+
name='Overdraft' if name is None else name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
@numba.jit
|
|
74
|
+
def _calculate_balance(
|
|
75
|
+
startings: numba.typed.List,
|
|
76
|
+
transactions: numba.typed.List) -> (numba.typed.List, numba.typed.List):
|
|
77
|
+
|
|
78
|
+
endings = numba.typed.List.empty_list(numba.float64)
|
|
79
|
+
for i in range(len(transactions)):
|
|
80
|
+
endings.append(float(startings[-1] + transactions[i]))
|
|
81
|
+
startings.append(endings[-1])
|
|
82
|
+
|
|
83
|
+
return (startings[:-1], endings)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Interest:
|
|
87
|
+
amounts: rk.flux.Flow
|
|
88
|
+
balance: Balance
|
|
89
|
+
|
|
90
|
+
class Type(enum.Enum):
|
|
15
91
|
INTEREST_ONLY = 'interest_only'
|
|
16
92
|
COMPOUND = 'compound'
|
|
17
93
|
CAPITALIZED = 'capitalized'
|
|
18
94
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def interest(
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
22
97
|
rate: float | rk.flux.Flow,
|
|
23
98
|
transactions: rk.flux.Flow,
|
|
24
99
|
frequency: rk.duration.Type,
|
|
25
|
-
|
|
26
|
-
|
|
100
|
+
type: Type = Type.COMPOUND,
|
|
101
|
+
):
|
|
27
102
|
"""
|
|
28
103
|
Calculate interest expense on a loan or other interest-bearing liability,
|
|
29
104
|
described by a series of transactions.
|
|
@@ -43,47 +118,45 @@ class Financial:
|
|
|
43
118
|
sequence=rk.duration.Sequence.from_datestamps(
|
|
44
119
|
datestamps=transactions.movements.index,
|
|
45
120
|
frequency=frequency)))
|
|
121
|
+
else:
|
|
122
|
+
raise NotImplementedError('Only float rates are supported currently.')
|
|
46
123
|
|
|
47
124
|
if len(transactions.movements) != len(rate.movements):
|
|
48
125
|
raise ValueError('The number of transactions must match the number of rates.')
|
|
49
126
|
|
|
50
|
-
startings, endings, interests =
|
|
127
|
+
startings, endings, interests = self._calc_interest(
|
|
51
128
|
transactions=numba.typed.List(transactions.movements.to_list()),
|
|
52
129
|
rates=numba.typed.List(rate.movements.to_list()),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
balance = rk.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
frequency=frequency)
|
|
68
|
-
|
|
69
|
-
interest = rk.flux.Flow(
|
|
130
|
+
type=type.value)
|
|
131
|
+
|
|
132
|
+
self.balance = rk.formula.financial.Balance.from_flows(
|
|
133
|
+
startings=rk.flux.Flow(
|
|
134
|
+
movements=pd.Series(startings, index=transactions.movements.index),
|
|
135
|
+
units=transactions.units,
|
|
136
|
+
name='Start Balance'),
|
|
137
|
+
endings=rk.flux.Flow(
|
|
138
|
+
movements=pd.Series(endings, index=transactions.movements.index),
|
|
139
|
+
units=transactions.units,
|
|
140
|
+
name='End Balance')
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.amounts = rk.flux.Flow(
|
|
70
144
|
movements=pd.Series(interests, index=transactions.movements.index),
|
|
71
145
|
units=transactions.units,
|
|
72
146
|
name='Interest')
|
|
73
147
|
|
|
74
|
-
return (balance, interest)
|
|
75
|
-
|
|
76
148
|
|
|
77
149
|
@staticmethod
|
|
78
150
|
@numba.jit
|
|
79
151
|
def _calc_interest(
|
|
80
152
|
transactions: numba.typed.List,
|
|
81
153
|
rates: numba.typed.List,
|
|
82
|
-
|
|
154
|
+
type: str,
|
|
155
|
+
) -> (numba.typed.List, numba.typed.List, numba.typed.List):
|
|
83
156
|
|
|
84
157
|
startings = numba.typed.List.empty_list(numba.float64)
|
|
85
158
|
endings = numba.typed.List.empty_list(numba.float64)
|
|
86
|
-
|
|
159
|
+
amounts = numba.typed.List.empty_list(numba.float64)
|
|
87
160
|
|
|
88
161
|
for i in range(len(transactions)):
|
|
89
162
|
if i == 0:
|
|
@@ -92,87 +165,25 @@ class Financial:
|
|
|
92
165
|
startings.append(endings[-1])
|
|
93
166
|
|
|
94
167
|
principal = startings[i] + transactions[i]
|
|
95
|
-
if
|
|
168
|
+
if type == 'interest_only':
|
|
96
169
|
interest = principal * rates[i] if principal > 0 else 0
|
|
97
170
|
endings.append(principal)
|
|
98
171
|
|
|
99
|
-
elif
|
|
172
|
+
elif type == 'compound':
|
|
100
173
|
interest = principal * rates[i] if principal > 0 else 0
|
|
101
174
|
endings.append(principal + interest)
|
|
102
175
|
|
|
103
|
-
elif
|
|
176
|
+
elif type == 'capitalized':
|
|
104
177
|
interest = (principal * rates[i]) / (1 - rates[i]) if principal > 0 else 0 # Since we are capitalizing
|
|
105
|
-
# interest, the
|
|
178
|
+
# interest, the amount (draw) must include interest to pay on the principal. Derived from i = r * (P + i)
|
|
106
179
|
endings.append(principal + interest)
|
|
107
180
|
|
|
108
181
|
else:
|
|
109
|
-
raise ValueError(f'Invalid instrument: {
|
|
182
|
+
raise ValueError(f'Invalid instrument: {type}')
|
|
110
183
|
|
|
111
|
-
|
|
184
|
+
amounts.append(interest)
|
|
112
185
|
|
|
113
|
-
return (startings, endings,
|
|
186
|
+
return (startings, endings, amounts)
|
|
114
187
|
|
|
115
188
|
|
|
116
|
-
@staticmethod
|
|
117
|
-
def balance(
|
|
118
|
-
starting: float,
|
|
119
|
-
transactions: rk.flux.Flow,
|
|
120
|
-
frequency: rk.duration.Type,
|
|
121
|
-
name: str = None) -> rk.flux.Stream:
|
|
122
|
-
"""
|
|
123
|
-
Calculate the balance of a financial account given a starting balance and a series of transactions.
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
starting = numba.typed.List([np.float64(starting)])
|
|
127
|
-
transaction_amounts = numba.typed.List(transactions.movements.to_list())
|
|
128
|
-
|
|
129
|
-
balance = rk.formula.Financial._calculate_balance(
|
|
130
|
-
starting=starting,
|
|
131
|
-
transactions=transaction_amounts)
|
|
132
|
-
|
|
133
|
-
flows = [
|
|
134
|
-
rk.flux.Flow(
|
|
135
|
-
movements=pd.Series(record, index=transactions.movements.index),
|
|
136
|
-
units=transactions.units,
|
|
137
|
-
name=name)
|
|
138
|
-
for record, name in zip(balance, ('Start Balance', 'End Balance'))
|
|
139
|
-
]
|
|
140
189
|
|
|
141
|
-
statement = rk.flux.Stream(
|
|
142
|
-
name=name,
|
|
143
|
-
flows=flows,
|
|
144
|
-
frequency=frequency)
|
|
145
|
-
|
|
146
|
-
return statement
|
|
147
|
-
|
|
148
|
-
@staticmethod
|
|
149
|
-
@numba.jit
|
|
150
|
-
def _calculate_balance(
|
|
151
|
-
starting: numba.typed.List,
|
|
152
|
-
transactions: numba.typed.List) -> (numba.typed.List, numba.typed.List):
|
|
153
|
-
|
|
154
|
-
ending = numba.typed.List.empty_list(numba.float64)
|
|
155
|
-
for i in range(len(transactions)):
|
|
156
|
-
ending.append(float(starting[-1] + transactions[i]))
|
|
157
|
-
starting.append(ending[-1])
|
|
158
|
-
return (starting[:-1], ending)
|
|
159
|
-
|
|
160
|
-
@staticmethod
|
|
161
|
-
def overdraft(
|
|
162
|
-
balance: rk.flux.Stream,
|
|
163
|
-
name: str = None) -> rk.flux.Flow:
|
|
164
|
-
"""
|
|
165
|
-
Identifies the portion of transactions occurring while the balance is negative.
|
|
166
|
-
"""
|
|
167
|
-
if len(balance.flows) != 2:
|
|
168
|
-
raise ValueError('Balance must be Stream with two flows; Start and End.')
|
|
169
|
-
if balance.flows[0].units != balance.flows[1].units:
|
|
170
|
-
raise ValueError('Units of the Start and End Flows must be the same.')
|
|
171
|
-
|
|
172
|
-
starts = balance.flows[0].movements.where(balance.flows[0].movements < 0).fillna(0)
|
|
173
|
-
ends = balance.flows[1].movements.where(balance.flows[1].movements < 0).fillna(0)
|
|
174
|
-
|
|
175
|
-
return rk.flux.Flow(
|
|
176
|
-
movements=ends - starts,
|
|
177
|
-
units=balance.flows[0].units,
|
|
178
|
-
name='Overdraft' if name is None else name)
|
|
@@ -41,9 +41,9 @@ class Span:
|
|
|
41
41
|
self.end_date = self._interval.right
|
|
42
42
|
|
|
43
43
|
def __str__(self):
|
|
44
|
-
return 'Span: {}\n' \
|
|
45
|
-
'Start Date: {}\n' \
|
|
46
|
-
'End Date: {
|
|
44
|
+
return f'Span: {self.name}\n' \
|
|
45
|
+
f'Start Date: {self.start_date.date()}\n' \
|
|
46
|
+
f'End Date: {self.end_date.date()}\n'
|
|
47
47
|
|
|
48
48
|
def __repr__(self):
|
|
49
49
|
return self.__str__()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|