rangekeeper 0.7.2.post3__tar.gz → 0.7.3__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.post3 → rangekeeper-0.7.3}/PKG-INFO +8 -8
  2. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/pyproject.toml +9 -9
  3. rangekeeper-0.7.3/rangekeeper/formula/__init__.py +1 -0
  4. rangekeeper-0.7.2.post3/rangekeeper/formula.py → rangekeeper-0.7.3/rangekeeper/formula/financial.py +109 -101
  5. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/span.py +3 -3
  6. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/README.md +0 -0
  7. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/__init__.py +0 -0
  8. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/api.py +0 -0
  9. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/conftest.py +0 -0
  10. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/distribution.py +0 -0
  11. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/duration.py +0 -0
  12. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/dynamics/__init__.py +0 -0
  13. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/dynamics/black_swan.py +0 -0
  14. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/dynamics/cyclicality.py +0 -0
  15. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/dynamics/market.py +0 -0
  16. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/dynamics/noise.py +0 -0
  17. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/dynamics/trend.py +0 -0
  18. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/dynamics/volatility.py +0 -0
  19. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/extrapolation.py +0 -0
  20. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/flux.py +0 -0
  21. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/format.py +0 -0
  22. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/graph.py +0 -0
  23. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/measure.py +0 -0
  24. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/policy.py +0 -0
  25. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/projection.py +0 -0
  26. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/segmentation.py +0 -0
  27. {rangekeeper-0.7.2.post3 → rangekeeper-0.7.3}/rangekeeper/space.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Rangekeeper
3
- Version: 0.7.2.post3
3
+ Version: 0.7.3
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,23 @@ 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)
15
+ Requires-Dist: matplotlib (>=3.10,<4.0)
16
16
  Requires-Dist: mpld3 (>=0.5,<0.6)
17
17
  Requires-Dist: multiprocess (>=0.70,<0.71)
18
18
  Requires-Dist: networkx (>=3.3,<4.0)
19
- Requires-Dist: numba (>=0.60,<0.61)
19
+ Requires-Dist: numba (>=0.61,<0.62)
20
20
  Requires-Dist: numpy-financial (>=1.0,<2.0)
21
- Requires-Dist: pandas (>=2.2,<3.0)
21
+ Requires-Dist: pandas (>=2,<3)
22
22
  Requires-Dist: pint (>=0.24,<0.25)
23
- Requires-Dist: plotly (>=5.22,<6.0)
23
+ Requires-Dist: plotly (>=6.0,<7.0)
24
24
  Requires-Dist: py-moneyed (>=3.0,<4.0)
25
- Requires-Dist: pyarrow (>=18.1,<19.0)
25
+ Requires-Dist: pyarrow (>=19.0,<20.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)
29
- Requires-Dist: scipy (>=1.13,<2.0)
29
+ Requires-Dist: scipy (>=1.14,<2.0)
30
30
  Requires-Dist: seaborn (>=0.13,<0.14)
31
- Requires-Dist: specklepy (>=2.19,<3.0)
31
+ Requires-Dist: specklepy (>=2.21,<3.0)
32
32
  Requires-Dist: tabulate (>=0.9,<0.10)
33
33
  Description-Content-Type: text/markdown
34
34
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Rangekeeper"
3
- version = "0.7.2-3"
3
+ version = "0.7.3"
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,28 @@ readme = "README.md"
8
8
 
9
9
  [tool.poetry.dependencies]
10
10
  python = ">=3.10, <3.13"
11
- matplotlib = "^3.9"
12
- pandas = "^2.2"
11
+ matplotlib = "^3.10"
12
+ pandas = "^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
19
  pyyaml = "^6.0"
20
- specklepy = "^2.19"
21
- numba = "^0.60"
20
+ specklepy = "^2.21"
21
+ numba = "^0.61"
22
22
  multiprocess = "^0.70"
23
23
  seaborn = "^0.13"
24
24
  pyvis = "^0.3"
25
- plotly = "^5.22"
25
+ plotly = "^6.0"
26
26
  mpld3 = "^0.5"
27
- pyarrow = "^18.1"
27
+ pyarrow = "^19.0"
28
28
  aenum = "^3.1"
29
29
  numpy-financial = "^1.0"
30
30
 
31
31
  [tool.poetry.dev-dependencies]
32
- pytest = "^8.2"
32
+ pytest = "^8.3"
33
33
 
34
34
  [build-system]
35
35
  requires = ["setuptools", "poetry-core>=1.0.0"]
