bw-essentials-core 0.0.1__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.

Potentially problematic release.


This version of bw-essentials-core might be problematic. Click here for more details.

@@ -0,0 +1,406 @@
1
+ """
2
+ Module to make API calls to User Portfolio service.
3
+ """
4
+ import json
5
+ import logging
6
+
7
+ from bw_essentials.constants.services import Services
8
+ from bw_essentials.services.api_client import ApiClient
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class UserPortfolio(ApiClient):
14
+ """
15
+ Class for making API calls to the User Portfolio Service.
16
+
17
+ Args:
18
+ user (str): The user for whom the API calls are being made.
19
+ """
20
+
21
+ def __init__(self, service_user: str):
22
+ logger.info(f"Initializing UserPortfolio client for user: {service_user}")
23
+ super().__init__(user=service_user)
24
+ self.base_url = self.get_base_url(Services.USER_PORTFOLIO.value)
25
+ self.name = Services.USER_PORTFOLIO.value
26
+ self.urls = {
27
+ "holding_transaction": "holding/transaction",
28
+ "portfolio_rebalance": "userportfolio/portfolio/rebalance",
29
+ "rebalance_transaction": "userportfolio/portfolio/rebalance/transaction",
30
+ "user_instructions": "userportfolio/portfolio/rebalance/transaction/user-instruction",
31
+ "user_inputs": "userportfolio/portfolio/rebalance",
32
+ "user_portfolio_holdings": "holding/holdings",
33
+ "user_holdings": "holding/user/holdings",
34
+ "user_portfolios": "userportfolio/portfolio",
35
+ "orders": "userportfolio/portfolio/rebalance/orders",
36
+ "update_user_portfolio_rebalance": "userportfolio/portfolio/rebalance",
37
+ "update_portfolio_transaction": "userportfolio/portfolio/rebalance/transactions",
38
+ "complete_rebalance_transaction": "userportfolio/portfolio/rebalance/transactions/complete",
39
+ "get_portfolio_rebalances": "userportfolio/portfolio/rebalance",
40
+ "create_basket": "userportfolio/basket",
41
+ "order_instructions": "userportfolio/order/instructions/",
42
+ "retry": "userportfolio/order/retry/basket",
43
+ "skip": "userportfolio/order/skip/basket",
44
+ "create_order_instructions": "userportfolio/order/order-instruction",
45
+ "basket_details": "userportfolio/basket",
46
+ "broker_users_holdings": "holding/{}/users"
47
+ }
48
+
49
+ def create_holding_transaction(self, payload):
50
+ """
51
+ Make a holding transaction.
52
+
53
+ Args:
54
+ payload (str): The payload for the holding transaction.
55
+ Returns:
56
+ dict: Holding transaction data.
57
+ """
58
+ logger.info(f"In - holding_transaction {payload =}")
59
+ data = self._post(url=self.base_url,
60
+ endpoint=self.urls.get("holding_transaction"),
61
+ data=payload)
62
+ logger.info(f"{data =}")
63
+ return data.get("data")
64
+
65
+ def create_portfolio_rebalance(self, payload):
66
+ """
67
+ Perform a portfolio rebalance.
68
+
69
+ Args:
70
+ payload (str): The payload for the portfolio rebalance.
71
+
72
+ Returns:
73
+ dict: Portfolio rebalance data.
74
+ """
75
+ logger.info(f"In - portfolio_rebalance {payload =}")
76
+ data = self._post(url=self.base_url,
77
+ endpoint=self.urls.get("portfolio_rebalance"),
78
+ data=payload)
79
+ logger.info(f"{data =}")
80
+ return data.get("data")
81
+
82
+ def create_rebalance_transaction(self, payload):
83
+ """
84
+ Perform a rebalance transaction.
85
+
86
+ Args:
87
+ payload (str): The payload for the rebalance transaction.
88
+
89
+ Returns:
90
+ dict: Rebalance transaction data.
91
+ """
92
+ logger.info(f"In - rebalance_transaction {payload =}")
93
+ data = self._post(url=self.base_url,
94
+ endpoint=self.urls.get("rebalance_transaction"),
95
+ data=payload)
96
+ logger.info(f"{data =}")
97
+ return data.get("data")
98
+
99
+ def update_user_instructions(self, payload):
100
+ """
101
+ Provide user instructions.
102
+
103
+ Args:
104
+ payload (str): The payload for user instructions.
105
+
106
+ """
107
+ logger.info(f"In - user_instructions {payload =}")
108
+ payload['filled_quantity'] = payload['quantity']
109
+ payload = json.dumps(payload)
110
+ data = self._put(url=self.base_url,
111
+ endpoint=self.urls.get("user_instructions"),
112
+ data=payload)
113
+ logger.info(f"{data =}")
114
+ return data.get("data")
115
+
116
+ def get_user_inputs(self, params, user_portfolio_id):
117
+ """
118
+ Get user inputs for a specific user portfolio.
119
+
120
+ Args:
121
+ params (dict): Additional parameters for the request.
122
+ user_portfolio_id (str): The ID of the user portfolio.
123
+
124
+ Returns:
125
+ dict: User inputs data.
126
+ """
127
+ logger.info(f"In - user_inputs {params =}, {user_portfolio_id =}")
128
+ data = self._get(url=self.base_url,
129
+ endpoint=f'{self.urls.get("user_inputs")}/{user_portfolio_id}',
130
+ params=params)
131
+ logger.info(f"{data =}")
132
+ return data.get("data")
133
+
134
+ def get_user_portfolio_holdings(self, user_portfolio_id):
135
+ """
136
+ Get user holdings for a specific user portfolio.
137
+
138
+ Args:
139
+ user_portfolio_id (str): The ID of the user portfolio.
140
+ Returns:
141
+ dict: User holdings data.
142
+ """
143
+ logger.info(f"In - user_holdings {user_portfolio_id =}")
144
+ data = self._get(url=self.base_url,
145
+ endpoint=f'{self.urls.get("user_portfolio_holdings")}/{user_portfolio_id}')
146
+ logger.info(f"{data =}")
147
+ return data.get("data")
148
+
149
+ def get_user_holdings(self, user_id, broker):
150
+ """
151
+ Get user holdings for a all user portfolios.
152
+
153
+ Args:
154
+ user_id (str): The ID of the user portfolio.
155
+ broker (str): Broker of user
156
+
157
+ Returns:
158
+ dict: User holdings data.
159
+ """
160
+ logger.info(f"In - user_holdings {user_id = }, {broker = }")
161
+ data = self._get(url=self.base_url,
162
+ endpoint=f'{self.urls.get("user_holdings")}/{user_id}',
163
+ params={"broker": broker})
164
+ logger.info(f"{data =}")
165
+ return data.get("data")
166
+
167
+ def get_user_portfolio_by_id(self, user_portfolio_id):
168
+ """
169
+ Retrieves a user's portfolio data by the provided ID.
170
+
171
+ Args:
172
+ - user_portfolio_id (str): The ID of the user's portfolio.
173
+
174
+ Returns:
175
+ - dict: The data associated with the user's portfolio.
176
+ """
177
+ logger.info(f"In - user_holdings {user_portfolio_id =}")
178
+ data = self._get(url=self.base_url,
179
+ endpoint=f'{self.urls.get("user_portfolios")}/{user_portfolio_id}')
180
+ logger.info(f"{data =}")
181
+ return data.get("data")
182
+
183
+ def get_rebalance_orders(self, rebalance_type, user_portfolio_rebalance_id):
184
+ """
185
+ Fetches rebalance orders for a specific user portfolio rebalance.
186
+
187
+ Args:
188
+ - rebalance_type: Type of rebalance.
189
+ - user_portfolio_rebalance_id: ID of the user's portfolio rebalance.
190
+
191
+ Returns:
192
+ - dict: Data related to rebalance orders.
193
+ """
194
+ logger.info(f"In - rebalance_orders {rebalance_type =}, {user_portfolio_rebalance_id =}")
195
+ data = self._get(url=self.base_url,
196
+ endpoint=f'{self.urls.get("orders")}',
197
+ params={"type": rebalance_type,
198
+ "user_portfolio_rebalance_id": user_portfolio_rebalance_id
199
+ })
200
+ logger.info(f"{data =}")
201
+ return data.get("data")
202
+
203
+ def update_user_portfolio_rebalance(self, payload, user_portfolio_rebalance_id):
204
+ """
205
+ Update a user's portfolio rebalance.
206
+
207
+ Args:
208
+ - payload (dict): The data payload to update the user's portfolio rebalance.
209
+ - user_portfolio_rebalance_id (int): The ID of the user's portfolio rebalance to be updated.
210
+
211
+ Returns:
212
+ - dict: The updated data of the user's portfolio rebalance.
213
+ """
214
+ logger.info(f"In - update_user_portfolio_rebalance {payload =}, {user_portfolio_rebalance_id =}")
215
+ data = self._put(url=self.base_url,
216
+ endpoint=f"{self.urls.get('update_user_portfolio_rebalance')}/{user_portfolio_rebalance_id}/",
217
+ data=payload)
218
+ logger.info(f"{data =}")
219
+ return data.get("data")
220
+
221
+ def update_portfolio_transaction(self, payload, portfolio_rebalance_transaction_id):
222
+ """
223
+ Updates the portfolio transaction with the provided payload.
224
+
225
+ Parameters:
226
+ - payload (str): JSON-formatted payload containing information to update the portfolio transaction.
227
+ - portfolio_rebalance_transaction_id (str): The ID of the portfolio rebalance transaction to be updated.
228
+
229
+ Returns:
230
+ str: The updated data from the portfolio transaction.
231
+
232
+ Note:
233
+ This method sends a PUT request to the specified endpoint to update the portfolio transaction.
234
+ """
235
+ logger.info(f"In - update_portfolio_transaction {payload =}, {portfolio_rebalance_transaction_id =}")
236
+ data = self._put(url=self.base_url,
237
+ endpoint=f"{self.urls.get('update_portfolio_transaction')}/{portfolio_rebalance_transaction_id}",
238
+ data=payload)
239
+ logger.info(f"{data =}")
240
+ return data.get("data")
241
+
242
+ def complete_rebalance_transaction(self, portfolio_rebalance_transaction_id, status=None):
243
+ """
244
+ Marks a portfolio rebalance as complete based on the provided data.
245
+
246
+ Parameters:
247
+ - item (RebalanceComplete): An instance of the RebalanceComplete class containing relevant information.
248
+ - request (Request): An instance of the Request class representing the incoming request.
249
+
250
+ Returns:
251
+ dict:
252
+ """
253
+ logger.info(f"In - update_portfolio_transaction, {portfolio_rebalance_transaction_id =}")
254
+ payload = {}
255
+ if status:
256
+ payload = json.dumps({
257
+ "status": status
258
+ })
259
+ data = self._put(url=self.base_url,
260
+ endpoint=f"{self.urls.get('complete_rebalance_transaction')}/{portfolio_rebalance_transaction_id}",
261
+ data=payload)
262
+ logger.info(f"{data =}")
263
+ return data.get("data")
264
+
265
+ def get_portfolio_rebalances(self, user_portfolio_id, current_state):
266
+ """
267
+ Retrieve portfolio rebalances data.
268
+
269
+ This method retrieves portfolio rebalances data for a specific user portfolio and current state.
270
+
271
+ Args:
272
+ user_portfolio_id (int): The ID of the user portfolio.
273
+ current_state (list): The current state of the portfolio.
274
+
275
+ Returns:
276
+ dict: Portfolio rebalances data.
277
+ """
278
+ logger.info(f"In - update_portfolio_transaction, {user_portfolio_id =}, {current_state =}")
279
+ params = {
280
+ 'current_state': ','.join(current_state)
281
+ }
282
+ data = self._get(url=self.base_url,
283
+ endpoint=f"{self.urls.get('get_portfolio_rebalances')}/{user_portfolio_id}",
284
+ params=params)
285
+ logger.info(f"{data =}")
286
+ return data.get("data")
287
+
288
+ def create_basket(self, basket_payload):
289
+ """
290
+ Creates a new basket by sending a POST request with the provided basket payload.
291
+
292
+ This method sends a POST request to create a basket using the specified `basket_payload`.
293
+ The response is logged and the data is returned.
294
+
295
+ Args:
296
+ basket_payload (json()): A dictionary containing the details for creating a new basket.
297
+ It should include all necessary fields to create the basket, such as user ID, model ID,
298
+ basket type, product type, and other relevant data.
299
+
300
+ Returns:
301
+ dict: A dictionary containing the response data, including the created basket details.
302
+ It returns the value of the 'data' field from the response.
303
+
304
+ Logs:
305
+ Logs the request payload and the response data for debugging and traceability.
306
+ """
307
+ logger.info(f"In create_basket {basket_payload =}")
308
+ data = self._post(url=self.base_url,
309
+ endpoint=self.urls.get('create_basket'),
310
+ data=basket_payload)
311
+ logger.info(f"{data =}")
312
+ return data.get('data')
313
+
314
+ def update_order_instructions(self, payload):
315
+ """
316
+ Sends order instructions by updating the order details through a PUT request.
317
+
318
+ This method takes the given `payload`, updates the filled quantity, converts it to a JSON string,
319
+ and sends it via a PUT request to update the order instructions. The response is logged and returned.
320
+
321
+ Args:
322
+ payload (dict): A dictionary containing the order details, including the symbol,
323
+ quantity, and other relevant order information. The 'filled_quantity' field is automatically
324
+ set to the value of 'quantity' in the payload.
325
+
326
+ Returns:
327
+ dict: A dictionary containing the response data, which includes the updated order instructions.
328
+ It returns the value of the 'data' field from the response.
329
+
330
+ Logs:
331
+ Logs the request payload and the response data for debugging and traceability.
332
+ """
333
+ logger.info(f"In - user_instructions {payload =}")
334
+ payload['filled_quantity'] = payload['quantity']
335
+ payload = json.dumps(payload)
336
+ data = self._put(url=self.base_url,
337
+ endpoint=self.urls.get("order_instructions"),
338
+ data=payload)
339
+ logger.info(f"{data =}")
340
+ return data.get("data")
341
+
342
+ def create_basket_orders(self, basket_id, action: str):
343
+ """
344
+ Processes basket orders based on the specified action (retry or skip).
345
+
346
+ Args:
347
+ basket_id (int): The ID of the user's basket.
348
+ action (str): The action to perform, either 'retry' or 'skip'.
349
+
350
+ Returns:
351
+ dict: The response data containing information about the processed orders.
352
+ """
353
+ logger.info(f"Processing basket orders for {basket_id =} with action '{action}'")
354
+ endpoint = f"{self.urls.get(action)}/{basket_id}"
355
+ data = self._post(url=self.base_url, endpoint=endpoint, data={})
356
+
357
+ logger.info(f"Response data: {data =}")
358
+ return data.get('data')
359
+
360
+ def create_instructions(self, payload: str) -> list:
361
+ """
362
+ Sends a request to create order instructions based on the provided payload.
363
+
364
+ Args:
365
+ payload (str): The JSON string payload containing the instructions data.
366
+
367
+ Returns:
368
+ list: A list of newly created instructions data from the response.
369
+ """
370
+ logger.info(f"In create_instructions: {payload =}")
371
+
372
+ endpoint = self.urls.get('create_order_instructions')
373
+ response = self._post(
374
+ url=self.base_url,
375
+ endpoint=endpoint,
376
+ data=payload)
377
+ return response.get('data')
378
+
379
+ def get_basket_details(self, user_id, current_state):
380
+ """
381
+ Fetches basket details for a given user based on the current state.
382
+
383
+ Parameters:
384
+ user_id (str): The user identifier.
385
+ current_state (str): The current state of the basket (e.g., 'uninvested', 'invested').
386
+
387
+ Returns:
388
+ Optional[Dict]: The basket details if available, otherwise None.
389
+ """
390
+ logger.info(f"In basket_details {user_id =}, {current_state =}")
391
+ endpoint = self.urls.get('basket_details')
392
+ params = {
393
+ 'user_id': user_id,
394
+ 'current_state': current_state
395
+ }
396
+ response = self._get(url=self.base_url,
397
+ endpoint=endpoint,
398
+ params=params)
399
+
400
+ return response.get('data')
401
+
402
+ def get_broker_users_holdings(self, broker):
403
+ logger.info(f"In broker_user_holdings {broker =}")
404
+ endpoint = self.urls.get('broker_users_holdings').format(broker)
405
+ response = self._get(url=self.base_url, endpoint=endpoint)
406
+ return response.get('data')
@@ -0,0 +1,153 @@
1
+ """
2
+ Module to interact with the User Reporting service.
3
+
4
+ This module provides a Python client interface for making API calls to the User Reporting
5
+ microservice. It supports operations such as:
6
+
7
+ - Submitting user instructions (buy/sell orders) for portfolios
8
+ - Retrieving overall portfolio performance metrics
9
+ - Retrieving a detailed breakdown of portfolio performance
10
+
11
+ The `UserReporting` class inherits from a generic `ApiClient` and uses shared service
12
+ constants from `bw_essentials`. It is initialized with user-level and request-level
13
+ payloads and handle responses from the service.
14
+
15
+ Typical use cases include:
16
+ - Sending trade execution data from the frontend/backend to the reporting service
17
+ - Fetching user portfolio performance data for dashboards or reports
18
+
19
+ Dependencies:
20
+ - bw_essentials.constants.services.Services
21
+ - bw_essentials.services.api_client.ApiClient
22
+
23
+ Example usage:
24
+ reporting_client = UserReporting(
25
+ service_user="portfolio_service"
26
+ )
27
+
28
+ # Submit instructions
29
+ reporting_client.add_instructions(request_data)
30
+
31
+ # Fetch performance
32
+ performance = reporting_client.get_portfolio_performance(user_id, portfolio_id)
33
+
34
+ # Fetch breakdown
35
+ breakdown = reporting_client.get_portfolio_performance_breakdown(user_id, portfolio_id)
36
+ """
37
+
38
+ import json
39
+ import logging
40
+
41
+ from bw_essentials.constants.services import Services
42
+ from bw_essentials.services.api_client import ApiClient
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class UserReporting(ApiClient):
48
+ SELL = 'sell'
49
+ BUY = 'buy'
50
+ MTF = 'mtf'
51
+ INTRADAY = 'intraday'
52
+ EQUITY = 'equity'
53
+
54
+ def __init__(self,
55
+ service_user: str):
56
+ """
57
+ Initializes the UserReporting client with user, base URL, request ID, tenant ID..
58
+
59
+ :param service_user: Service name or username initiating the request
60
+ """
61
+ logger.info(f"Initializing UserReporting client for user: {service_user}")
62
+ super().__init__(user=service_user)
63
+ self.base_url = self.get_base_url(Services.USER_REPORTING.value)
64
+ self.name = Services.USER_REPORTING.value
65
+ self.urls = {
66
+ "instructions": "reporting/instructions/",
67
+ "portfolio_performance": "reporting/user/%s/userportfolio/%s/performance/",
68
+ "portfolio_performance_breakdown": "reporting/user/%s/userportfolio/%s/performance/breakdown/"
69
+ }
70
+
71
+ def _get_integer_quantity(self, qty, side):
72
+ """
73
+ Returns quantity as a negative value if side is 'sell', otherwise returns it as-is.
74
+
75
+ :param qty: Quantity of the asset
76
+ :param side: Trade side, either 'buy' or 'sell'
77
+ :return: Signed quantity based on trade side
78
+ """
79
+ quantity = -1 * qty if side == self.SELL else qty
80
+ return quantity
81
+
82
+ def _build_instructions_data(self, request_data):
83
+ """
84
+ Builds the payload for submitting trade instructions.
85
+
86
+ :param request_data: Request payload containing metadata and trade details
87
+ :return: Formatted instruction payload as a dictionary
88
+ """
89
+ meta_data = request_data.get('metadata')
90
+ product_type = request_data.get('product_type')
91
+ instructions = {
92
+ "user_id": meta_data.get("user_id"),
93
+ "user_portfolio_id": meta_data.get("basket_id") if product_type == self.MTF
94
+ else meta_data.get('user_portfolio_id'),
95
+ "instruction_id": meta_data.get("instruction_id"),
96
+ "symbol": request_data.get("symbol"),
97
+ "qty": self._get_integer_quantity(qty=request_data.get("quantity"),
98
+ side=request_data.get("side")),
99
+ "execution_price": request_data.get("price"),
100
+ "date": meta_data.get("date"),
101
+ "product": product_type if product_type == self.MTF else self.EQUITY
102
+ }
103
+ instruction_data = {
104
+ "instructions": [
105
+ instructions
106
+ ]
107
+ }
108
+ return instruction_data
109
+
110
+ def add_instructions(self, request_data):
111
+ """
112
+ Sends trade instructions to the User Reporting service.
113
+
114
+ :param request_data: Request payload containing instruction metadata and trade details
115
+ """
116
+ logger.info(f"In - add_instructions {request_data =}")
117
+ instruction_data = self._build_instructions_data(request_data)
118
+ user_instructions_response = self._post(url=self.base_url,
119
+ endpoint=self.urls.get("instructions"),
120
+ data=json.dumps(instruction_data))
121
+ logger.info(f"{user_instructions_response =}")
122
+
123
+ def get_portfolio_performance(self, user_id, user_portfolio_id, product='equity'):
124
+ """
125
+ Retrieves portfolio performance data for a specific user and portfolio.
126
+
127
+ :param user_id: ID of the user
128
+ :param user_portfolio_id: ID of the user portfolio
129
+ :param product: Product type (default: 'equity')
130
+ :return: Performance data from the response
131
+ """
132
+ endpoint = self.urls.get('portfolio_performance') % (user_id, user_portfolio_id)
133
+ data = self._get(url=self.base_url,
134
+ endpoint=endpoint,
135
+ params={
136
+ "product": product
137
+ })
138
+ logger.info(f"{data =}")
139
+ return data.get("data")
140
+
141
+ def get_portfolio_performance_breakdown(self, user_id, user_portfolio_id):
142
+ """
143
+ Retrieves a detailed breakdown of portfolio performance for a specific user and portfolio.
144
+
145
+ :param user_id: ID of the user
146
+ :param user_portfolio_id: ID of the user portfolio
147
+ :return: Performance breakdown data from the response
148
+ """
149
+ endpoint = self.urls.get('portfolio_performance') % (user_id, user_portfolio_id)
150
+ data = self._get(url=self.base_url,
151
+ endpoint=endpoint)
152
+ logger.info(f"{data =}")
153
+ return data.get("data")