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.
Files changed (26) hide show
  1. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/PKG-INFO +2 -2
  2. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/pyproject.toml +2 -2
  3. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/duration.py +9 -0
  4. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/flux.py +35 -0
  5. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/formula.py +43 -12
  6. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/README.md +0 -0
  7. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/__init__.py +0 -0
  8. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/api.py +0 -0
  9. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/conftest.py +0 -0
  10. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/distribution.py +0 -0
  11. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/__init__.py +0 -0
  12. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/black_swan.py +0 -0
  13. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/cyclicality.py +0 -0
  14. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/market.py +0 -0
  15. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/noise.py +0 -0
  16. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/trend.py +0 -0
  17. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/dynamics/volatility.py +0 -0
  18. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/extrapolation.py +0 -0
  19. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/format.py +0 -0
  20. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/graph.py +0 -0
  21. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/measure.py +0 -0
  22. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/policy.py +0 -0
  23. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/projection.py +0 -0
  24. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/segmentation.py +0 -0
  25. {rangekeeper-0.7.0 → rangekeeper-0.7.2}/rangekeeper/space.py +0 -0
  26. {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.0
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 (>=17.0,<18.0)
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.0"
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 = "^17.0"
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) -> (rk.flux.Stream, rk.flux.Flow):
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
- rate=np.float64(rate),
33
- capitalized=capitalized)
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
- rate: np.float64,
62
- capitalized: bool = False) -> (numba.typed.List, numba.typed.List, numba.typed.List):
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 capitalized:
75
- interest = (principal * rate) / (1 - rate) if principal > 0 else 0 # Since we are capitalizing interest, the interest amount (draw) must include interest to pay on the principal. Derived from i = r * (P + i)
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
- else:
78
- interest = principal * rate if principal > 0 else 0
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:#, rk.flux.Flow):
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