@@ -0,0 +1 @@
1
+ from . import financial as financial
@@ -1,29 +1,101 @@
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
+ return rk.flux.Flow(
64
+ movements=ends - starts,
65
+ units=self.startings.units,
66
+ name='Overdraft' if name is None else name)
67
+
68
+
69
+ @staticmethod
70
+ @numba.jit
71
+ def _calculate_balance(
72
+ startings: numba.typed.List,
73
+ transactions: numba.typed.List) -> (numba.typed.List, numba.typed.List):
74
+
75
+ endings = numba.typed.List.empty_list(numba.float64)
76
+ for i in range(len(transactions)):
77
+ endings.append(float(startings[-1] + transactions[i]))
78
+ startings.append(endings[-1])
79
+
80
+ return (startings[:-1], endings)
81
+
82
+
83
+ class Interest:
84
+ amounts: rk.flux.Flow
85
+ balance: Balance
86
+
87
+ class Type(enum.Enum):
15
88
  INTEREST_ONLY = 'interest_only'
16
89
  COMPOUND = 'compound'
17
90
  CAPITALIZED = 'capitalized'
18
91
 
19
-
20
- @staticmethod
21
- def interest(
92
+ def __init__(
93
+ self,
22
94
  rate: float | rk.flux.Flow,
23
95
  transactions: rk.flux.Flow,
24
96
  frequency: rk.duration.Type,
25
- # capitalized: bool = False)\
26
- instrument: Instrument = Instrument.COMPOUND) -> (rk.flux.Stream, rk.flux.Flow):
97
+ type: Type = Type.COMPOUND,
98
+ ):
27
99
  """
28
100
  Calculate interest expense on a loan or other interest-bearing liability,
29
101
  described by a series of transactions.
@@ -43,47 +115,45 @@ class Financial:
43
115
  sequence=rk.duration.Sequence.from_datestamps(
44
116
  datestamps=transactions.movements.index,
45
117
  frequency=frequency)))
118
+ else:
119
+ raise NotImplementedError('Only float rates are supported currently.')
46
120
 
47
121
  if len(transactions.movements) != len(rate.movements):
48
122
  raise ValueError('The number of transactions must match the number of rates.')
49
123
 
50
- startings, endings, interests = rk.formula.Financial._calc_interest(
124
+ startings, endings, interests = self._calc_interest(
51
125
  transactions=numba.typed.List(transactions.movements.to_list()),
52
126
  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(
127
+ type=type.value)
128
+
129
+ self.balance = rk.formula.financial.Balance.from_flows(
130
+ startings=rk.flux.Flow(
131
+ movements=pd.Series(startings, index=transactions.movements.index),
132
+ units=transactions.units,
133
+ name='Start Balance'),
134
+ endings=rk.flux.Flow(
135
+ movements=pd.Series(endings, index=transactions.movements.index),
136
+ units=transactions.units,
137
+ name='End Balance')
138
+ )
139
+
140
+ self.amounts = rk.flux.Flow(
70
141
  movements=pd.Series(interests, index=transactions.movements.index),
71
142
  units=transactions.units,
72
143
  name='Interest')
73
144
 
74
- return (balance, interest)
75
-
76
145
 
77
146
  @staticmethod
78
147
  @numba.jit
79
148
  def _calc_interest(
80
149
  transactions: numba.typed.List,
81
150
  rates: numba.typed.List,
82
- instrument: str) -> (numba.typed.List, numba.typed.List, numba.typed.List):
151
+ type: str,
152
+ ) -> (numba.typed.List, numba.typed.List, numba.typed.List):
83
153
 
84
154
  startings = numba.typed.List.empty_list(numba.float64)
85
155
  endings = numba.typed.List.empty_list(numba.float64)
86
- interests = numba.typed.List.empty_list(numba.float64)
156
+ amounts = numba.typed.List.empty_list(numba.float64)
87
157
 
88
158
  for i in range(len(transactions)):
89
159
  if i == 0:
@@ -92,87 +162,25 @@ class Financial:
92
162
  startings.append(endings[-1])
93
163
 
94
164
  principal = startings[i] + transactions[i]
95
- if instrument == 'interest_only':
165
+ if type == 'interest_only':
96
166
  interest = principal * rates[i] if principal > 0 else 0
97
167
  endings.append(principal)
98
168
 
99
- elif instrument == 'compound':
169
+ elif type == 'compound':
100
170
  interest = principal * rates[i] if principal > 0 else 0
101
171
  endings.append(principal + interest)
102
172
 
103
- elif instrument == 'capitalized':
173
+ elif type == 'capitalized':
104
174
  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)
175
+ # interest, the amount (draw) must include interest to pay on the principal. Derived from i = r * (P + i)
106
176
  endings.append(principal + interest)
107
177
 
108
178
  else:
109
- raise ValueError(f'Invalid instrument: {instrument}')
110
-
111
- interests.append(interest)
179
+ raise ValueError(f'Invalid instrument: {type}')
112
180
 
113
- return (startings, endings, interests)
181
+ amounts.append(interest)
114
182
 
183
+ return (startings, endings, amounts)
115
184
 
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
185
 
141
- statement = rk.flux.Stream(
142
- name=name,
143
- flows=flows,
144
- frequency=frequency)
145
186
 
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