rangekeeper 0.7.7__tar.gz → 0.7.8__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.7 → rangekeeper-0.7.8}/PKG-INFO +16 -8
- rangekeeper-0.7.8/README.md +26 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/PKG-INFO +16 -8
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/pyproject.toml +1 -1
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/flux.py +34 -14
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/formula/financial.py +95 -52
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_api.py +56 -50
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_formulas.py +11 -7
- rangekeeper-0.7.7/README.md +0 -18
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/SOURCES.txt +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/dependency_links.txt +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/requires.txt +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/top_level.txt +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/__init__.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/api.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/conftest.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/distribution.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/duration.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/__init__.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/black_swan.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/cyclicality.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/market.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/noise.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/trend.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/volatility.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/extrapolation.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/format.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/formula/__init__.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/graph.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/measure.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/policy.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/projection.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/segmentation.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/space.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/setup.cfg +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_dynamics.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_graph.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_measures.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_models.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_modules.py +0 -0
- {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_projections.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rangekeeper
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.8
|
|
4
4
|
Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
|
|
5
5
|
Author-email: Daniel Fink <danfink@mit.edu>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -30,16 +30,24 @@ This directory holds the source code for the Rangekeeper Library
|
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
32
32
|
The library is offered through PyPI, and so its installation in other projects can be performed by:
|
|
33
|
-
`pip install rangekeeper` or `
|
|
33
|
+
`pip install rangekeeper`, `poetry add rangekeeper`, or `uv add rangekeeper`, depending on your virtual environment manager.
|
|
34
34
|
|
|
35
35
|
## Development
|
|
36
|
-
If you wish to contribute to its development, it is recommended to use [
|
|
36
|
+
If you wish to contribute to its development, it is recommended to use [uv](https://docs.astral.sh/uv/) for
|
|
37
|
+
environment and dependency management:
|
|
37
38
|
|
|
38
39
|
### Environment Setup
|
|
39
40
|
|
|
40
|
-
1. Install
|
|
41
|
+
1. Install uv, if you haven't yet: <https://docs.astral.sh/uv/>
|
|
41
42
|
2. Clone this repo.
|
|
42
|
-
3.
|
|
43
|
-
4.
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
3. Create a virtual environment: `uv venv .venv`
|
|
44
|
+
4. Activate the virtual environment: `source .venv/bin/activate`
|
|
45
|
+
5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
|
|
46
|
+
6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
|
|
47
|
+
(https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
|
|
48
|
+
|
|
49
|
+
### Publishing
|
|
50
|
+
1. First, remove any previously built packages: `rm -rf dist/*`
|
|
51
|
+
2. Build the package: `uv build`
|
|
52
|
+
3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
|
|
53
|
+
'^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Rangekeeper Source
|
|
2
|
+
This directory holds the source code for the Rangekeeper Library
|
|
3
|
+
|
|
4
|
+
## Installation
|
|
5
|
+
The library is offered through PyPI, and so its installation in other projects can be performed by:
|
|
6
|
+
`pip install rangekeeper`, `poetry add rangekeeper`, or `uv add rangekeeper`, depending on your virtual environment manager.
|
|
7
|
+
|
|
8
|
+
## Development
|
|
9
|
+
If you wish to contribute to its development, it is recommended to use [uv](https://docs.astral.sh/uv/) for
|
|
10
|
+
environment and dependency management:
|
|
11
|
+
|
|
12
|
+
### Environment Setup
|
|
13
|
+
|
|
14
|
+
1. Install uv, if you haven't yet: <https://docs.astral.sh/uv/>
|
|
15
|
+
2. Clone this repo.
|
|
16
|
+
3. Create a virtual environment: `uv venv .venv`
|
|
17
|
+
4. Activate the virtual environment: `source .venv/bin/activate`
|
|
18
|
+
5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
|
|
19
|
+
6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
|
|
20
|
+
(https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
|
|
21
|
+
|
|
22
|
+
### Publishing
|
|
23
|
+
1. First, remove any previously built packages: `rm -rf dist/*`
|
|
24
|
+
2. Build the package: `uv build`
|
|
25
|
+
3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
|
|
26
|
+
'^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rangekeeper
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.8
|
|
4
4
|
Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
|
|
5
5
|
Author-email: Daniel Fink <danfink@mit.edu>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -30,16 +30,24 @@ This directory holds the source code for the Rangekeeper Library
|
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
32
32
|
The library is offered through PyPI, and so its installation in other projects can be performed by:
|
|
33
|
-
`pip install rangekeeper` or `
|
|
33
|
+
`pip install rangekeeper`, `poetry add rangekeeper`, or `uv add rangekeeper`, depending on your virtual environment manager.
|
|
34
34
|
|
|
35
35
|
## Development
|
|
36
|
-
If you wish to contribute to its development, it is recommended to use [
|
|
36
|
+
If you wish to contribute to its development, it is recommended to use [uv](https://docs.astral.sh/uv/) for
|
|
37
|
+
environment and dependency management:
|
|
37
38
|
|
|
38
39
|
### Environment Setup
|
|
39
40
|
|
|
40
|
-
1. Install
|
|
41
|
+
1. Install uv, if you haven't yet: <https://docs.astral.sh/uv/>
|
|
41
42
|
2. Clone this repo.
|
|
42
|
-
3.
|
|
43
|
-
4.
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
3. Create a virtual environment: `uv venv .venv`
|
|
44
|
+
4. Activate the virtual environment: `source .venv/bin/activate`
|
|
45
|
+
5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
|
|
46
|
+
6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
|
|
47
|
+
(https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
|
|
48
|
+
|
|
49
|
+
### Publishing
|
|
50
|
+
1. First, remove any previously built packages: `rm -rf dist/*`
|
|
51
|
+
2. Build the package: `uv build`
|
|
52
|
+
3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
|
|
53
|
+
'^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "Rangekeeper"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.8"
|
|
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 = [
|
|
6
6
|
{ name = "Daniel Fink", email = "danfink@mit.edu" }
|
|
@@ -5,7 +5,7 @@ import itertools
|
|
|
5
5
|
import json
|
|
6
6
|
import locale
|
|
7
7
|
import os
|
|
8
|
-
from typing import Dict, Union, Optional, Tuple
|
|
8
|
+
from typing import Dict, Union, Optional, Tuple, List
|
|
9
9
|
|
|
10
10
|
import matplotlib.pyplot as plt
|
|
11
11
|
import numpy as np
|
|
@@ -19,19 +19,22 @@ import rangekeeper as rk
|
|
|
19
19
|
def _format_series(
|
|
20
20
|
series: pd.Series,
|
|
21
21
|
units: pint.Unit,
|
|
22
|
+
to_datestamps: bool = True,
|
|
22
23
|
decimals: int = 2,
|
|
23
24
|
):
|
|
25
|
+
index = pd.Series(series.index.date, name="date") if to_datestamps else series.index
|
|
26
|
+
|
|
24
27
|
if units.dimensionality == "[currency]":
|
|
25
28
|
formatted = pd.Series(
|
|
26
29
|
data=[str(locale.currency(value, grouping=True)) for value in series],
|
|
27
|
-
index=
|
|
30
|
+
index=index,
|
|
28
31
|
name=series.name,
|
|
29
32
|
)
|
|
30
33
|
else:
|
|
31
34
|
floatfmt = "{:." + str(decimals) + "f}"
|
|
32
35
|
formatted = pd.Series(
|
|
33
36
|
data=[str(floatfmt.format(value)) for value in series],
|
|
34
|
-
index=
|
|
37
|
+
index=index,
|
|
35
38
|
name=series.name,
|
|
36
39
|
)
|
|
37
40
|
|
|
@@ -133,7 +136,9 @@ class Flow:
|
|
|
133
136
|
"Movements: "
|
|
134
137
|
+ os.linesep
|
|
135
138
|
+ _format_series(
|
|
136
|
-
series=self.movements,
|
|
139
|
+
series=self.movements,
|
|
140
|
+
units=self.units,
|
|
141
|
+
decimals=decimals,
|
|
137
142
|
).to_markdown(
|
|
138
143
|
stralign="right",
|
|
139
144
|
numalign="right",
|
|
@@ -393,10 +398,10 @@ class Flow:
|
|
|
393
398
|
|
|
394
399
|
class Stream:
|
|
395
400
|
name: str
|
|
396
|
-
flows: [Flow]
|
|
401
|
+
flows: List[Flow]
|
|
397
402
|
frequency: rk.duration.Type
|
|
398
|
-
start_date:
|
|
399
|
-
end_date:
|
|
403
|
+
start_date: datetime.date
|
|
404
|
+
end_date: datetime.date
|
|
400
405
|
frame: pd.DataFrame
|
|
401
406
|
|
|
402
407
|
"""
|
|
@@ -405,7 +410,7 @@ class Stream:
|
|
|
405
410
|
|
|
406
411
|
def __init__(
|
|
407
412
|
self,
|
|
408
|
-
flows: [Flow],
|
|
413
|
+
flows: List[Flow],
|
|
409
414
|
frequency: rk.duration.Type,
|
|
410
415
|
name: str = None,
|
|
411
416
|
):
|
|
@@ -437,9 +442,9 @@ class Stream:
|
|
|
437
442
|
self.start_date = min(flows_dates)
|
|
438
443
|
self.end_date = max(flows_dates)
|
|
439
444
|
|
|
440
|
-
index = rk.duration.Sequence.from_bounds(
|
|
441
|
-
|
|
442
|
-
)
|
|
445
|
+
# index = rk.duration.Sequence.from_bounds(
|
|
446
|
+
# include_start=self.start_date, frequency=self.frequency, bound=self.end_date
|
|
447
|
+
# )
|
|
443
448
|
self._resampled_flows = [
|
|
444
449
|
flow.to_periods(frequency=self.frequency) for flow in self.flows
|
|
445
450
|
]
|
|
@@ -465,10 +470,22 @@ class Stream:
|
|
|
465
470
|
for flow in self.flows:
|
|
466
471
|
series = flow.to_periods(frequency=self.frequency)
|
|
467
472
|
formatted_flows.append(
|
|
468
|
-
_format_series(
|
|
473
|
+
_format_series(
|
|
474
|
+
series=series,
|
|
475
|
+
units=flow.units,
|
|
476
|
+
to_datestamps=False,
|
|
477
|
+
decimals=decimals,
|
|
478
|
+
)
|
|
469
479
|
)
|
|
470
480
|
# formatted_flows = [flow._format_movements(decimals=decimals) for flow in self._resampled_flows]
|
|
471
|
-
frame =
|
|
481
|
+
frame = (
|
|
482
|
+
pd.concat(
|
|
483
|
+
formatted_flows,
|
|
484
|
+
axis=1,
|
|
485
|
+
)
|
|
486
|
+
.fillna(0)
|
|
487
|
+
.sort_index()
|
|
488
|
+
)
|
|
472
489
|
return frame
|
|
473
490
|
|
|
474
491
|
def display(
|
|
@@ -487,7 +504,10 @@ class Stream:
|
|
|
487
504
|
"Flows: "
|
|
488
505
|
+ os.linesep
|
|
489
506
|
+ self._format_flows(decimals=decimals).to_markdown(
|
|
490
|
-
tablefmt=tablefmt,
|
|
507
|
+
tablefmt=tablefmt,
|
|
508
|
+
stralign="right",
|
|
509
|
+
numalign="right",
|
|
510
|
+
floatfmt=floatfmt,
|
|
491
511
|
)
|
|
492
512
|
)
|
|
493
513
|
|
|
@@ -10,48 +10,71 @@ import rangekeeper as rk
|
|
|
10
10
|
class Balance:
|
|
11
11
|
startings: rk.flux.Flow
|
|
12
12
|
endings: rk.flux.Flow
|
|
13
|
+
overdraft: rk.flux.Flow
|
|
13
14
|
|
|
14
15
|
def __init__(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
self,
|
|
17
|
+
starting: float,
|
|
18
|
+
transactions: rk.flux.Flow,
|
|
19
|
+
frequency: rk.duration.Type,
|
|
20
|
+
name: str = "",
|
|
19
21
|
):
|
|
20
22
|
"""
|
|
21
23
|
Calculate the balance of a financial account given a starting balance and a series of transactions.
|
|
24
|
+
Returns an object with the starting and ending balances, as well as the overdraft amount, as `Flow`s.
|
|
25
|
+
Overdraft is the portion of transactions occurring while the balance is non-positive.
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
28
|
transactions = transactions.resample(frequency=frequency)
|
|
25
29
|
|
|
26
30
|
result = self._calculate_balance(
|
|
27
31
|
startings=numba.typed.List([np.float64(starting)]),
|
|
28
|
-
transactions=numba.typed.List(transactions.movements.to_list())
|
|
32
|
+
transactions=numba.typed.List(transactions.movements.to_list()),
|
|
33
|
+
)
|
|
29
34
|
|
|
30
|
-
self.startings=rk.flux.Flow(
|
|
31
|
-
movements=pd.Series(
|
|
35
|
+
self.startings = rk.flux.Flow(
|
|
36
|
+
movements=pd.Series(
|
|
37
|
+
result[0],
|
|
38
|
+
index=transactions.movements.index,
|
|
39
|
+
),
|
|
32
40
|
units=transactions.units,
|
|
33
|
-
name=
|
|
41
|
+
name=f"{name} Start Balance",
|
|
42
|
+
)
|
|
34
43
|
|
|
35
|
-
self.endings=rk.flux.Flow(
|
|
36
|
-
movements=pd.Series(
|
|
44
|
+
self.endings = rk.flux.Flow(
|
|
45
|
+
movements=pd.Series(
|
|
46
|
+
result[1],
|
|
47
|
+
index=transactions.movements.index,
|
|
48
|
+
),
|
|
37
49
|
units=transactions.units,
|
|
38
|
-
name=
|
|
50
|
+
name=f"{name} End Balance",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.overdraft = rk.flux.Flow(
|
|
54
|
+
movements=pd.Series(
|
|
55
|
+
result[2],
|
|
56
|
+
index=transactions.movements.index,
|
|
57
|
+
),
|
|
58
|
+
units=transactions.units,
|
|
59
|
+
name=f"{name} Overdraft",
|
|
60
|
+
)
|
|
39
61
|
|
|
40
62
|
@classmethod
|
|
41
63
|
def from_flows(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
cls,
|
|
65
|
+
startings: rk.flux.Flow,
|
|
66
|
+
endings: rk.flux.Flow,
|
|
45
67
|
):
|
|
46
68
|
balance = super().__new__(cls)
|
|
47
69
|
balance.startings = startings
|
|
48
70
|
balance.endings = endings
|
|
71
|
+
balance.overdraft = balance._overdraft()
|
|
49
72
|
|
|
50
73
|
return balance
|
|
51
74
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
def _overdraft(
|
|
76
|
+
self,
|
|
77
|
+
name: str = None,
|
|
55
78
|
) -> rk.flux.Flow:
|
|
56
79
|
"""
|
|
57
80
|
Identifies the portion of transactions occurring while the balance is negative.
|
|
@@ -66,21 +89,35 @@ class Balance:
|
|
|
66
89
|
return rk.flux.Flow(
|
|
67
90
|
movements=overdrafts,
|
|
68
91
|
units=self.startings.units,
|
|
69
|
-
name=
|
|
70
|
-
|
|
92
|
+
name="Overdraft" if name is None else name,
|
|
93
|
+
)
|
|
71
94
|
|
|
72
95
|
@staticmethod
|
|
73
96
|
@numba.jit
|
|
74
97
|
def _calculate_balance(
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
startings: numba.typed.List,
|
|
99
|
+
transactions: numba.typed.List,
|
|
100
|
+
) -> (
|
|
101
|
+
numba.typed.List,
|
|
102
|
+
numba.typed.List,
|
|
103
|
+
numba.typed.List,
|
|
104
|
+
):
|
|
77
105
|
|
|
78
106
|
endings = numba.typed.List.empty_list(numba.float64)
|
|
107
|
+
overdrafts = numba.typed.List.empty_list(numba.float64)
|
|
79
108
|
for i in range(len(transactions)):
|
|
80
|
-
|
|
109
|
+
ending = float(startings[-1] + transactions[i])
|
|
110
|
+
|
|
111
|
+
overdrafts.append(ending if ending < 0 else 0)
|
|
112
|
+
endings.append(ending if ending > 0 else 0)
|
|
81
113
|
startings.append(endings[-1])
|
|
114
|
+
# startings.append(ending)
|
|
82
115
|
|
|
83
|
-
return (
|
|
116
|
+
return (
|
|
117
|
+
startings[:-1],
|
|
118
|
+
endings,
|
|
119
|
+
overdrafts,
|
|
120
|
+
)
|
|
84
121
|
|
|
85
122
|
|
|
86
123
|
class Interest:
|
|
@@ -88,16 +125,16 @@ class Interest:
|
|
|
88
125
|
balance: Balance
|
|
89
126
|
|
|
90
127
|
class Type(enum.Enum):
|
|
91
|
-
INTEREST_ONLY =
|
|
92
|
-
COMPOUND =
|
|
93
|
-
CAPITALIZED =
|
|
128
|
+
INTEREST_ONLY = "interest_only"
|
|
129
|
+
COMPOUND = "compound"
|
|
130
|
+
CAPITALIZED = "capitalized"
|
|
94
131
|
|
|
95
132
|
def __init__(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
self,
|
|
134
|
+
rate: float | rk.flux.Flow,
|
|
135
|
+
transactions: rk.flux.Flow,
|
|
136
|
+
frequency: rk.duration.Type,
|
|
137
|
+
type: Type = Type.COMPOUND,
|
|
101
138
|
):
|
|
102
139
|
"""
|
|
103
140
|
Calculate interest expense on a loan or other interest-bearing liability,
|
|
@@ -111,47 +148,54 @@ class Interest:
|
|
|
111
148
|
|
|
112
149
|
if isinstance(rate, float):
|
|
113
150
|
rate = rk.flux.Flow.from_projection(
|
|
114
|
-
name=
|
|
151
|
+
name="Interest Rates",
|
|
115
152
|
value=rate,
|
|
116
153
|
proj=rk.projection.Extrapolation(
|
|
117
154
|
form=rk.extrapolation.Recurring(),
|
|
118
155
|
sequence=rk.duration.Sequence.from_datestamps(
|
|
119
|
-
datestamps=transactions.movements.index,
|
|
120
|
-
|
|
156
|
+
datestamps=transactions.movements.index, frequency=frequency
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
121
160
|
else:
|
|
122
|
-
raise NotImplementedError(
|
|
161
|
+
raise NotImplementedError("Only float rates are supported currently.")
|
|
123
162
|
|
|
124
163
|
if len(transactions.movements) != len(rate.movements):
|
|
125
|
-
raise ValueError(
|
|
164
|
+
raise ValueError(
|
|
165
|
+
"The number of transactions must match the number of rates."
|
|
166
|
+
)
|
|
126
167
|
|
|
127
168
|
startings, endings, interests = self._calc_interest(
|
|
128
169
|
transactions=numba.typed.List(transactions.movements.to_list()),
|
|
129
170
|
rates=numba.typed.List(rate.movements.to_list()),
|
|
130
|
-
type=type.value
|
|
171
|
+
type=type.value,
|
|
172
|
+
)
|
|
131
173
|
|
|
132
174
|
self.balance = rk.formula.financial.Balance.from_flows(
|
|
133
175
|
startings=rk.flux.Flow(
|
|
134
176
|
movements=pd.Series(startings, index=transactions.movements.index),
|
|
135
177
|
units=transactions.units,
|
|
136
|
-
name=
|
|
178
|
+
name="Start Balance",
|
|
179
|
+
),
|
|
137
180
|
endings=rk.flux.Flow(
|
|
138
181
|
movements=pd.Series(endings, index=transactions.movements.index),
|
|
139
182
|
units=transactions.units,
|
|
140
|
-
name=
|
|
183
|
+
name="End Balance",
|
|
184
|
+
),
|
|
141
185
|
)
|
|
142
186
|
|
|
143
187
|
self.amounts = rk.flux.Flow(
|
|
144
188
|
movements=pd.Series(interests, index=transactions.movements.index),
|
|
145
189
|
units=transactions.units,
|
|
146
|
-
name=
|
|
147
|
-
|
|
190
|
+
name="Interest",
|
|
191
|
+
)
|
|
148
192
|
|
|
149
193
|
@staticmethod
|
|
150
194
|
@numba.jit
|
|
151
195
|
def _calc_interest(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
196
|
+
transactions: numba.typed.List,
|
|
197
|
+
rates: numba.typed.List,
|
|
198
|
+
type: str,
|
|
155
199
|
) -> (numba.typed.List, numba.typed.List, numba.typed.List):
|
|
156
200
|
|
|
157
201
|
startings = numba.typed.List.empty_list(numba.float64)
|
|
@@ -165,25 +209,24 @@ class Interest:
|
|
|
165
209
|
startings.append(endings[-1])
|
|
166
210
|
|
|
167
211
|
principal = startings[i] + transactions[i]
|
|
168
|
-
if type ==
|
|
212
|
+
if type == "interest_only":
|
|
169
213
|
interest = principal * rates[i] if principal > 0 else 0
|
|
170
214
|
endings.append(principal)
|
|
171
215
|
|
|
172
|
-
elif type ==
|
|
216
|
+
elif type == "compound":
|
|
173
217
|
interest = principal * rates[i] if principal > 0 else 0
|
|
174
218
|
endings.append(principal + interest)
|
|
175
219
|
|
|
176
|
-
elif type ==
|
|
177
|
-
interest = (
|
|
220
|
+
elif type == "capitalized":
|
|
221
|
+
interest = (
|
|
222
|
+
(principal * rates[i]) / (1 - rates[i]) if principal > 0 else 0
|
|
223
|
+
) # Since we are capitalizing
|
|
178
224
|
# interest, the amount (draw) must include interest to pay on the principal. Derived from i = r * (P + i)
|
|
179
225
|
endings.append(principal + interest)
|
|
180
226
|
|
|
181
227
|
else:
|
|
182
|
-
raise ValueError(f
|
|
228
|
+
raise ValueError(f"Invalid instrument: {type}")
|
|
183
229
|
|
|
184
230
|
amounts.append(interest)
|
|
185
231
|
|
|
186
232
|
return (startings, endings, amounts)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
@@ -1,90 +1,93 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
import pprint as pp
|
|
5
3
|
# Pytests file.
|
|
6
4
|
# Note: gathers tests according to a naming convention.
|
|
7
5
|
# By default any file that is to contain tests must be named starting with 'test_',
|
|
8
6
|
# classes that hold tests must be named starting with 'Test',
|
|
9
7
|
# and any function in a file that should be treated as a test must also start with 'test_'.
|
|
10
8
|
import os
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
from typing import List, Callable, Dict
|
|
14
|
-
|
|
15
|
-
import matplotlib
|
|
16
|
-
import matplotlib.pyplot as plt
|
|
17
|
-
import networkx as nx
|
|
18
|
-
from pyvis.network import Network
|
|
19
|
-
import plotly.express as px
|
|
20
|
-
import plotly.graph_objects as go
|
|
9
|
+
import pprint as pp
|
|
10
|
+
|
|
21
11
|
# In addition, in order to enable pytest to find all modules,
|
|
22
12
|
# run tests via a 'python -m pytest tests/<test_file>.py' command from the root directory of this project
|
|
23
13
|
import pandas as pd
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import scipy.stats as ss
|
|
27
|
-
import pint
|
|
14
|
+
|
|
15
|
+
pd.set_option("display.max_columns", None)
|
|
28
16
|
|
|
29
17
|
import rangekeeper as rk
|
|
30
18
|
|
|
31
19
|
|
|
32
20
|
class TestApi:
|
|
33
21
|
stream_id = "c0f66c35e3"
|
|
34
|
-
speckle = rk.api.Speckle(token=os.getenv(
|
|
22
|
+
speckle = rk.api.Speckle(token=os.getenv("SPECKLE_TOKEN"))
|
|
35
23
|
model = speckle.get_commit(stream_id=stream_id)
|
|
36
24
|
|
|
37
25
|
def test_connection(self):
|
|
38
|
-
latest_commit_id = TestApi.speckle.get_latest_commit_id(
|
|
39
|
-
|
|
26
|
+
latest_commit_id = TestApi.speckle.get_latest_commit_id(
|
|
27
|
+
stream_id=TestApi.stream_id
|
|
28
|
+
)
|
|
29
|
+
print("Latest Commit: {0}".format(latest_commit_id))
|
|
40
30
|
|
|
41
31
|
metadata = TestApi.speckle.get_metadata(stream_id=TestApi.stream_id)
|
|
42
32
|
print(metadata)
|
|
43
33
|
|
|
44
|
-
assert metadata.name ==
|
|
34
|
+
assert metadata.name == "RangekeeperDemo"
|
|
45
35
|
|
|
46
36
|
roots = TestApi.model.get_dynamic_member_names()
|
|
47
37
|
|
|
48
38
|
assert len(roots) == 2
|
|
49
|
-
assert
|
|
39
|
+
assert "@property" in roots
|
|
50
40
|
|
|
51
41
|
def test_conversion(self):
|
|
52
|
-
parsed = rk.api.Speckle.parse(base=TestApi.model[
|
|
42
|
+
parsed = rk.api.Speckle.parse(base=TestApi.model["@property"])
|
|
53
43
|
root_base = list(parsed.values())[0]
|
|
54
|
-
print(
|
|
55
|
-
assert root_base.speckle_type ==
|
|
44
|
+
print("Root Base:\n {0}".format(root_base))
|
|
45
|
+
assert root_base.speckle_type == "Rangekeeper.Entity:Rangekeeper.Assembly"
|
|
56
46
|
|
|
57
47
|
root_entity = rk.graph.Entity.from_base(root_base)
|
|
58
|
-
assert root_entity.name ==
|
|
48
|
+
assert root_entity.name == "property"
|
|
59
49
|
assert len(root_entity.graph.nodes) == 5
|
|
60
50
|
assert type(root_entity) == rk.graph.Assembly
|
|
61
|
-
print(
|
|
62
|
-
|
|
63
|
-
buildingA = [
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
print(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
51
|
+
print("Root Entity:\n {0}".format(root_entity))
|
|
52
|
+
|
|
53
|
+
buildingA = [
|
|
54
|
+
node for node in root_entity.graph.nodes if node.name == "buildingA"
|
|
55
|
+
][0]
|
|
56
|
+
print("BuildingA:\n {0}".format(buildingA))
|
|
57
|
+
buildingA_containment = buildingA.get_relatives(
|
|
58
|
+
outgoing=True, relationship_type="contains"
|
|
59
|
+
)
|
|
60
|
+
print("BuildingA Containment: \n {0}\n".format(buildingA_containment))
|
|
61
|
+
|
|
62
|
+
buildingAresidential = [
|
|
63
|
+
node
|
|
64
|
+
for node in buildingA.graph.nodes
|
|
65
|
+
if node.name == "buildingAresidential"
|
|
66
|
+
][0]
|
|
67
|
+
print("BuildingAresidential:\n {0}".format(buildingAresidential))
|
|
70
68
|
|
|
71
69
|
def test_develop(self):
|
|
72
|
-
parsed = rk.api.Speckle.parse(base=TestApi.model[
|
|
70
|
+
parsed = rk.api.Speckle.parse(base=TestApi.model["@property"])
|
|
73
71
|
# print('\nParsed: \n{0}'.format(pp.pprint([base['name'] for base in parsed.values()])))
|
|
74
72
|
# print('\nCount Parsed: \n{0}'.format(len(parsed)))
|
|
75
73
|
|
|
76
74
|
property = rk.api.Speckle.to_rk(
|
|
77
|
-
bases=list(parsed.values()),
|
|
78
|
-
|
|
79
|
-
type='property')
|
|
75
|
+
bases=list(parsed.values()), name="property", type="property"
|
|
76
|
+
)
|
|
80
77
|
# print('\nScenario: \n{0}'.format(scenario))
|
|
81
78
|
# print('\nCount Scenario: \n{0}'.format(len(scenario.graph.nodes())))
|
|
82
79
|
|
|
83
80
|
spatial_containment = rk.graph.Assembly.from_graph(
|
|
84
81
|
graph=property.graph.edge_subgraph(
|
|
85
|
-
[
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
[
|
|
83
|
+
edge
|
|
84
|
+
for edge in property.graph.edges(keys=True)
|
|
85
|
+
if edge[2] == "spatiallyContains"
|
|
86
|
+
]
|
|
87
|
+
),
|
|
88
|
+
name="spatial_containment",
|
|
89
|
+
type="subgraph",
|
|
90
|
+
)
|
|
88
91
|
# print('\nSpatial Containment: \n{0}'.format(spatial_containment))
|
|
89
92
|
|
|
90
93
|
roots = property.get_roots() # ['spatiallyContains']
|
|
@@ -93,7 +96,11 @@ class TestApi:
|
|
|
93
96
|
subassemblies = property.get_subassemblies()
|
|
94
97
|
# print('\nSubassemblies: \n{0}'.format(pp.pprint(list(subassemblies.values()))))
|
|
95
98
|
|
|
96
|
-
buildingA = [
|
|
99
|
+
buildingA = [
|
|
100
|
+
assembly
|
|
101
|
+
for assembly in subassemblies.values()
|
|
102
|
+
if assembly["name"] == "buildingA"
|
|
103
|
+
][0]
|
|
97
104
|
# print('\nBuildingA: \n{0}'.format(buildingA))
|
|
98
105
|
|
|
99
106
|
subentities = buildingA.get_subentities()
|
|
@@ -106,27 +113,26 @@ class TestApi:
|
|
|
106
113
|
hierarchical_layout=False,
|
|
107
114
|
display=False,
|
|
108
115
|
height=1600,
|
|
109
|
-
|
|
116
|
+
)
|
|
110
117
|
|
|
111
118
|
spatial_containment_dict = spatial_containment.to_dict()
|
|
112
119
|
# print('\nSpatial Containment Dict: \n{0}'.format(pp.pprint(spatial_containment_dict)))
|
|
113
120
|
|
|
114
|
-
spatial_containment.aggregate(
|
|
115
|
-
property='gfa',
|
|
116
|
-
label='subtotal_gfa')
|
|
121
|
+
spatial_containment.aggregate(property="gfa", label="subtotal_gfa")
|
|
117
122
|
|
|
118
|
-
print(
|
|
123
|
+
print(
|
|
124
|
+
"\nGFA Aggregation: \n{0}".format(pp.pprint(spatial_containment.to_dict()))
|
|
125
|
+
)
|
|
119
126
|
#
|
|
120
|
-
df = pd.DataFrame.from_dict(spatial_containment.to_dict(), orient=
|
|
127
|
+
df = pd.DataFrame.from_dict(spatial_containment.to_dict(), orient="index")
|
|
121
128
|
print(df)
|
|
122
129
|
|
|
123
130
|
foo = spatial_containment.to_DataFrame()
|
|
124
131
|
#
|
|
125
132
|
|
|
126
|
-
fig = spatial_containment.sunburst(
|
|
133
|
+
fig = spatial_containment.sunburst("subtotal_gfa")
|
|
127
134
|
fig.show()
|
|
128
135
|
|
|
129
|
-
|
|
130
136
|
#
|
|
131
137
|
|
|
132
138
|
#
|
|
@@ -122,7 +122,9 @@ class TestFinancial:
|
|
|
122
122
|
draws_periods=9,
|
|
123
123
|
payments=value,
|
|
124
124
|
payments_start_date=rk.duration.offset(
|
|
125
|
-
date=start_date,
|
|
125
|
+
date=start_date,
|
|
126
|
+
duration=rk.duration.Type.MONTH,
|
|
127
|
+
amount=9,
|
|
126
128
|
),
|
|
127
129
|
payments_periods=3,
|
|
128
130
|
)
|
|
@@ -206,9 +208,7 @@ class TestFinancial:
|
|
|
206
208
|
def test_balances(self):
|
|
207
209
|
transactions = rk.flux.Stream(
|
|
208
210
|
name="Transactions",
|
|
209
|
-
flows=[
|
|
210
|
-
self.model.draws.sum().invert(),
|
|
211
|
-
],
|
|
211
|
+
flows=[self.model.draws.sum().invert()],
|
|
212
212
|
frequency=self.params["frequency"],
|
|
213
213
|
)
|
|
214
214
|
equity = rk.formula.financial.Balance(
|
|
@@ -221,14 +221,17 @@ class TestFinancial:
|
|
|
221
221
|
equity.startings.display()
|
|
222
222
|
equity.endings.display()
|
|
223
223
|
|
|
224
|
-
overdraft = equity.overdraft
|
|
224
|
+
overdraft = equity.overdraft
|
|
225
225
|
overdraft.display()
|
|
226
226
|
|
|
227
227
|
interest = rk.formula.financial.Interest(
|
|
228
228
|
rate=self.params["interest_rate_pa"]
|
|
229
229
|
/ rk.duration.Period.yearly_count((self.params["frequency"])),
|
|
230
230
|
transactions=rk.flux.Stream(
|
|
231
|
-
flows=[
|
|
231
|
+
flows=[
|
|
232
|
+
overdraft.invert(),
|
|
233
|
+
self.model.payments.invert(),
|
|
234
|
+
],
|
|
232
235
|
frequency=self.params["frequency"],
|
|
233
236
|
).sum(),
|
|
234
237
|
frequency=self.params["frequency"],
|
|
@@ -239,7 +242,8 @@ class TestFinancial:
|
|
|
239
242
|
interest.amounts.display()
|
|
240
243
|
print(interest.amounts.total())
|
|
241
244
|
|
|
242
|
-
profit = interest.balance.overdraft
|
|
245
|
+
profit = interest.balance.overdraft.invert()
|
|
246
|
+
profit.name = "Profit"
|
|
243
247
|
profit.display()
|
|
244
248
|
|
|
245
249
|
assert profit.total() == approx(671969.16)
|
rangekeeper-0.7.7/README.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Rangekeeper Source
|
|
2
|
-
This directory holds the source code for the Rangekeeper Library
|
|
3
|
-
|
|
4
|
-
## Installation
|
|
5
|
-
The library is offered through PyPI, and so its installation in other projects can be performed by:
|
|
6
|
-
`pip install rangekeeper` or `poetry add rangekeeper`
|
|
7
|
-
|
|
8
|
-
## Development
|
|
9
|
-
If you wish to contribute to its development, it is recommended to use [Poetry](https://python-poetry.org/) for environment and dependency management:
|
|
10
|
-
|
|
11
|
-
### Environment Setup
|
|
12
|
-
|
|
13
|
-
1. Install poetry, if you haven't yet: <https://python-poetry.org/docs/master/#installing-with-the-official-installer>
|
|
14
|
-
2. Clone this repo.
|
|
15
|
-
3. Use a terminal to install poetry packages from the repo's directory: `<path_to_repo>$ poetry install`
|
|
16
|
-
4. If you wish to develop this repo alongside other projects locally, you may install the local Rangekeeper library via `<path_to_repo>$ poetry run pip install -e <path_to_rangekeeper_repo>`,
|
|
17
|
-
replacing `<path_to_rangekeeper_repo>` with its location on your system.
|
|
18
|
-
5. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use the [Poetry Dotenv Plugin](https://github.com/mpeteuil/poetry-dotenv-plugin) via `poetry self add poetry-dotenv-plugin`, and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
|
|
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
|
|
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
|