rangekeeper 0.7.0__tar.gz → 0.7.2__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.0 → rangekeeper-0.7.2}/PKG-INFO +2 -2
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/pyproject.toml +2 -2
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/duration.py +9 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/flux.py +35 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/formula.py +43 -12
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/README.md +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/__init__.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/api.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/conftest.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/distribution.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/__init__.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/black_swan.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/cyclicality.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/market.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/noise.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/trend.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/volatility.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/extrapolation.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/format.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/graph.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/measure.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/policy.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/projection.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/segmentation.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/space.py +0 -0
- {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/span.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.2
|
|
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
|
|
@@ -22,7 +22,7 @@ Requires-Dist: pandas (>=2.2,<3.0)
|
|
|
22
22
|
Requires-Dist: pint (>=0.24,<0.25)
|
|
23
23
|
Requires-Dist: plotly (>=5.22,<6.0)
|
|
24
24
|
Requires-Dist: py-moneyed (>=3.0,<4.0)
|
|
25
|
-
Requires-Dist: pyarrow (>=
|
|
25
|
+
Requires-Dist: pyarrow (>=18.1,<19.0)
|
|
26
26
|
Requires-Dist: pyvis (>=0.3,<0.4)
|
|
27
27
|
Requires-Dist: pyxirr (>=0.10,<0.11)
|
|
28
28
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "Rangekeeper"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.2"
|
|
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"
|
|
@@ -24,7 +24,7 @@ seaborn = "^0.13"
|
|
|
24
24
|
pyvis = "^0.3"
|
|
25
25
|
plotly = "^5.22"
|
|
26
26
|
mpld3 = "^0.5"
|
|
27
|
-
pyarrow = "^
|
|
27
|
+
pyarrow = "^18.1"
|
|
28
28
|
aenum = "^3.1"
|
|
29
29
|
numpy-financial = "^1.0"
|
|
30
30
|
|
|
@@ -213,6 +213,15 @@ class Period:
|
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
class Sequence:
|
|
216
|
+
@staticmethod
|
|
217
|
+
def from_datestamps(
|
|
218
|
+
datestamps: pd.DatetimeIndex,
|
|
219
|
+
frequency: Type) -> pd.PeriodIndex:
|
|
220
|
+
"""
|
|
221
|
+
Returns a pd.PeriodIndex from a pd.DatetimeIndex with Datetimes being Dates
|
|
222
|
+
"""
|
|
223
|
+
return datestamps.to_period(freq=Type.period(frequency))
|
|
224
|
+
|
|
216
225
|
@staticmethod
|
|
217
226
|
def from_bounds(
|
|
218
227
|
include_start: pd.Timestamp,
|
|
@@ -458,6 +458,9 @@ class Stream:
|
|
|
458
458
|
flows=flows,
|
|
459
459
|
frequency=self.frequency)
|
|
460
460
|
|
|
461
|
+
def is_homogeneous(self) -> bool:
|
|
462
|
+
return len(list(set(list(self.units.values())))) == 1
|
|
463
|
+
|
|
461
464
|
def duplicate(self) -> Stream:
|
|
462
465
|
return self.__class__(
|
|
463
466
|
name=self.name,
|
|
@@ -670,6 +673,38 @@ class Stream:
|
|
|
670
673
|
data=self.frame.prod(axis=1).to_list(),
|
|
671
674
|
units=reduced_units)
|
|
672
675
|
|
|
676
|
+
def min(
|
|
677
|
+
self,
|
|
678
|
+
name):
|
|
679
|
+
"""
|
|
680
|
+
Returns a Flow whose movements are the minimum of the Stream's flows by period
|
|
681
|
+
"""
|
|
682
|
+
if not self.is_homogeneous():
|
|
683
|
+
raise ValueError("Error: minimum requires all flows' units to be the same. Units: {0}".format(
|
|
684
|
+
json.dumps(
|
|
685
|
+
{flow.name: flow.units.__str__() for flow in self.flows}, indent=4)))
|
|
686
|
+
else:
|
|
687
|
+
return Flow(
|
|
688
|
+
movements=self.frame.min(axis=1),
|
|
689
|
+
units=next(iter(self.units.values())),
|
|
690
|
+
name=name)
|
|
691
|
+
|
|
692
|
+
def max(
|
|
693
|
+
self,
|
|
694
|
+
name):
|
|
695
|
+
"""
|
|
696
|
+
Returns a Flow whose movements are the maximum of the Stream's flows by period
|
|
697
|
+
"""
|
|
698
|
+
if not self.is_homogeneous():
|
|
699
|
+
raise ValueError("Error: maximum requires all flows' units to be the same. Units: {0}".format(
|
|
700
|
+
json.dumps(
|
|
701
|
+
{flow.name: flow.units.__str__() for flow in self.flows}, indent=4)))
|
|
702
|
+
else:
|
|
703
|
+
return Flow(
|
|
704
|
+
movements=self.frame.max(axis=1),
|
|
705
|
+
units=next(iter(self.units.values())),
|
|
706
|
+
name=name)
|
|
707
|
+
|
|
673
708
|
def collapse(self) -> Stream:
|
|
674
709
|
"""
|
|
675
710
|
Returns an Stream with Flows' movements collapsed (summed) to the Stream's final period
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import Callable, Optional
|
|
2
|
-
|
|
2
|
+
import enum
|
|
3
3
|
import numpy as np
|
|
4
4
|
from numba import jit
|
|
5
5
|
import numba
|
|
@@ -11,12 +11,19 @@ import rangekeeper as rk
|
|
|
11
11
|
|
|
12
12
|
class Financial:
|
|
13
13
|
|
|
14
|
+
class Instrument(enum.Enum):
|
|
15
|
+
INTEREST_ONLY = 'interest_only'
|
|
16
|
+
COMPOUND = 'compound'
|
|
17
|
+
CAPITALIZED = 'capitalized'
|
|
18
|
+
|
|
19
|
+
|
|
14
20
|
@staticmethod
|
|
15
21
|
def interest(
|
|
16
|
-
rate: float,
|
|
22
|
+
rate: float | rk.flux.Flow,
|
|
17
23
|
transactions: rk.flux.Flow,
|
|
18
24
|
frequency: rk.duration.Type,
|
|
19
|
-
capitalized: bool = False)
|
|
25
|
+
# capitalized: bool = False)\
|
|
26
|
+
instrument: Instrument = Instrument.COMPOUND) -> (rk.flux.Stream, rk.flux.Flow):
|
|
20
27
|
"""
|
|
21
28
|
Calculate interest expense on a loan or other interest-bearing liability,
|
|
22
29
|
described by a series of transactions.
|
|
@@ -27,10 +34,23 @@ class Financial:
|
|
|
27
34
|
|
|
28
35
|
transactions = transactions.resample(frequency=frequency)
|
|
29
36
|
|
|
37
|
+
if isinstance(rate, float):
|
|
38
|
+
rate = rk.flux.Flow.from_projection(
|
|
39
|
+
name='Interest Rates',
|
|
40
|
+
value=rate,
|
|
41
|
+
proj=rk.projection.Extrapolation(
|
|
42
|
+
form=rk.extrapolation.Recurring(),
|
|
43
|
+
sequence=rk.duration.Sequence.from_datestamps(
|
|
44
|
+
datestamps=transactions.movements.index,
|
|
45
|
+
frequency=frequency)))
|
|
46
|
+
|
|
47
|
+
if len(transactions.movements) != len(rate.movements):
|
|
48
|
+
raise ValueError('The number of transactions must match the number of rates.')
|
|
49
|
+
|
|
30
50
|
startings, endings, interests = rk.formula.Financial._calc_interest(
|
|
31
51
|
transactions=numba.typed.List(transactions.movements.to_list()),
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
rates=numba.typed.List(rate.movements.to_list()),
|
|
53
|
+
instrument=instrument.value)
|
|
34
54
|
|
|
35
55
|
balance = rk.flux.Stream(
|
|
36
56
|
name='Balance',
|
|
@@ -58,8 +78,8 @@ class Financial:
|
|
|
58
78
|
@numba.jit
|
|
59
79
|
def _calc_interest(
|
|
60
80
|
transactions: numba.typed.List,
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
rates: numba.typed.List,
|
|
82
|
+
instrument: str) -> (numba.typed.List, numba.typed.List, numba.typed.List):
|
|
63
83
|
|
|
64
84
|
startings = numba.typed.List.empty_list(numba.float64)
|
|
65
85
|
endings = numba.typed.List.empty_list(numba.float64)
|
|
@@ -70,13 +90,24 @@ class Financial:
|
|
|
70
90
|
startings.append(0)
|
|
71
91
|
else:
|
|
72
92
|
startings.append(endings[-1])
|
|
93
|
+
|
|
73
94
|
principal = startings[i] + transactions[i]
|
|
74
|
-
if
|
|
75
|
-
interest =
|
|
95
|
+
if instrument == 'interest_only':
|
|
96
|
+
interest = principal * rates[i] if principal > 0 else 0
|
|
97
|
+
endings.append(principal)
|
|
98
|
+
|
|
99
|
+
elif instrument == 'compound':
|
|
100
|
+
interest = principal * rates[i] if principal > 0 else 0
|
|
76
101
|
endings.append(principal + interest)
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
|
|
103
|
+
elif instrument == 'capitalized':
|
|
104
|
+
interest = (principal * rates[i]) / (1 - rates[i]) if principal > 0 else 0 # Since we are capitalizing
|
|
105
|
+
# interest, the interest amount (draw) must include interest to pay on the principal. Derived from i = r * (P + i)
|
|
79
106
|
endings.append(principal + interest)
|
|
107
|
+
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(f'Invalid instrument: {instrument}')
|
|
110
|
+
|
|
80
111
|
interests.append(interest)
|
|
81
112
|
|
|
82
113
|
return (startings, endings, interests)
|
|
@@ -87,7 +118,7 @@ class Financial:
|
|
|
87
118
|
starting: float,
|
|
88
119
|
transactions: rk.flux.Flow,
|
|
89
120
|
frequency: rk.duration.Type,
|
|
90
|
-
name: str = None) -> rk.flux.Stream
|
|
121
|
+
name: str = None) -> rk.flux.Stream:
|
|
91
122
|
"""
|
|
92
123
|
Calculate the balance of a financial account given a starting balance and a series of transactions.
|
|
93
124
|
"""
|
|
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
|