finbourne-sdk-utils 0.0.24__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.
- features/__init__.py +0 -0
- features/main.py +11 -0
- finbourne_sdk_utils/__init__.py +8 -0
- finbourne_sdk_utils/cocoon/__init__.py +34 -0
- finbourne_sdk_utils/cocoon/async_tools.py +94 -0
- finbourne_sdk_utils/cocoon/cocoon.py +1862 -0
- finbourne_sdk_utils/cocoon/cocoon_printer.py +455 -0
- finbourne_sdk_utils/cocoon/config/domain_settings.json +125 -0
- finbourne_sdk_utils/cocoon/config/seed_sample_data.json +36 -0
- finbourne_sdk_utils/cocoon/dateorcutlabel.py +198 -0
- finbourne_sdk_utils/cocoon/instruments.py +482 -0
- finbourne_sdk_utils/cocoon/properties.py +442 -0
- finbourne_sdk_utils/cocoon/seed_sample_data.py +137 -0
- finbourne_sdk_utils/cocoon/systemConfiguration.py +92 -0
- finbourne_sdk_utils/cocoon/transaction_type_upload.py +136 -0
- finbourne_sdk_utils/cocoon/utilities.py +1877 -0
- finbourne_sdk_utils/cocoon/validator.py +243 -0
- finbourne_sdk_utils/extract/__init__.py +1 -0
- finbourne_sdk_utils/extract/group_holdings.py +400 -0
- finbourne_sdk_utils/iam/__init__.py +1 -0
- finbourne_sdk_utils/iam/roles.py +74 -0
- finbourne_sdk_utils/jupyter_tools/__init__.py +2 -0
- finbourne_sdk_utils/jupyter_tools/hide_code_button.py +23 -0
- finbourne_sdk_utils/jupyter_tools/stop_execution.py +14 -0
- finbourne_sdk_utils/logger/LusidLogger.py +41 -0
- finbourne_sdk_utils/logger/__init__.py +1 -0
- finbourne_sdk_utils/lpt/__init__.py +0 -0
- finbourne_sdk_utils/lpt/back_compat.py +20 -0
- finbourne_sdk_utils/lpt/cash_ladder.py +191 -0
- finbourne_sdk_utils/lpt/connect_lusid.py +64 -0
- finbourne_sdk_utils/lpt/connect_none.py +5 -0
- finbourne_sdk_utils/lpt/connect_token.py +9 -0
- finbourne_sdk_utils/lpt/dfq.py +321 -0
- finbourne_sdk_utils/lpt/either.py +65 -0
- finbourne_sdk_utils/lpt/get_instruments.py +101 -0
- finbourne_sdk_utils/lpt/lpt.py +374 -0
- finbourne_sdk_utils/lpt/lse.py +188 -0
- finbourne_sdk_utils/lpt/map_instruments.py +164 -0
- finbourne_sdk_utils/lpt/pager.py +32 -0
- finbourne_sdk_utils/lpt/record.py +13 -0
- finbourne_sdk_utils/lpt/refreshing_token.py +43 -0
- finbourne_sdk_utils/lpt/search_instruments.py +48 -0
- finbourne_sdk_utils/lpt/stdargs.py +154 -0
- finbourne_sdk_utils/lpt/txn_config.py +128 -0
- finbourne_sdk_utils/lpt/txn_config_yaml.py +493 -0
- finbourne_sdk_utils/pandas_utils/__init__.py +0 -0
- finbourne_sdk_utils/pandas_utils/lusid_pandas.py +128 -0
- finbourne_sdk_utils-0.0.24.dist-info/LICENSE +21 -0
- finbourne_sdk_utils-0.0.24.dist-info/METADATA +25 -0
- finbourne_sdk_utils-0.0.24.dist-info/RECORD +52 -0
- finbourne_sdk_utils-0.0.24.dist-info/WHEEL +5 -0
- finbourne_sdk_utils-0.0.24.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Validator:
|
|
5
|
+
def __init__(self, value, value_name):
|
|
6
|
+
self.value = value
|
|
7
|
+
self.value_name = value_name
|
|
8
|
+
|
|
9
|
+
def check_is_not_instance(self, instance_type):
|
|
10
|
+
|
|
11
|
+
if isinstance(self.value, instance_type):
|
|
12
|
+
raise TypeError(
|
|
13
|
+
f"The {self.value_name} must be of type {str(instance_type)}, you supplied '{str(type(self.value))}' instead."
|
|
14
|
+
)
|
|
15
|
+
return self
|
|
16
|
+
|
|
17
|
+
def check_allowed_value(self, allowed_values: list):
|
|
18
|
+
"""
|
|
19
|
+
Checks that value exists in the provided list
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
allowed_values : list
|
|
24
|
+
The list of allowed values
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
Validator
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
if self.value not in allowed_values:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"The {self.value_name} must be one of {str(allowed_values)}, you supplied '{self.value}' instead."
|
|
34
|
+
)
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def make_singular(self):
|
|
38
|
+
"""
|
|
39
|
+
Makes a plural string singular
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
Validator
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if isinstance(self.value, str):
|
|
47
|
+
self.value = self.value.rstrip("s")
|
|
48
|
+
logging.debug(
|
|
49
|
+
f"The value of {self.value_name} has had any 's' characters stripped from the right to make it singular"
|
|
50
|
+
)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def make_lower(self):
|
|
54
|
+
"""
|
|
55
|
+
Makes a string lowercase
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
Validator
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
if isinstance(self.value, str):
|
|
63
|
+
self.value = self.value.lower()
|
|
64
|
+
logging.debug(f"The value of {self.value_name} has been made lower case")
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def set_default_value_if_none(self, default):
|
|
68
|
+
"""
|
|
69
|
+
Sets a default value if the current value is None
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
default
|
|
74
|
+
The default value
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Validator
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if self.value is None:
|
|
82
|
+
self.value = default
|
|
83
|
+
logging.debug(
|
|
84
|
+
f"The value of {self.value_name} has been updated from None to {default}"
|
|
85
|
+
)
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def override_value(self, override_flag: bool, override_value):
|
|
89
|
+
"""
|
|
90
|
+
Overrides the current value of the ovverride_flag is True
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
override_flag : bool
|
|
95
|
+
Whether or not to override the current value
|
|
96
|
+
override_value
|
|
97
|
+
The value to use to override the existing values
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
Validator
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
if override_flag:
|
|
105
|
+
self.value = override_value
|
|
106
|
+
logging.debug(
|
|
107
|
+
f"The value of {self.value_name} has been overriden with {override_value}"
|
|
108
|
+
)
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def discard_dict_keys_none_value(self):
|
|
112
|
+
"""
|
|
113
|
+
Discards dictionary key, value pairs where the value is None
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
Validator
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
if isinstance(self.value, dict):
|
|
121
|
+
invalid_keys = [key for key, value in self.value.items() if value is None]
|
|
122
|
+
if len(invalid_keys) > 0:
|
|
123
|
+
logging.info(
|
|
124
|
+
f"The values for the keys {str(invalid_keys)} are None and have thus been removed from {self.value_name}"
|
|
125
|
+
)
|
|
126
|
+
for key in invalid_keys:
|
|
127
|
+
self.value.pop(key, None)
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def get_dict_values(self):
|
|
131
|
+
"""
|
|
132
|
+
Gets a list of values from a dictionary
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
Validator
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if isinstance(self.value, dict):
|
|
140
|
+
self.value = list(self.value.values())
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def filter_list_using_first_character(self, first_character: str):
|
|
144
|
+
"""
|
|
145
|
+
Filters a list of strings by looking to see if the first character matches the provided
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
first_character : str
|
|
150
|
+
The character to look for in the first character of each element
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
Validator
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
if isinstance(self.value, list):
|
|
158
|
+
to_remove = []
|
|
159
|
+
for value in self.value:
|
|
160
|
+
if isinstance(value, str) and (
|
|
161
|
+
len(value) > 0 and value[0] == first_character
|
|
162
|
+
):
|
|
163
|
+
to_remove.append(value)
|
|
164
|
+
self.value = [value for value in self.value if value not in to_remove]
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
def check_subset_of_list(self, superset: list, superset_name: str):
|
|
168
|
+
"""
|
|
169
|
+
Checks if one list is a subset of another
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
superset : list
|
|
174
|
+
The superset to check of the value is a subset of
|
|
175
|
+
superset_name : list
|
|
176
|
+
The name of the superset
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
Validator
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
if isinstance(self.value, list):
|
|
184
|
+
if len(set(self.value) - set(superset)) > 0:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"""The values {str(set(self.value) - set(superset))} exist in the {self.value_name}
|
|
187
|
+
but do not exist in the {superset_name}."""
|
|
188
|
+
)
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def check_no_intersection_with_list(self, other_list: list, list_name: str):
|
|
192
|
+
"""
|
|
193
|
+
Checks that the value has no intersection with a provided list
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
other_list : list
|
|
198
|
+
The list to check the value has no intersection with
|
|
199
|
+
list_name : list
|
|
200
|
+
The name of the list
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
Validator
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
if isinstance(self.value, list):
|
|
209
|
+
if len(set(other_list).intersection(set(self.value))) > 0:
|
|
210
|
+
err = f"""The columns {str(set(other_list).intersection(set(self.value)))} are specified in {self.value_name}
|
|
211
|
+
yet they contain null (NaN) values for some rows in the provided data. Null values are not
|
|
212
|
+
allowed for required fields. Please ensure that required columns do not contain ANY null
|
|
213
|
+
values or specify a default value in your mapping by specifying a dictionary with the keys
|
|
214
|
+
"column" and "default"."""
|
|
215
|
+
logging.error(err)
|
|
216
|
+
raise ValueError(err)
|
|
217
|
+
|
|
218
|
+
def check_entries_are_strings_or_dict_containing_key(self, expected_key):
|
|
219
|
+
missing_items = []
|
|
220
|
+
|
|
221
|
+
for item in self.value:
|
|
222
|
+
if not (isinstance(item, dict) or isinstance(item, str)):
|
|
223
|
+
missing_items.append(f"{item} is not a string or dictionary")
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
if isinstance(item, dict):
|
|
227
|
+
if expected_key not in item:
|
|
228
|
+
missing_items.append(
|
|
229
|
+
f"{item} does not contain the mandatory 'source' key"
|
|
230
|
+
)
|
|
231
|
+
continue
|
|
232
|
+
if not isinstance(item[expected_key], str):
|
|
233
|
+
missing_items.append(
|
|
234
|
+
f"{item[expected_key]} in {item} is not a string"
|
|
235
|
+
)
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if len(missing_items) > 0:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"The value {self.value} provided in {self.value_name} is invalid. {', '.join(missing_items)}."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return self
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from finbourne_sdk_utils.extract.group_holdings import get_holdings_for_group
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
from finbourne_sdk_utils.cocoon.async_tools import run_in_executor, ThreadPool
|
|
2
|
+
import lusid
|
|
3
|
+
import asyncio
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
from finbourne_sdk_utils.cocoon.async_tools import (
|
|
7
|
+
start_event_loop_new_thread,
|
|
8
|
+
stop_event_loop_new_thread,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _join_holdings(
|
|
13
|
+
holdings_to_join: Dict[str, List[lusid.models.PortfolioHolding]],
|
|
14
|
+
group_by_portfolio: bool = False,
|
|
15
|
+
dict_key="GroupHoldings",
|
|
16
|
+
) -> Dict[str, List[lusid.models.PortfolioHolding]]:
|
|
17
|
+
"""
|
|
18
|
+
This function joins the holdings together from multiple Portfolios into a single list of PortfolioHolding
|
|
19
|
+
|
|
20
|
+
Currently the following constraints exist:
|
|
21
|
+
|
|
22
|
+
1) Only support for instrument properties where the first one is taken
|
|
23
|
+
2) Sub-holding-keys are ignored
|
|
24
|
+
3) The portfolio currency is assumed to be the same across all Portfolios
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
holdings_to_join : Dict[str, List[lusid.models.PortfolioHolding]
|
|
29
|
+
The dictionary of lists of PortfolioHolding keyed by the unique combination of Scope and Code
|
|
30
|
+
for the Portfolio Group or Portfolios. These will either be returned as is or joined to form a new dictionary
|
|
31
|
+
with a single key against a single list of PortfolioHolding.
|
|
32
|
+
|
|
33
|
+
group_by_portfolio : bool
|
|
34
|
+
Whether or not to group the holdings by portfolio
|
|
35
|
+
|
|
36
|
+
dict_key : str
|
|
37
|
+
The key to use for the merged holdings, usually the scope/code combination of the Portfolio Group
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
holdings_joined : Dict[str, List[lusid.models.PortfolioHolding]
|
|
42
|
+
The joined dictionary of a list of PortfolioHolding which either has a single key for the Portfolio
|
|
43
|
+
Group or a key for each Portfolio in the group
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# If each portfolio should remain separated
|
|
47
|
+
if group_by_portfolio:
|
|
48
|
+
return holdings_to_join
|
|
49
|
+
|
|
50
|
+
# Flatten the holdings into a single list
|
|
51
|
+
all_holdings = [
|
|
52
|
+
holding
|
|
53
|
+
for holding_list in holdings_to_join.values()
|
|
54
|
+
for holding in holding_list
|
|
55
|
+
]
|
|
56
|
+
# Initialise a dictionary to hold the positions against a key
|
|
57
|
+
all_holdings_keyed = {}
|
|
58
|
+
# Construct the keyed dictionary using the LUSID instrument id, holding type and currency as the unique key for each holding
|
|
59
|
+
[
|
|
60
|
+
all_holdings_keyed.setdefault(
|
|
61
|
+
f"{holding.instrument_uid}:{holding.holding_type}:{holding.cost.currency}",
|
|
62
|
+
[],
|
|
63
|
+
).append(holding)
|
|
64
|
+
for holding in all_holdings
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# Initialise a list to hold the joined holdings
|
|
68
|
+
joined_holdings = []
|
|
69
|
+
|
|
70
|
+
# Reduce the list of holdings against each key to a single holding
|
|
71
|
+
for key, value in all_holdings_keyed.items():
|
|
72
|
+
joined_holdings.append(
|
|
73
|
+
lusid.models.PortfolioHolding(
|
|
74
|
+
# Use the instrument_uid from the key
|
|
75
|
+
instrument_uid=key.split(":")[0],
|
|
76
|
+
# Use the holding type from the key
|
|
77
|
+
holding_type=key.split(":")[1],
|
|
78
|
+
# Add the metric fields together
|
|
79
|
+
units=reduce((lambda x, y: x + y), list(map(lambda x: x.units, value))),
|
|
80
|
+
settled_units=reduce(
|
|
81
|
+
(lambda x, y: x + y), list(map(lambda x: x.settled_units, value))
|
|
82
|
+
),
|
|
83
|
+
cost=lusid.models.CurrencyAndAmount(
|
|
84
|
+
# Use the currency form the key
|
|
85
|
+
currency=key.split(":")[2],
|
|
86
|
+
amount=reduce(
|
|
87
|
+
(lambda x, y: x + y), list(map(lambda x: x.cost.amount, value))
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
cost_portfolio_ccy=lusid.models.CurrencyAndAmount(
|
|
91
|
+
# Use the first currency, these holdings could be from different portfolios, so the validity of this is questionable
|
|
92
|
+
currency=value[0].cost_portfolio_ccy.currency,
|
|
93
|
+
amount=reduce(
|
|
94
|
+
(lambda x, y: x + y),
|
|
95
|
+
list(map(lambda x: x.cost_portfolio_ccy.amount, value)),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
# Takes the properties from the first value, only allows instrument properties
|
|
99
|
+
properties={
|
|
100
|
+
property_key: value
|
|
101
|
+
for property_key, value in value[0].properties.items()
|
|
102
|
+
if value.key.split("/")[0] == "Instrument"
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return {dict_key: joined_holdings}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@run_in_executor
|
|
111
|
+
def _get_portfolio_group(
|
|
112
|
+
api_factory: lusid.SyncApiClientFactory, scope: str, code: str, **kwargs
|
|
113
|
+
) -> lusid.models.PortfolioGroup:
|
|
114
|
+
"""
|
|
115
|
+
This function gets a Portfolio Group from LUSID.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
api_factory : lusid.SyncApiClientFactory
|
|
120
|
+
The api factory to use
|
|
121
|
+
scope : str
|
|
122
|
+
The scope of the Portfolio Group
|
|
123
|
+
code : str
|
|
124
|
+
The code of the Portfolio Group, with the scope this uniquely identifiers the Portfolio Group
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
response : lusid.models.PortfolioGroup
|
|
129
|
+
The Portfolio Group
|
|
130
|
+
|
|
131
|
+
Other Parameters
|
|
132
|
+
------
|
|
133
|
+
effective_at : datetime
|
|
134
|
+
The effective datetime at which to get the Portfolio Group
|
|
135
|
+
as_at : datetime
|
|
136
|
+
The as at datetime at which to get the Portfolio Group
|
|
137
|
+
thread_pool
|
|
138
|
+
The thread pool to run this function in
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# Filter out the relevant keyword arguments as the LUSID API will raise an exception if given extras
|
|
142
|
+
lusid_keyword_arguments = {
|
|
143
|
+
key: value for key, value in kwargs.items() if key in ["effective_at", "as_at"]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Call LUSID to get the portfolio group
|
|
147
|
+
response = api_factory.build(lusid.api.PortfolioGroupsApi).get_portfolio_group(scope=scope, code=code, **lusid_keyword_arguments)
|
|
148
|
+
|
|
149
|
+
return response
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@run_in_executor
|
|
153
|
+
def _get_portfolio_holdings(
|
|
154
|
+
api_factory: lusid.SyncApiClientFactory, scope: str, code: str, **kwargs
|
|
155
|
+
) -> Dict[str, List[lusid.models.PortfolioHolding]]:
|
|
156
|
+
"""
|
|
157
|
+
This function gets the holdings of a Portfolio from LUSID.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
api_factory : lusid.SyncApiClientFactory
|
|
162
|
+
The api factory to use
|
|
163
|
+
scope : str
|
|
164
|
+
The scope of the Portfolio
|
|
165
|
+
code : str
|
|
166
|
+
The code of the Portfolio, with the scope this uniquely identifiers the Portfolio
|
|
167
|
+
effective_at : datetime
|
|
168
|
+
The effective datetime at which to get the Portfolio Group
|
|
169
|
+
as_at : datetime
|
|
170
|
+
The as at datetime at which to get the Portfolio Group
|
|
171
|
+
filter : str
|
|
172
|
+
by_taxlots : bool
|
|
173
|
+
property_keys : list[str]
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
response : Dict[str, List[lusid.models.PortfolioHolding]]
|
|
178
|
+
The list of PortfolioHolding keyed by the unique combination of the Portfolio's scope and code
|
|
179
|
+
|
|
180
|
+
Other Parameters
|
|
181
|
+
------
|
|
182
|
+
effective_at : datetime
|
|
183
|
+
The effective datetime at which to get the Portfolio Group
|
|
184
|
+
as_at : datetime
|
|
185
|
+
The as at datetime at which to get the Portfolio Group
|
|
186
|
+
filter : str
|
|
187
|
+
The filter to use to filter the holdings
|
|
188
|
+
by_taxlots : bool
|
|
189
|
+
Whether or not to break the holdings down into individual tax lots
|
|
190
|
+
property_keys : list[str]
|
|
191
|
+
The list of property keys to decorate onto the holdings, must be from the Instrument domain
|
|
192
|
+
thread_pool
|
|
193
|
+
The thread pool to run this function in
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
# Filter out the relevant keyword arguments as the LUSID API will raise an exception if given extras
|
|
197
|
+
lusid_keyword_arguments = {
|
|
198
|
+
key: value
|
|
199
|
+
for key, value in kwargs.items()
|
|
200
|
+
if key in ["effective_at", "as_at", "filter", "by_taxlots", "property_keys"]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Call LUSID to get the holdings for the Portfolio
|
|
204
|
+
response = api_factory.build(lusid.api.TransactionPortfoliosApi).get_holdings(scope=scope, code=code, **lusid_keyword_arguments)
|
|
205
|
+
|
|
206
|
+
# Key the response with the unique scope/code combination
|
|
207
|
+
return {f"{scope} : {code}": response.values}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def _get_holdings_for_group_recursive(
|
|
211
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
212
|
+
group_scope: str,
|
|
213
|
+
group_code: str,
|
|
214
|
+
group_by_portfolio=False,
|
|
215
|
+
**kwargs,
|
|
216
|
+
) -> Dict[str, List[lusid.models.PortfolioHolding]]:
|
|
217
|
+
"""
|
|
218
|
+
This function recursively gets the holdings for a Portfolio Group in LUSID by making a request to get the holdings for
|
|
219
|
+
each sub-group and portofolio and then joining the results together above the API.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
api_factory : lusid.SyncApiClientFactory
|
|
224
|
+
The api factory to use
|
|
225
|
+
group_scope : str
|
|
226
|
+
The scope of the Portfolio Group
|
|
227
|
+
group_code : str
|
|
228
|
+
The code of the Portfolio Group
|
|
229
|
+
group_by_portfolio : bool
|
|
230
|
+
Whether or not to group the holdings by Portfolio, if False will merge all Holdings together based on Instrument
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
Dict[str, List[lusid.models.PortfolioHolding]]
|
|
235
|
+
The single set of holdings
|
|
236
|
+
|
|
237
|
+
Other Parameters
|
|
238
|
+
------
|
|
239
|
+
effective_at : datetime
|
|
240
|
+
The effective datetime at which to get the Portfolio Group
|
|
241
|
+
as_at : datetime
|
|
242
|
+
The as at datetime at which to get the Portfolio Group
|
|
243
|
+
filter : str
|
|
244
|
+
The filter to use to filter the holdings
|
|
245
|
+
by_taxlots : bool
|
|
246
|
+
Whether or not to break the holdings down into individual tax lots
|
|
247
|
+
property_keys : list[str]
|
|
248
|
+
The list of property keys to decorate onto the holdings, must be from the Instrument domain
|
|
249
|
+
thread_pool
|
|
250
|
+
The thread pool to run this function in
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
# Get the details for the Portfolio Group including its sub-group and Portfolio members
|
|
254
|
+
response = await _get_portfolio_group(
|
|
255
|
+
api_factory, group_scope, group_code, **kwargs
|
|
256
|
+
)
|
|
257
|
+
portfolios = response.portfolios
|
|
258
|
+
sub_groups = response.sub_groups
|
|
259
|
+
|
|
260
|
+
# Get the holdings for each portfolio
|
|
261
|
+
portfolio_holdings = await asyncio.gather(
|
|
262
|
+
*[
|
|
263
|
+
_get_portfolio_holdings(
|
|
264
|
+
api_factory=api_factory,
|
|
265
|
+
scope=portfolio.scope,
|
|
266
|
+
code=portfolio.code,
|
|
267
|
+
**kwargs,
|
|
268
|
+
)
|
|
269
|
+
for portfolio in portfolios
|
|
270
|
+
],
|
|
271
|
+
return_exceptions=False,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Turn the list of dictionaries into a single dictionary where the holdings are keyed by the Portfolio scope : code
|
|
275
|
+
portfolio_holdings_keyed = {k: v for d in portfolio_holdings for k, v in d.items()}
|
|
276
|
+
|
|
277
|
+
# Join the holdings across all portfolios together
|
|
278
|
+
joined_portfolio_holdings = _join_holdings(
|
|
279
|
+
portfolio_holdings_keyed,
|
|
280
|
+
group_by_portfolio,
|
|
281
|
+
dict_key=f"{group_scope} : {group_code}",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# If there aren't any sub-groups return these joined holdings
|
|
285
|
+
if len(sub_groups) == 0:
|
|
286
|
+
return joined_portfolio_holdings
|
|
287
|
+
# Otherwise get the joined holdings for the sub-groups
|
|
288
|
+
else:
|
|
289
|
+
sub_group_holdings = await asyncio.gather(
|
|
290
|
+
*[
|
|
291
|
+
_get_holdings_for_group_recursive(
|
|
292
|
+
api_factory=api_factory,
|
|
293
|
+
group_scope=sub_group.scope,
|
|
294
|
+
group_code=sub_group.code,
|
|
295
|
+
group_by_portfolio=group_by_portfolio,
|
|
296
|
+
**kwargs,
|
|
297
|
+
)
|
|
298
|
+
for sub_group in sub_groups
|
|
299
|
+
],
|
|
300
|
+
return_exceptions=False,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Turn the list of dictionaries into a single dictionary where the holdings are keyed by the Portfolio scope : code
|
|
304
|
+
sub_group_holdings_keyed = {
|
|
305
|
+
k: v for d in sub_group_holdings for k, v in d.items()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Join together the holdings across all the sub-groups
|
|
309
|
+
joined_sub_group_holdings = _join_holdings(
|
|
310
|
+
sub_group_holdings_keyed, group_by_portfolio, dict_key="SubGroupHoldings"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# There shouldn't be any overlap between the keys in these two sets
|
|
314
|
+
if (
|
|
315
|
+
len(
|
|
316
|
+
set(joined_sub_group_holdings.keys()).intersection(
|
|
317
|
+
set(joined_portfolio_holdings)
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
> 0
|
|
321
|
+
):
|
|
322
|
+
raise ValueError(
|
|
323
|
+
"There are duplicate Portfolios in the Portfolio Group. This is not currently supported. "
|
|
324
|
+
"Please ensure that there are no duplicates in your Porfolio Group via sub-groups and try again"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Join and return the joined sub-group holdings with the joined portfolio holdings
|
|
328
|
+
return _join_holdings(
|
|
329
|
+
{**joined_sub_group_holdings, **joined_portfolio_holdings},
|
|
330
|
+
group_by_portfolio,
|
|
331
|
+
dict_key=f"{group_scope} : {group_code}",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_holdings_for_group(
|
|
336
|
+
api_factory: lusid.SyncApiClientFactory,
|
|
337
|
+
group_scope: str,
|
|
338
|
+
group_code: str,
|
|
339
|
+
group_by_portfolio: bool = False,
|
|
340
|
+
num_threads=5,
|
|
341
|
+
**kwargs,
|
|
342
|
+
) -> Dict[str, List[lusid.models.PortfolioHolding]]:
|
|
343
|
+
"""
|
|
344
|
+
This function gets the holdings for a Portfolio Group in LUSID.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
api_factory : lusid.SyncApiClientFactory
|
|
349
|
+
The api factory to use
|
|
350
|
+
group_scope : str
|
|
351
|
+
The scope of the Portfolio Group
|
|
352
|
+
group_code : str
|
|
353
|
+
The code of the Portfolio Group
|
|
354
|
+
group_by_portfolio : bool
|
|
355
|
+
Whether or not to group the holdings by Portfolio, if False will merge all Holdings together based on Instrument
|
|
356
|
+
num_threads : int
|
|
357
|
+
The number of threads to use for asynchronous programming
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
group_holdings : Dict[str, List[lusid.models.PortfolioHolding]]
|
|
362
|
+
The single set of holdings either keyed by the Portfolio scope/code or the Portfolio Group scope/code
|
|
363
|
+
|
|
364
|
+
Other Parameters
|
|
365
|
+
------
|
|
366
|
+
effective_at : datetime
|
|
367
|
+
The effective datetime at which to get the Portfolio Group
|
|
368
|
+
as_at : datetime
|
|
369
|
+
The as at datetime at which to get the Portfolio Group
|
|
370
|
+
filter : str
|
|
371
|
+
The filter to use to filter the holdings
|
|
372
|
+
by_taxlots : bool
|
|
373
|
+
Whether or not to break the holdings down into individual tax lots
|
|
374
|
+
property_keys : list[str]
|
|
375
|
+
The list of property keys to decorate onto the holdings, must be from the Instrument domain
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
# Create a new thread pool to run the asynchronous tasks in
|
|
379
|
+
thread_pool = ThreadPool(num_threads).thread_pool
|
|
380
|
+
kwargs["thread_pool"] = thread_pool
|
|
381
|
+
|
|
382
|
+
# Start a new event loop in a new thread, this is required to run inside a Jupyter notebook
|
|
383
|
+
loop = start_event_loop_new_thread()
|
|
384
|
+
|
|
385
|
+
# Get the responses from LUSID
|
|
386
|
+
group_holdings = asyncio.run_coroutine_threadsafe(
|
|
387
|
+
_get_holdings_for_group_recursive(
|
|
388
|
+
api_factory=api_factory,
|
|
389
|
+
group_scope=group_scope,
|
|
390
|
+
group_code=group_code,
|
|
391
|
+
group_by_portfolio=group_by_portfolio,
|
|
392
|
+
**kwargs,
|
|
393
|
+
),
|
|
394
|
+
loop,
|
|
395
|
+
).result()
|
|
396
|
+
|
|
397
|
+
# Stop the additional event loop
|
|
398
|
+
stop_event_loop_new_thread(loop)
|
|
399
|
+
|
|
400
|
+
return group_holdings
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import finbourne_sdk_utils.iam.roles
|