rangekeeper 0.1.0__py3-none-any.whl
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/__init__.py +40 -0
- rangekeeper/api.py +88 -0
- rangekeeper/distribution.py +269 -0
- rangekeeper/dynamics/__init__.py +6 -0
- rangekeeper/dynamics/black_swan.py +77 -0
- rangekeeper/dynamics/cyclicality.py +414 -0
- rangekeeper/dynamics/market.py +328 -0
- rangekeeper/dynamics/noise.py +34 -0
- rangekeeper/dynamics/trend.py +79 -0
- rangekeeper/dynamics/volatility.py +140 -0
- rangekeeper/extrapolation.py +100 -0
- rangekeeper/flux.py +733 -0
- rangekeeper/graph.py +144 -0
- rangekeeper/measure.py +138 -0
- rangekeeper/models/__init__.py +5 -0
- rangekeeper/models/deterministic.py +143 -0
- rangekeeper/models/flexible.py +177 -0
- rangekeeper/models/linear.py +144 -0
- rangekeeper/models/linear_graph.py +171 -0
- rangekeeper/models/probabilistic.py +157 -0
- rangekeeper/periodicity.py +236 -0
- rangekeeper/policy.py +20 -0
- rangekeeper/projection.py +248 -0
- rangekeeper/segmentation.py +220 -0
- rangekeeper/setup.py +14 -0
- rangekeeper/space.py +42 -0
- rangekeeper/span.py +226 -0
- rangekeeper-0.1.0.dist-info/LICENSE +373 -0
- rangekeeper-0.1.0.dist-info/METADATA +32 -0
- rangekeeper-0.1.0.dist-info/RECORD +31 -0
- rangekeeper-0.1.0.dist-info/WHEEL +4 -0
rangekeeper/graph.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
import enum
|
|
6
|
+
from typing import List, Dict
|
|
7
|
+
|
|
8
|
+
import networkx as nx
|
|
9
|
+
from pint import Quantity
|
|
10
|
+
from py_linq import Enumerable
|
|
11
|
+
|
|
12
|
+
from . import measure
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# class Type(enum.Enum):
|
|
17
|
+
# name: str
|
|
18
|
+
# parent: Type
|
|
19
|
+
# children: [Type]
|
|
20
|
+
#
|
|
21
|
+
# def __init__(
|
|
22
|
+
# self,
|
|
23
|
+
# name: str,
|
|
24
|
+
# parent: Type = None,
|
|
25
|
+
# children: List[Type] = None):
|
|
26
|
+
# super().__init__()
|
|
27
|
+
# self.name
|
|
28
|
+
# if parent is not None:
|
|
29
|
+
# self.set_parent(parent)
|
|
30
|
+
# if children is not None:
|
|
31
|
+
# self.set_children(children)
|
|
32
|
+
# else:
|
|
33
|
+
# self.children = []
|
|
34
|
+
#
|
|
35
|
+
# def __str__(self):
|
|
36
|
+
# result = self.name
|
|
37
|
+
# try:
|
|
38
|
+
# result = self.parent.__str__() + '.' + result
|
|
39
|
+
# except AttributeError:
|
|
40
|
+
# pass
|
|
41
|
+
# return result
|
|
42
|
+
#
|
|
43
|
+
# def set_parent(
|
|
44
|
+
# self,
|
|
45
|
+
# parent: Type):
|
|
46
|
+
# try:
|
|
47
|
+
# self.parent.children.remove(self)
|
|
48
|
+
# except AttributeError:
|
|
49
|
+
# pass
|
|
50
|
+
# self.parent = parent
|
|
51
|
+
# parent.children.append(self)
|
|
52
|
+
#
|
|
53
|
+
# def set_children(
|
|
54
|
+
# self,
|
|
55
|
+
# children: List[Type]):
|
|
56
|
+
# for child in children:
|
|
57
|
+
# child.set_parent(self)
|
|
58
|
+
#
|
|
59
|
+
|
|
60
|
+
class Event:
|
|
61
|
+
name: str
|
|
62
|
+
type: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Element:
|
|
66
|
+
id: str
|
|
67
|
+
name: str
|
|
68
|
+
type: str
|
|
69
|
+
attributes: dict
|
|
70
|
+
events: List[Event]
|
|
71
|
+
measurements: Dict[measure.Measure, Quantity]
|
|
72
|
+
|
|
73
|
+
def __str__(self):
|
|
74
|
+
return 'Element: ' + self.name + '. Type: ' + self.type
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
name: str,
|
|
79
|
+
type: str,
|
|
80
|
+
id: str = None,
|
|
81
|
+
attributes: dict = None,
|
|
82
|
+
events: List[Event] = None,
|
|
83
|
+
measurements: Dict[measure.Measure, Quantity] = None):
|
|
84
|
+
self.name = name
|
|
85
|
+
self.type = type
|
|
86
|
+
self.id = str(uuid.uuid4()) if id is None else id
|
|
87
|
+
self.attributes = {} if attributes is None else attributes
|
|
88
|
+
self.events = [] if events is None else events
|
|
89
|
+
self.measurements = {} if measurements is None else measurements
|
|
90
|
+
|
|
91
|
+
def get_relatives(
|
|
92
|
+
self,
|
|
93
|
+
assembly: Assembly,
|
|
94
|
+
relationship_type: str,
|
|
95
|
+
outgoing: bool = True) -> List[Element]:
|
|
96
|
+
|
|
97
|
+
if outgoing:
|
|
98
|
+
edges = Enumerable(
|
|
99
|
+
assembly.edges(
|
|
100
|
+
nbunch=[self],
|
|
101
|
+
data='type')
|
|
102
|
+
).where(
|
|
103
|
+
lambda edge: edge[2] == relationship_type)
|
|
104
|
+
return [edge[1] for edge in edges]
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
edges = Enumerable(
|
|
108
|
+
assembly.in_edges(
|
|
109
|
+
nbunch=[self],
|
|
110
|
+
data='type')
|
|
111
|
+
).where(
|
|
112
|
+
lambda edge: edge[2] == relationship_type)
|
|
113
|
+
return [edge[0] for edge in edges]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Assembly(nx.MultiDiGraph, Element):
|
|
117
|
+
elements: Enumerable
|
|
118
|
+
relationships: Enumerable
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
name: str,
|
|
123
|
+
type: str,
|
|
124
|
+
elements: List[Element],
|
|
125
|
+
relationships: List[tuple[Element, Element, str]], # A list of tuples of the form [From, To, Type]
|
|
126
|
+
id: str = None,
|
|
127
|
+
attributes: dict = None,
|
|
128
|
+
events: List[Event] = None,
|
|
129
|
+
measurements: Dict[measure.Measure, Quantity] = None):
|
|
130
|
+
super().__init__(
|
|
131
|
+
name=name)
|
|
132
|
+
self.name = name
|
|
133
|
+
self.type = type
|
|
134
|
+
self.id = str(uuid.uuid4()) if id is None else id
|
|
135
|
+
self.attributes = {} if attributes is None else attributes
|
|
136
|
+
self.events = [] if events is None else events
|
|
137
|
+
self.measurements = {} if measurements is None else measurements
|
|
138
|
+
|
|
139
|
+
self.add_nodes_from(elements)
|
|
140
|
+
for relationship in relationships:
|
|
141
|
+
self.add_edge(relationship[0], relationship[1], type=relationship[2])
|
|
142
|
+
|
|
143
|
+
self.elements = Enumerable(self.nodes)
|
|
144
|
+
self.relationships = Enumerable(self.edges)
|
rangekeeper/measure.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import locale
|
|
4
|
+
|
|
5
|
+
import pint
|
|
6
|
+
#from pint.definitions import UnitDefinition
|
|
7
|
+
#from pint.converters import ScaleConverter
|
|
8
|
+
import moneyed
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Index:
|
|
12
|
+
# Define Fractions:
|
|
13
|
+
registry = pint.UnitRegistry()
|
|
14
|
+
# preprocessors=[lambda s: s.replace('%', ' percent ')]
|
|
15
|
+
# )
|
|
16
|
+
# # registry.define('percent = 0.01 * dimensionless = %')
|
|
17
|
+
# registry.define(UnitDefinition('percent', '%', (), ScaleConverter(1 / 100.0)))
|
|
18
|
+
|
|
19
|
+
# Add Additional Terms:
|
|
20
|
+
registry.define('squaremeter = 1 m**2 = m2 = sqm')
|
|
21
|
+
registry.define('squarefoot = 1 foot**2 = ft2 = sqft')
|
|
22
|
+
|
|
23
|
+
# Define Domain Units:
|
|
24
|
+
registry.define('zone = [space]')
|
|
25
|
+
registry.define('parking_stall = 1 * zone')
|
|
26
|
+
|
|
27
|
+
# self.registry = registry
|
|
28
|
+
|
|
29
|
+
# # Define Scalar:
|
|
30
|
+
# scalar = Measure(
|
|
31
|
+
# name='Scalar Quantity',
|
|
32
|
+
# definition='Dimensionless or Unitless Quantity',
|
|
33
|
+
# units=pint.UnitRegistry().dimensionless)
|
|
34
|
+
|
|
35
|
+
# @staticmethod
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def remove_dimension(
|
|
39
|
+
quantity: pint.Quantity,
|
|
40
|
+
dimension: str,
|
|
41
|
+
registry: pint.UnitRegistry = None) -> pint.Quantity:
|
|
42
|
+
"""
|
|
43
|
+
Remove a dimension from the units of a quantity. Specify the dimension as a string wrapped in square brackets (['']).
|
|
44
|
+
"""
|
|
45
|
+
if dimension in quantity.dimensionality:
|
|
46
|
+
for unit_str in quantity.units._units:
|
|
47
|
+
units = registry.Unit(unit_str) if registry is not None else pint.Unit(unit_str)
|
|
48
|
+
if units.dimensionality == dimension:
|
|
49
|
+
if quantity.dimensionality[dimension] == -1:
|
|
50
|
+
return quantity * units
|
|
51
|
+
elif quantity.dimensionality[dimension] == 1:
|
|
52
|
+
return quantity / units
|
|
53
|
+
else:
|
|
54
|
+
raise NotImplementedError(
|
|
55
|
+
'Error: Dimension reduction currently only works for single-order (^1 or ^-1) dimensions.')
|
|
56
|
+
else:
|
|
57
|
+
return quantity
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def multiply_units(
|
|
61
|
+
units: [pint.Unit],
|
|
62
|
+
registry: pint.UnitRegistry = None) -> pint.Unit:
|
|
63
|
+
"""
|
|
64
|
+
Multiply units together.
|
|
65
|
+
"""
|
|
66
|
+
registry = registry if registry is not None else pint.UnitRegistry()
|
|
67
|
+
quantities = [registry.Quantity(1, unit) for unit in units]
|
|
68
|
+
result = 1 * registry.dimensionless
|
|
69
|
+
for quantity in quantities:
|
|
70
|
+
result *= quantity
|
|
71
|
+
return result.units
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def register_currency(
|
|
75
|
+
registry: pint.UnitRegistry):
|
|
76
|
+
# if unit_registry is None:
|
|
77
|
+
# unit_registry = pint.UnitRegistry()
|
|
78
|
+
|
|
79
|
+
currency = moneyed.Currency(code=locale.localeconv()['int_curr_symbol'].strip())
|
|
80
|
+
|
|
81
|
+
if '[currency]' not in registry._dimensions:
|
|
82
|
+
registry.define('money = [currency]')
|
|
83
|
+
registry.define(
|
|
84
|
+
'{0} = nan money = {0} = {1}'.format(
|
|
85
|
+
''.join(currency.code.split()),
|
|
86
|
+
''.join(currency.name.split())))
|
|
87
|
+
else:
|
|
88
|
+
registry.define(
|
|
89
|
+
'{0} = nan money = {0} = {1}'.format(
|
|
90
|
+
''.join(currency.code.split()),
|
|
91
|
+
''.join(currency.name.split())))
|
|
92
|
+
|
|
93
|
+
return Measure(
|
|
94
|
+
name=currency.name,
|
|
95
|
+
definition='Currency of {0}'.format(currency.countries),
|
|
96
|
+
units=registry[currency.code].units)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
#
|
|
100
|
+
# class Quantity(pint.Quantity):
|
|
101
|
+
# def __init__(
|
|
102
|
+
# self,
|
|
103
|
+
# value,
|
|
104
|
+
# units: pint.Unit
|
|
105
|
+
# ):
|
|
106
|
+
# """
|
|
107
|
+
# value : str, pint.Quantity or any numeric type. Value of the physical quantity to be created.
|
|
108
|
+
# units :
|
|
109
|
+
# """
|
|
110
|
+
# super.__init__()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Measure:
|
|
114
|
+
name: str
|
|
115
|
+
definition: str
|
|
116
|
+
units: pint.Unit
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
name: str,
|
|
121
|
+
units: pint.Unit,
|
|
122
|
+
definition: str = None):
|
|
123
|
+
self.name = name
|
|
124
|
+
self.units = units
|
|
125
|
+
self.definition = definition
|
|
126
|
+
|
|
127
|
+
def __str__(self):
|
|
128
|
+
return 'Rangekeeper Measure: "{0}". {1}. Units: {2}'.format(
|
|
129
|
+
self.name,
|
|
130
|
+
self.definition,
|
|
131
|
+
self.units)
|
|
132
|
+
|
|
133
|
+
def __hash__(self):
|
|
134
|
+
return hash((self.name, self.units.__hash__()))
|
|
135
|
+
|
|
136
|
+
def __eq__(self, other):
|
|
137
|
+
return (self.name == other.name and
|
|
138
|
+
self.units == other.units)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import rangekeeper as rk
|
|
4
|
+
|
|
5
|
+
# except:
|
|
6
|
+
# import modules.rangekeeper.distribution
|
|
7
|
+
# from modules.rangekeeper.flux import Flow, Stream
|
|
8
|
+
# from modules.rangekeeper.periodicity import Periodicity
|
|
9
|
+
# from modules.rangekeeper.span import Span
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Base Model:
|
|
13
|
+
class Model:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
params: dict):
|
|
17
|
+
|
|
18
|
+
# Phasing:
|
|
19
|
+
self.acquisition_span = rk.span.Span.from_num_periods(
|
|
20
|
+
name='Acquisition',
|
|
21
|
+
date=params['start_date'],
|
|
22
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
23
|
+
num_periods=1)
|
|
24
|
+
|
|
25
|
+
self.operation_span = rk.span.Span.from_num_periods(
|
|
26
|
+
name='Operation',
|
|
27
|
+
date=rk.periodicity.offset_date(
|
|
28
|
+
date=self.acquisition_span.end_date,
|
|
29
|
+
period_type=rk.periodicity.Type.DAY,
|
|
30
|
+
num_periods=1),
|
|
31
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
32
|
+
num_periods=params['num_periods'])
|
|
33
|
+
|
|
34
|
+
self.projection_span = rk.span.Span.from_num_periods(
|
|
35
|
+
name='Projection',
|
|
36
|
+
date=rk.periodicity.offset_date(
|
|
37
|
+
date=self.operation_span.end_date,
|
|
38
|
+
period_type=rk.periodicity.Type.DAY,
|
|
39
|
+
num_periods=1),
|
|
40
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
41
|
+
num_periods=1)
|
|
42
|
+
|
|
43
|
+
self.noi_calc_span = rk.span.Span.merge(
|
|
44
|
+
name='NOI Calculation Span',
|
|
45
|
+
spans=[self.operation_span, self.projection_span])
|
|
46
|
+
|
|
47
|
+
# Cashflows:
|
|
48
|
+
self.escalation = rk.extrapolation.Compounding(rate=params['growth_rate'])
|
|
49
|
+
|
|
50
|
+
# Potential Gross Income
|
|
51
|
+
self.pgi = rk.flux.Flow.from_projection(
|
|
52
|
+
name='Potential Gross Income',
|
|
53
|
+
value=params['initial_pgi'],
|
|
54
|
+
proj=rk.projection.Extrapolation(
|
|
55
|
+
form=self.escalation,
|
|
56
|
+
sequence=self.noi_calc_span.to_index(period_type=params['period_type'])),
|
|
57
|
+
units=params['units'])
|
|
58
|
+
|
|
59
|
+
self.addl_pgi = rk.flux.Flow(
|
|
60
|
+
movements=pd.Series(
|
|
61
|
+
data=range(self.pgi.movements.size),
|
|
62
|
+
index=self.pgi.movements.index,
|
|
63
|
+
dtype=float),
|
|
64
|
+
units=params['units'],
|
|
65
|
+
name='Additional PGI')
|
|
66
|
+
|
|
67
|
+
self.addl_pgi.movements = self.addl_pgi.movements * params['addl_pgi_per_period']
|
|
68
|
+
self.pgi = rk.flux.Flow(
|
|
69
|
+
movements=self.pgi.movements + self.addl_pgi.movements,
|
|
70
|
+
units=params['units'],
|
|
71
|
+
name=self.pgi.name)
|
|
72
|
+
|
|
73
|
+
# Vacancy Allowance
|
|
74
|
+
self.vacancy = rk.flux.Flow.from_periods(
|
|
75
|
+
name='Vacancy Allowance',
|
|
76
|
+
index=self.noi_calc_span.to_index(period_type=params['period_type']),
|
|
77
|
+
data=self.pgi.movements * params['vacancy_rate'],
|
|
78
|
+
units=params['units']).invert()
|
|
79
|
+
|
|
80
|
+
# Effective Gross Income:
|
|
81
|
+
self.egi = rk.flux.Stream(
|
|
82
|
+
name='Effective Gross Income',
|
|
83
|
+
flows=[self.pgi, self.vacancy],
|
|
84
|
+
period_type=params['period_type'])
|
|
85
|
+
|
|
86
|
+
# Operating Expenses:
|
|
87
|
+
self.opex = rk.flux.Flow.from_periods(
|
|
88
|
+
name='Operating Expenses',
|
|
89
|
+
index=self.noi_calc_span.to_index(period_type=params['period_type']),
|
|
90
|
+
data=self.pgi.movements * params['opex_pgi_ratio'],
|
|
91
|
+
units=params['units']).invert()
|
|
92
|
+
|
|
93
|
+
# Net Operating Income:
|
|
94
|
+
self.noi = rk.flux.Stream(
|
|
95
|
+
name='Net Operating Income',
|
|
96
|
+
flows=[self.egi.sum('Effective Gross Income'), self.opex],
|
|
97
|
+
period_type=params['period_type'])
|
|
98
|
+
|
|
99
|
+
# Capital Expenses:
|
|
100
|
+
self.capex = rk.flux.Flow.from_periods(
|
|
101
|
+
name='Capital Expenditures',
|
|
102
|
+
index=self.noi_calc_span.to_index(period_type=params['period_type']),
|
|
103
|
+
data=self.pgi.movements * params['capex_pgi_ratio'],
|
|
104
|
+
units=params['units']).invert()
|
|
105
|
+
|
|
106
|
+
# Net Cashflows:
|
|
107
|
+
self.ncf = rk.flux.Stream(
|
|
108
|
+
name='Net Cashflows',
|
|
109
|
+
flows=[self.noi.sum(), self.capex],
|
|
110
|
+
period_type=params['period_type'])
|
|
111
|
+
|
|
112
|
+
# Disposition
|
|
113
|
+
# We require each next period's NCF as the numerator:
|
|
114
|
+
sale_values = list(self.ncf.sum().movements.iloc[1:] / params['cap_rate'])
|
|
115
|
+
self.disposition = rk.flux.Flow.from_periods(
|
|
116
|
+
name='Disposition',
|
|
117
|
+
# We no longer need the noi_calc_span:
|
|
118
|
+
index=self.operation_span.to_index(period_type=params['period_type']),
|
|
119
|
+
data=sale_values,
|
|
120
|
+
units=params['units'])
|
|
121
|
+
|
|
122
|
+
# Calculate the Present Value of the NCFs:
|
|
123
|
+
self.pv_ncf = self.ncf.sum().pv(
|
|
124
|
+
period_type=params['period_type'],
|
|
125
|
+
discount_rate=params['discount_rate'])
|
|
126
|
+
|
|
127
|
+
# Calculate the Present Value of Reversion CFs:
|
|
128
|
+
self.pv_disposition = self.disposition.pv(
|
|
129
|
+
period_type=params['period_type'],
|
|
130
|
+
discount_rate=params['discount_rate'])
|
|
131
|
+
|
|
132
|
+
# Add Cumulative Sum of Discounted Net Cashflows to each period's Discounted Disposition
|
|
133
|
+
pv_ncf_cumsum = rk.flux.Flow(
|
|
134
|
+
movements=self.pv_ncf.movements.cumsum(),
|
|
135
|
+
name='Discounted Net Cashflow Cumulative Sums',
|
|
136
|
+
units=params['units'])
|
|
137
|
+
self.pv_ncf_agg = rk.flux.Stream(
|
|
138
|
+
name='Discounted Net Cashflow Sums',
|
|
139
|
+
flows=[pv_ncf_cumsum, self.pv_disposition],
|
|
140
|
+
period_type=params['period_type'])
|
|
141
|
+
|
|
142
|
+
self.pv_sums = self.pv_ncf_agg.sum()
|
|
143
|
+
self.pv_sums.movements = self.pv_sums.movements[:-1]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import rangekeeper as rk
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Base Model:
|
|
7
|
+
class Model:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
params: dict):
|
|
11
|
+
|
|
12
|
+
# Phasing:
|
|
13
|
+
self.acquisition_span = rk.span.Span.from_num_periods(
|
|
14
|
+
name='Acquisition',
|
|
15
|
+
date=params['start_date'],
|
|
16
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
17
|
+
num_periods=1)
|
|
18
|
+
|
|
19
|
+
self.operation_span = rk.span.Span.from_num_periods(
|
|
20
|
+
name='Operation',
|
|
21
|
+
date=rk.periodicity.offset_date(
|
|
22
|
+
date=self.acquisition_span.end_date,
|
|
23
|
+
period_type=rk.periodicity.Type.DAY,
|
|
24
|
+
num_periods=1),
|
|
25
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
26
|
+
num_periods=params['num_periods'])
|
|
27
|
+
|
|
28
|
+
self.disposition_span = rk.span.Span.from_num_periods(
|
|
29
|
+
name='Disposition',
|
|
30
|
+
date=rk.periodicity.offset_date(
|
|
31
|
+
date=self.acquisition_span.start_date,
|
|
32
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
33
|
+
num_periods=params['num_periods']),
|
|
34
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
35
|
+
num_periods=1)
|
|
36
|
+
|
|
37
|
+
self.projection_span = rk.span.Span.from_num_periods(
|
|
38
|
+
name='Projection',
|
|
39
|
+
date=rk.periodicity.offset_date(
|
|
40
|
+
date=self.operation_span.end_date,
|
|
41
|
+
period_type=rk.periodicity.Type.DAY,
|
|
42
|
+
num_periods=1),
|
|
43
|
+
period_type=rk.periodicity.Type.YEAR,
|
|
44
|
+
num_periods=1)
|
|
45
|
+
|
|
46
|
+
self.noi_calc_span = rk.span.Span.merge(
|
|
47
|
+
name='NOI Calculation Span',
|
|
48
|
+
spans=[self.operation_span, self.projection_span])
|
|
49
|
+
|
|
50
|
+
# Cashflows:
|
|
51
|
+
self.escalation = rk.extrapolation.Compounding(rate=params['growth_rate'])
|
|
52
|
+
|
|
53
|
+
# Potential Gross Income
|
|
54
|
+
self.pgi = rk.flux.Flow.from_projection(
|
|
55
|
+
name='Potential Gross Income',
|
|
56
|
+
value=params['initial_pgi'],
|
|
57
|
+
proj=rk.projection.Extrapolation(
|
|
58
|
+
form=self.escalation,
|
|
59
|
+
sequence=self.noi_calc_span.to_index(period_type=params['period_type'])),
|
|
60
|
+
units=params['units'])
|
|
61
|
+
|
|
62
|
+
factors = params['space_market_dist'].sample(size=self.pgi.movements.size)
|
|
63
|
+
self.pgi_factor = rk.flux.Flow(
|
|
64
|
+
movements=pd.Series(
|
|
65
|
+
data=factors,
|
|
66
|
+
index=self.pgi.movements.index,
|
|
67
|
+
dtype=float),
|
|
68
|
+
units=params['units'],
|
|
69
|
+
name='Space Market Pricing Factors')
|
|
70
|
+
|
|
71
|
+
self.pgi = rk.flux.Flow(
|
|
72
|
+
movements=self.pgi.movements * self.pgi_factor.movements,
|
|
73
|
+
units=params['units'],
|
|
74
|
+
name=self.pgi.name)
|
|
75
|
+
|
|
76
|
+
# Vacancy Allowance
|
|
77
|
+
self.vacancy = rk.flux.Flow.from_periods(
|
|
78
|
+
name='Vacancy Allowance',
|
|
79
|
+
index=self.noi_calc_span.to_index(period_type=params['period_type']),
|
|
80
|
+
data=self.pgi.movements * params['vacancy_rate'],
|
|
81
|
+
units=params['units']).invert()
|
|
82
|
+
|
|
83
|
+
# Effective Gross Income:
|
|
84
|
+
self.egi = rk.flux.Stream(
|
|
85
|
+
name='Effective Gross Income',
|
|
86
|
+
flows=[self.pgi, self.vacancy],
|
|
87
|
+
period_type=params['period_type'])
|
|
88
|
+
|
|
89
|
+
# Operating Expenses:
|
|
90
|
+
self.opex = rk.flux.Flow.from_periods(
|
|
91
|
+
name='Operating Expenses',
|
|
92
|
+
index=self.noi_calc_span.to_index(period_type=params['period_type']),
|
|
93
|
+
data=self.pgi.movements * params['opex_pgi_ratio'],
|
|
94
|
+
units=params['units']).invert()
|
|
95
|
+
|
|
96
|
+
# Net Operating Income:
|
|
97
|
+
self.noi = rk.flux.Stream(
|
|
98
|
+
name='Net Operating Income',
|
|
99
|
+
flows=[self.egi.sum('Effective Gross Income'), self.opex],
|
|
100
|
+
period_type=params['period_type'])
|
|
101
|
+
|
|
102
|
+
# Capital Expenses:
|
|
103
|
+
self.capex = rk.flux.Flow.from_periods(
|
|
104
|
+
name='Capital Expenditures',
|
|
105
|
+
index=self.noi_calc_span.to_index(period_type=params['period_type']),
|
|
106
|
+
data=self.pgi.movements * params['capex_pgi_ratio'],
|
|
107
|
+
units=params['units']).invert()
|
|
108
|
+
|
|
109
|
+
# Net Cashflows:
|
|
110
|
+
self.ncf = rk.flux.Stream(
|
|
111
|
+
name='Net Cashflows',
|
|
112
|
+
flows=[self.noi.sum(), self.capex],
|
|
113
|
+
period_type=params['period_type'])
|
|
114
|
+
|
|
115
|
+
# Reversion:
|
|
116
|
+
# We no longer need the noi_calc_span:
|
|
117
|
+
operation_periods = self.operation_span.to_index(period_type=params['period_type'])
|
|
118
|
+
self.cap_rates = params['asset_market_dist'].sample(size=operation_periods.size)
|
|
119
|
+
self.sale_values = rk.flux.Flow.from_periods(
|
|
120
|
+
name='Sale Values',
|
|
121
|
+
# We no longer need the noi_calc_span:
|
|
122
|
+
index=operation_periods,
|
|
123
|
+
data=list(self.ncf.sum().movements[
|
|
124
|
+
:self.operation_span.end_date] / self.cap_rates),
|
|
125
|
+
units=params['units'])
|
|
126
|
+
|
|
127
|
+
# Flexibility Rules:
|
|
128
|
+
disposition_flags = []
|
|
129
|
+
for i in range(self.sale_values.movements.size):
|
|
130
|
+
flag = False
|
|
131
|
+
if i > 1:
|
|
132
|
+
if not any(disposition_flags):
|
|
133
|
+
if self.pgi_factor.movements[i] > 1.2:
|
|
134
|
+
self.disposition_date = self.sale_values.movements.index[i]
|
|
135
|
+
flag = True
|
|
136
|
+
disposition_flags.append(flag)
|
|
137
|
+
|
|
138
|
+
self.disposition = rk.flux.Flow(
|
|
139
|
+
name="Disposition",
|
|
140
|
+
movements=pd.Series(
|
|
141
|
+
data=self.sale_values.movements * disposition_flags,
|
|
142
|
+
index=self.sale_values.movements.index),
|
|
143
|
+
units=params['units'])
|
|
144
|
+
|
|
145
|
+
# Calculate the Present Value of the NCFs:
|
|
146
|
+
self.pv_ncf = self.ncf.sum().pv(
|
|
147
|
+
period_type=params['period_type'],
|
|
148
|
+
discount_rate=params['discount_rate'])
|
|
149
|
+
|
|
150
|
+
# Calculate the Present Value of Disposition CFs:
|
|
151
|
+
self.pv_disposition = self.disposition.pv(
|
|
152
|
+
period_type=params['period_type'],
|
|
153
|
+
discount_rate=params['discount_rate'])
|
|
154
|
+
|
|
155
|
+
self.pv_ncf_agg = rk.flux.Stream(
|
|
156
|
+
name='Discounted Net Cashflow Sums',
|
|
157
|
+
flows=[self.pv_ncf, self.pv_disposition],
|
|
158
|
+
period_type=params['period_type'])
|
|
159
|
+
|
|
160
|
+
self.pv_sums = self.pv_ncf_agg.sum()
|
|
161
|
+
|
|
162
|
+
self.pv_sums.movements = self.pv_sums.movements[:self.disposition_date]
|
|
163
|
+
|
|
164
|
+
self.acquisition = rk.flux.Flow.from_periods(
|
|
165
|
+
index=self.acquisition_span.to_index(rk.periodicity.Type.YEAR),
|
|
166
|
+
data=[-abs(params['acquisition_price'])],
|
|
167
|
+
units=params['units'],
|
|
168
|
+
name='Acquisition Price')
|
|
169
|
+
|
|
170
|
+
self.investment_cashflows = rk.flux.Stream(
|
|
171
|
+
name='Investment Cashflows',
|
|
172
|
+
flows=[self.ncf.sum(), self.disposition, self.acquisition],
|
|
173
|
+
period_type=params['period_type'])
|
|
174
|
+
|
|
175
|
+
self.investment_cashflows.frame = self.investment_cashflows.frame[:self.disposition_date]
|
|
176
|
+
self.irr = self.investment_cashflows.sum().xirr()
|
|
177
|
+
self.npv = self.investment_cashflows.sum().xnpv(params['discount_rate'])
|