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.
Files changed (27) hide show
  1. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/PKG-INFO +7 -10
  2. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/pyproject.toml +8 -11
  3. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/flux.py +1 -1
  4. rangekeeper-0.7.4/rangekeeper/formula/__init__.py +1 -0
  5. rangekeeper-0.7.2.post4/rangekeeper/formula.py → rangekeeper-0.7.4/rangekeeper/formula/financial.py +112 -101
  6. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/span.py +3 -3
  7. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/README.md +0 -0
  8. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/__init__.py +0 -0
  9. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/api.py +0 -0
  10. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/conftest.py +0 -0
  11. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/distribution.py +0 -0
  12. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/duration.py +0 -0
  13. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/__init__.py +0 -0
  14. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/black_swan.py +0 -0
  15. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/cyclicality.py +0 -0
  16. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/market.py +0 -0
  17. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/noise.py +0 -0
  18. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/trend.py +0 -0
  19. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/dynamics/volatility.py +0 -0
  20. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/extrapolation.py +0 -0
  21. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/format.py +0 -0
  22. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/graph.py +0 -0
  23. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/measure.py +0 -0
  24. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/policy.py +0 -0
  25. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/projection.py +0 -0
  26. {rangekeeper-0.7.2.post4 → rangekeeper-0.7.4}/rangekeeper/segmentation.py +0 -0
  27. {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.2.post4
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.9,<4.0)
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.60,<0.61)
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 (>=5.22,<6.0)
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: pyyaml (>=6.0,<7.0)
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.19,<3.0)
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.2-4"
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.9"
12
- pandas = "^2"
11
+ matplotlib = "^3.10"
12
+ pandas = "^2.2"
13
13
  pint = "^0.24"
14
14
  pyxirr = "^0.10"
15
- scipy = "^1.13"
15
+ scipy = "^1.14"
16
16
  networkx = "^3.3"
17
17
  py-moneyed = "^3.0"
18
18
  tabulate = "^0.9"
19
- pyyaml = "^6.0"
20
- specklepy = "^2.19"
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 = "^5.22"
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.2"
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 dates
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
@@ -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 Financial:
10
+ class Balance:
11
+ startings: rk.flux.Flow
12
+ endings: rk.flux.Flow
13
13
 
14
- class Instrument(enum.Enum):
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
- @staticmethod
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
- # capitalized: bool = False)\
26
- instrument: Instrument = Instrument.COMPOUND) -> (rk.flux.Stream, rk.flux.Flow):
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 = rk.formula.Financial._calc_interest(
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
- instrument=instrument.value)
54
-
55
- balance = rk.flux.Stream(
56
- name='Balance',
57
- flows=[
58
- rk.flux.Flow(
59
- movements=pd.Series(startings, index=transactions.movements.index),
60
- units=transactions.units,
61
- name='Start Balance'),
62
- rk.flux.Flow(
63
- movements=pd.Series(endings, index=transactions.movements.index),
64
- units=transactions.units,
65
- name='End Balance')
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
- instrument: str) -> (numba.typed.List, numba.typed.List, numba.typed.List):
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
- interests = numba.typed.List.empty_list(numba.float64)
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 instrument == 'interest_only':
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 instrument == 'compound':
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 instrument == 'capitalized':
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 interest amount (draw) must include interest to pay on the principal. Derived from i = r * (P + i)
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: {instrument}')
182
+ raise ValueError(f'Invalid instrument: {type}')
110
183
 
111
- interests.append(interest)
184
+ amounts.append(interest)
112
185
 
113
- return (startings, endings, interests)
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: {}'.format(self.name, self.start_date.date(), self.end_date.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