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.
- bw_essentials/__init__.py +0 -0
- bw_essentials/constants/__init__.py +0 -0
- bw_essentials/constants/services.py +19 -0
- bw_essentials/data_loch/__init__.py +0 -0
- bw_essentials/data_loch/data_loch.py +282 -0
- bw_essentials/email_client/__init__.py +0 -0
- bw_essentials/email_client/email_client.py +243 -0
- bw_essentials/notifications/__init__.py +0 -0
- bw_essentials/notifications/teams_notification_schemas.py +173 -0
- bw_essentials/notifications/teams_notifications.py +189 -0
- bw_essentials/s3_utils/__init__.py +0 -0
- bw_essentials/s3_utils/s3_utils.py +361 -0
- bw_essentials/services/__init__.py +0 -0
- bw_essentials/services/api_client.py +229 -0
- bw_essentials/services/broker.py +250 -0
- bw_essentials/services/market_pricer.py +233 -0
- bw_essentials/services/master_data.py +257 -0
- bw_essentials/services/model_portfolio_reporting.py +81 -0
- bw_essentials/services/trade_placement.py +499 -0
- bw_essentials/services/user_portfolio.py +406 -0
- bw_essentials/services/user_portfolio_reporting.py +153 -0
- bw_essentials_core-0.0.1.dist-info/METADATA +213 -0
- bw_essentials_core-0.0.1.dist-info/RECORD +25 -0
- bw_essentials_core-0.0.1.dist-info/WHEEL +5 -0
- bw_essentials_core-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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")
|