bw-essentials-core 0.1.5__py3-none-any.whl → 0.1.33__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.
- bw_essentials/constants/services.py +12 -0
- bw_essentials/email_client/email_client.py +31 -18
- bw_essentials/services/api_client.py +49 -0
- bw_essentials/services/broker.py +30 -4
- bw_essentials/services/compliance.py +78 -0
- bw_essentials/services/job_scheduler.py +62 -0
- bw_essentials/services/market_pricer.py +71 -17
- bw_essentials/services/master_data.py +49 -0
- bw_essentials/services/model_portfolio_reporting.py +30 -1
- bw_essentials/services/notification.py +19 -15
- bw_essentials/services/payment.py +84 -0
- bw_essentials/services/portfolio_catalogue.py +169 -1
- bw_essentials/services/portfolio_content.py +24 -0
- bw_essentials/services/trade_placement.py +262 -9
- bw_essentials/services/user_app.py +23 -4
- bw_essentials/services/user_portfolio.py +562 -1
- bw_essentials/services/user_portfolio_reporting.py +37 -1
- {bw_essentials_core-0.1.5.dist-info → bw_essentials_core-0.1.33.dist-info}/METADATA +1 -1
- bw_essentials_core-0.1.33.dist-info/RECORD +32 -0
- bw_essentials_core-0.1.5.dist-info/RECORD +0 -29
- {bw_essentials_core-0.1.5.dist-info → bw_essentials_core-0.1.33.dist-info}/WHEEL +0 -0
- {bw_essentials_core-0.1.5.dist-info → bw_essentials_core-0.1.33.dist-info}/top_level.txt +0 -0
|
@@ -20,3 +20,15 @@ class Services(Enum):
|
|
|
20
20
|
PROMETHEUS_USER_APP = "Prometheus_User_App"
|
|
21
21
|
PORTFOLIO_CATALOGUE = "Portfolio_Catalogue"
|
|
22
22
|
PORTFOLIO_CONTENT = "Portfolio_Content"
|
|
23
|
+
JOB_SCHEDULER = "Job_Scheduler"
|
|
24
|
+
COMPLIANCE = "Compliance"
|
|
25
|
+
|
|
26
|
+
class PortfolioStatus(Enum):
|
|
27
|
+
"""
|
|
28
|
+
Enum representing Status of Basket.
|
|
29
|
+
"""
|
|
30
|
+
ACTIVE = "active"
|
|
31
|
+
INACTIVE = "inactive"
|
|
32
|
+
PAUSED = "paused"
|
|
33
|
+
|
|
34
|
+
ACTIVE_FOR_SUBSCRIBED_USER = (ACTIVE, PAUSED)
|
|
@@ -132,7 +132,7 @@ class EmailClient:
|
|
|
132
132
|
cc_addresses: Union[str, List[str], None],
|
|
133
133
|
subject: str,
|
|
134
134
|
body: str,
|
|
135
|
-
attachment_path: str,
|
|
135
|
+
attachment_path: Optional[Union[str, List[str]]],
|
|
136
136
|
is_html: bool = True,
|
|
137
137
|
):
|
|
138
138
|
"""
|
|
@@ -143,7 +143,7 @@ class EmailClient:
|
|
|
143
143
|
cc_addresses (Union[str, List[str], None]): One or more CC addresses (optional).
|
|
144
144
|
subject (str): Email subject line.
|
|
145
145
|
body (str): Email body content.
|
|
146
|
-
attachment_path (str):
|
|
146
|
+
attachment_path (Optional[Union[str, List[str]]]): File path(s) for attachments.
|
|
147
147
|
is_html (bool): If True, body is interpreted as HTML; otherwise, plain text.
|
|
148
148
|
"""
|
|
149
149
|
logger.info(
|
|
@@ -158,7 +158,7 @@ class EmailClient:
|
|
|
158
158
|
cc_addresses: Union[str, List[str], None],
|
|
159
159
|
subject: str,
|
|
160
160
|
body: str,
|
|
161
|
-
attachment_path: Optional[str],
|
|
161
|
+
attachment_path: Optional[Union[str, List[str]]],
|
|
162
162
|
is_html: bool,
|
|
163
163
|
):
|
|
164
164
|
"""
|
|
@@ -169,7 +169,7 @@ class EmailClient:
|
|
|
169
169
|
cc_addresses (Union[str, List[str], None]): CC addresses.
|
|
170
170
|
subject (str): Subject of the email.
|
|
171
171
|
body (str): Email body content.
|
|
172
|
-
attachment_path (Optional[str]): File path for
|
|
172
|
+
attachment_path (Optional[Union[str, List[str]]]): File path(s) for attachments.
|
|
173
173
|
is_html (bool): True if the body is HTML-formatted.
|
|
174
174
|
"""
|
|
175
175
|
msg = MIMEMultipart()
|
|
@@ -180,23 +180,36 @@ class EmailClient:
|
|
|
180
180
|
|
|
181
181
|
msg.attach(MIMEText(body, 'html' if is_html else 'plain'))
|
|
182
182
|
logger.debug("Email headers and body constructed")
|
|
183
|
+
attachment_paths = []
|
|
183
184
|
|
|
184
185
|
if attachment_path:
|
|
185
|
-
if
|
|
186
|
-
|
|
186
|
+
if isinstance(attachment_path, list):
|
|
187
|
+
attachment_paths = attachment_path
|
|
187
188
|
else:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
189
|
+
attachment_paths = [attachment_path]
|
|
190
|
+
|
|
191
|
+
for path in attachment_paths:
|
|
192
|
+
if not path:
|
|
193
|
+
continue
|
|
194
|
+
if not os.path.exists(path):
|
|
195
|
+
logger.warning("Attachment file not found: %s", path)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
with open(path, 'rb') as attachment:
|
|
200
|
+
part = MIMEBase('application', 'octet-stream')
|
|
201
|
+
part.set_payload(attachment.read())
|
|
202
|
+
encoders.encode_base64(part)
|
|
203
|
+
part.add_header(
|
|
204
|
+
'Content-Disposition',
|
|
205
|
+
'attachment',
|
|
206
|
+
filename=os.path.basename(path)
|
|
207
|
+
)
|
|
208
|
+
msg.attach(part)
|
|
209
|
+
logger.info("Attachment added: %s", os.path.basename(path))
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.exception("Failed to read attachment file: %s", path)
|
|
212
|
+
raise e
|
|
200
213
|
|
|
201
214
|
try:
|
|
202
215
|
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
|
|
@@ -15,6 +15,7 @@ import logging
|
|
|
15
15
|
import os
|
|
16
16
|
import sys
|
|
17
17
|
import time
|
|
18
|
+
import httpx
|
|
18
19
|
from importlib.util import spec_from_file_location, module_from_spec
|
|
19
20
|
from typing import Optional, Dict, Any
|
|
20
21
|
|
|
@@ -80,6 +81,19 @@ class ApiClient:
|
|
|
80
81
|
env_key = f"{service_name.upper()}_BASE_URL"
|
|
81
82
|
return self._get_env_var(env_key)
|
|
82
83
|
|
|
84
|
+
def get_api_key(self, service_name: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Resolve the service API Key for a given service name using environment variables.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
service_name (str): The logical name of the service.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
str: The resolved api key from the environment.
|
|
93
|
+
"""
|
|
94
|
+
env_key = f"{service_name.upper()}_API_KEY"
|
|
95
|
+
return self._get_env_var(env_key)
|
|
96
|
+
|
|
83
97
|
def set_tenant_id(self, tenant_id: str) -> None:
|
|
84
98
|
"""
|
|
85
99
|
Set the tenant ID in the request headers.
|
|
@@ -182,6 +196,41 @@ class ApiClient:
|
|
|
182
196
|
"""
|
|
183
197
|
return self._request("get", url, endpoint, params=params)
|
|
184
198
|
|
|
199
|
+
async def _async_get(self, url: str, endpoint: str, headers: dict | None = None, params: dict | None = None):
|
|
200
|
+
"""
|
|
201
|
+
Async GET request, aligned with the sync _request() style.
|
|
202
|
+
"""
|
|
203
|
+
headers = self._update_headers(headers or {})
|
|
204
|
+
params = params or {}
|
|
205
|
+
formatted_url = f"{url.rstrip('/')}/{endpoint.lstrip('/')}"
|
|
206
|
+
|
|
207
|
+
logger.info(f"GET {formatted_url} | Headers: {headers} | Params: {params}")
|
|
208
|
+
|
|
209
|
+
start = time.time()
|
|
210
|
+
try:
|
|
211
|
+
async with httpx.AsyncClient() as client:
|
|
212
|
+
response = await client.get(formatted_url, headers=headers, params=params)
|
|
213
|
+
|
|
214
|
+
elapsed_time_ms = (time.time() - start) * 1000
|
|
215
|
+
|
|
216
|
+
# parse JSON safely
|
|
217
|
+
try:
|
|
218
|
+
json_data = response.json()
|
|
219
|
+
except ValueError:
|
|
220
|
+
logger.error(f"Non-JSON response from {formatted_url}")
|
|
221
|
+
json_data = None
|
|
222
|
+
|
|
223
|
+
self._log_response("GET", formatted_url, response.status_code, elapsed_time_ms, json_data)
|
|
224
|
+
|
|
225
|
+
response.raise_for_status()
|
|
226
|
+
return json_data
|
|
227
|
+
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
elapsed_time_ms = (time.time() - start) * 1000
|
|
230
|
+
logger.error(f"GET {formatted_url} failed after {elapsed_time_ms:.2f}ms")
|
|
231
|
+
logger.exception(exc)
|
|
232
|
+
raise
|
|
233
|
+
|
|
185
234
|
def _post(
|
|
186
235
|
self,
|
|
187
236
|
url: str,
|
bw_essentials/services/broker.py
CHANGED
|
@@ -30,6 +30,7 @@ APIs supported:
|
|
|
30
30
|
- Acknowledge surveillance orders
|
|
31
31
|
"""
|
|
32
32
|
import logging
|
|
33
|
+
from typing import Optional
|
|
33
34
|
|
|
34
35
|
from bw_essentials.constants.services import Services
|
|
35
36
|
from bw_essentials.services.api_client import ApiClient
|
|
@@ -62,7 +63,8 @@ class Broker(ApiClient):
|
|
|
62
63
|
"authorised_holdings": "brokers/{}/holdings/authorise",
|
|
63
64
|
"user_instructions": "brokers/{}/user/instructions",
|
|
64
65
|
"details": "brokers/{}/trade/details",
|
|
65
|
-
"surveillance_orders": "brokers/{}/surveillance/orders"
|
|
66
|
+
"surveillance_orders": "brokers/{}/surveillance/orders",
|
|
67
|
+
"profile": "brokers/{}/profile"
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
def authenticate(self, broker_name: str, user_id: str, entity_id: str,
|
|
@@ -118,22 +120,46 @@ class Broker(ApiClient):
|
|
|
118
120
|
logger.info(f"{response =}")
|
|
119
121
|
return response["data"]["balance"]
|
|
120
122
|
|
|
121
|
-
def
|
|
123
|
+
def get_profile(self, broker_name: str, user_id: str) -> dict:
|
|
124
|
+
"""
|
|
125
|
+
Fetch user profile from the broker service.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
broker_name (str): Broker name.
|
|
129
|
+
user_id (str): User ID.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dict: User Profile.
|
|
133
|
+
"""
|
|
134
|
+
logger.info(f"In - get_profile {user_id =}")
|
|
135
|
+
response = self._get(
|
|
136
|
+
url=self.base_url,
|
|
137
|
+
endpoint=self.urls["profile"].format(broker_name),
|
|
138
|
+
params={"user_id": user_id}
|
|
139
|
+
)
|
|
140
|
+
logger.info(f"{response =}")
|
|
141
|
+
return response["data"]
|
|
142
|
+
|
|
143
|
+
def get_holdings(self, broker_name: str, user_id: str, product_type: Optional[str] = None) -> dict:
|
|
122
144
|
"""
|
|
123
145
|
Fetch user holdings from the broker service.
|
|
124
146
|
|
|
125
147
|
Args:
|
|
126
148
|
broker_name (str): Broker name.
|
|
127
149
|
user_id (str): User ID.
|
|
150
|
+
product_type (str, optional): Product type.
|
|
128
151
|
|
|
129
152
|
Returns:
|
|
130
|
-
|
|
153
|
+
dict: Dict of user holdings.
|
|
131
154
|
"""
|
|
132
155
|
logger.info(f"In - get_holdings {user_id =}")
|
|
156
|
+
params = {"user_id": user_id}
|
|
157
|
+
if product_type:
|
|
158
|
+
params['product_type'] = product_type
|
|
133
159
|
response = self._get(
|
|
134
160
|
url=self.base_url,
|
|
135
161
|
endpoint=self.urls["holdings"].format(broker_name),
|
|
136
|
-
params=
|
|
162
|
+
params=params
|
|
137
163
|
)
|
|
138
164
|
logger.info(f"{response =}")
|
|
139
165
|
return response["data"]["holdings"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from bw_essentials.constants.services import Services
|
|
3
|
+
from bw_essentials.services.api_client import ApiClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Compliance(ApiClient):
|
|
10
|
+
"""
|
|
11
|
+
Compliance service integration class.
|
|
12
|
+
|
|
13
|
+
This class facilitates communication with the compliance service.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, user, tenant_id: str = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the Compliance service wrapper.
|
|
19
|
+
|
|
20
|
+
:param user: The user initiating the request.
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(user)
|
|
23
|
+
self.base_url = self.get_base_url(Services.COMPLIANCE.value)
|
|
24
|
+
self.name = Services.COMPLIANCE.value
|
|
25
|
+
self.tenant_id = tenant_id
|
|
26
|
+
self.urls = {
|
|
27
|
+
"kra_data": "kra/data"
|
|
28
|
+
}
|
|
29
|
+
logger.info("Compliance service initialized.")
|
|
30
|
+
|
|
31
|
+
def _headers(self):
|
|
32
|
+
"""
|
|
33
|
+
Construct headers for compliance service requests.
|
|
34
|
+
|
|
35
|
+
:return: Dictionary of headers.
|
|
36
|
+
"""
|
|
37
|
+
return {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
**({'X-Tenant-ID': self.tenant_id} if self.tenant_id else {})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def get_kra_data(self, payload):
|
|
43
|
+
"""
|
|
44
|
+
Fetches KRA details by calling API.
|
|
45
|
+
|
|
46
|
+
This method prepares the request headers, constructs the API endpoint,
|
|
47
|
+
sends a POST request with the provided payload, and returns the
|
|
48
|
+
parsed KRA data received from the compliance service.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
payload (dict):
|
|
53
|
+
Dictionary containing the required fields for KRA lookup.
|
|
54
|
+
Example:
|
|
55
|
+
{
|
|
56
|
+
"pan": "ABCDE1234F",
|
|
57
|
+
"dob": "1985-05-12"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
dict:
|
|
62
|
+
Parsed KRA data dictionary returned by the Compliance Service.
|
|
63
|
+
Returns an empty dictionary if `data` is missing from the response.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
{
|
|
67
|
+
"kra_status": "KYC_REGISTERED",
|
|
68
|
+
"name": "John Doe",
|
|
69
|
+
"dob": "1985-05-12",
|
|
70
|
+
...
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
logger.info(f"In get_kra_data {payload =}")
|
|
74
|
+
url = f"{self.base_url}"
|
|
75
|
+
self.headers = self._headers()
|
|
76
|
+
kra_data = self._post(url=url, endpoint=self.urls.get('kra_data'), json=payload)
|
|
77
|
+
logger.info(f"Out get_kra_data {kra_data =}")
|
|
78
|
+
return kra_data.get('data', {})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from bw_essentials.constants.services import Services
|
|
3
|
+
from bw_essentials.services.api_client import ApiClient
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JobScheduler(ApiClient):
|
|
9
|
+
"""
|
|
10
|
+
Class for making API calls to the Job Scheduler Service.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
user (str): The user for whom the API calls are being made.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, user, tenant_id: str = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the Job Scheduler object.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
user (str): The user for whom the API calls are being made.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(user)
|
|
24
|
+
self.base_url = self.get_base_url(Services.JOB_SCHEDULER.value)
|
|
25
|
+
self.api_key = self.get_api_key(Services.JOB_SCHEDULER.value)
|
|
26
|
+
self.name = Services.JOB_SCHEDULER.value
|
|
27
|
+
self.tenant_id = tenant_id
|
|
28
|
+
self.urls = {
|
|
29
|
+
"process_user_profile": "taskmanager/process/user-portfolios/"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def _headers(self):
|
|
33
|
+
"""
|
|
34
|
+
Prepares headers for API calls.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: Headers
|
|
38
|
+
"""
|
|
39
|
+
self.headers.update({
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
**({'X-Tenant-ID': self.tenant_id} if self.tenant_id else {}),
|
|
42
|
+
'x-api-key': self.api_key
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
def process_user_profile(self, data):
|
|
46
|
+
"""
|
|
47
|
+
Send a request to process a user profile.
|
|
48
|
+
|
|
49
|
+
This method prepares the required headers and sends a POST request
|
|
50
|
+
to the `process_user_profile` endpoint with the provided user data.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
data (dict): A dictionary containing user profile information
|
|
54
|
+
to be sent in the request body.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
None: The method does not return a value.
|
|
58
|
+
"""
|
|
59
|
+
logger.info(f"In - process_user_profile {self.user =}")
|
|
60
|
+
self._headers()
|
|
61
|
+
self._post(url=self.base_url, endpoint=self.urls.get('process_user_profile'), json=data)
|
|
62
|
+
logger.info(f"Out - process_user_profile")
|
|
@@ -76,14 +76,14 @@ class MarketPricer(ApiClient):
|
|
|
76
76
|
Retrieves live market prices for a list of securities on a specific exchange.
|
|
77
77
|
|
|
78
78
|
Args:
|
|
79
|
-
securities (
|
|
79
|
+
securities (list): List of security symbols for which live prices are requested.
|
|
80
80
|
exchange (str): The exchange on which the securities are traded.
|
|
81
81
|
Returns:
|
|
82
|
-
|
|
82
|
+
dict: A dictionary containing the live market price data for the specified securities.
|
|
83
83
|
|
|
84
84
|
Example:
|
|
85
85
|
market_pricer = MarketPricer(user)
|
|
86
|
-
securities = "TCS,RELIANCE"
|
|
86
|
+
securities = ["TCS", "RELIANCE"]
|
|
87
87
|
exchange = "NSE"
|
|
88
88
|
live_prices = market_pricer.get_live_prices(securities, exchange)
|
|
89
89
|
|
|
@@ -96,20 +96,21 @@ class MarketPricer(ApiClient):
|
|
|
96
96
|
|
|
97
97
|
API Response:
|
|
98
98
|
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
"data": {
|
|
100
|
+
"TCS": {
|
|
101
|
+
"security": "TCS",
|
|
102
|
+
"exchange": "NSE",
|
|
103
|
+
"price": 3207.8,
|
|
104
|
+
"timestamp": "2026-01-09 16:00:00"
|
|
105
|
+
},
|
|
106
|
+
"RELIANCE": {
|
|
107
|
+
"security": "RELIANCE",
|
|
108
|
+
"exchange": "NSE",
|
|
109
|
+
"price": 1475.3,
|
|
110
|
+
"timestamp": "2026-01-09 16:00:00"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"success": true
|
|
113
114
|
}
|
|
114
115
|
"""
|
|
115
116
|
logger.info(f"In - get_live_prices {securities =}, {exchange =}")
|
|
@@ -300,3 +301,56 @@ class MarketPricer(ApiClient):
|
|
|
300
301
|
data = response.get("data")
|
|
301
302
|
logger.info("Out - get_index_performance_by_dates success")
|
|
302
303
|
return data
|
|
304
|
+
|
|
305
|
+
def get_bulk_option_eod_prices(
|
|
306
|
+
self,
|
|
307
|
+
symbols,
|
|
308
|
+
from_date,
|
|
309
|
+
to_date,
|
|
310
|
+
exchange=None,
|
|
311
|
+
underlying_symbol=None,
|
|
312
|
+
option_type=None,
|
|
313
|
+
expiry_date=None,
|
|
314
|
+
):
|
|
315
|
+
"""
|
|
316
|
+
Fetch bulk EOD OHLCV data for multiple option symbols over a date range.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
symbols (list[str] | str): Option symbols as list or comma-separated string.
|
|
320
|
+
from_date (str): Start date in YYYY-MM-DD format.
|
|
321
|
+
to_date (str): End date in YYYY-MM-DD format.
|
|
322
|
+
exchange (str, optional): Exchange filter.
|
|
323
|
+
underlying_symbol (str, optional): Underlying instrument filter.
|
|
324
|
+
option_type (str, optional): CE or PE.
|
|
325
|
+
expiry_date (str, optional): Option expiry date.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
dict: Dictionary keyed by symbol containing EOD data.
|
|
329
|
+
"""
|
|
330
|
+
logger.info(f"In - get_bulk_option_eod_prices | {symbols=}, {from_date=}, {to_date=}")
|
|
331
|
+
|
|
332
|
+
if not all([symbols, from_date, to_date]):
|
|
333
|
+
raise ValueError("symbols, from_date and to_date are required")
|
|
334
|
+
|
|
335
|
+
symbols = ",".join(symbols) if isinstance(symbols, list) else symbols
|
|
336
|
+
|
|
337
|
+
params = {"symbols": symbols, "from_date": from_date, "to_date": to_date}
|
|
338
|
+
|
|
339
|
+
for key, value in {
|
|
340
|
+
"exchange": exchange,
|
|
341
|
+
"underlying_symbol": underlying_symbol,
|
|
342
|
+
"option_type": option_type,
|
|
343
|
+
"expiry_date": expiry_date,
|
|
344
|
+
}.items():
|
|
345
|
+
if value is not None:
|
|
346
|
+
params[key] = value
|
|
347
|
+
|
|
348
|
+
response = self._get(
|
|
349
|
+
url=self.base_url,
|
|
350
|
+
endpoint="options/bulk/eod",
|
|
351
|
+
params=params,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
logger.info(f"Out - get_bulk_option_eod_prices | symbols_count={len(symbols.split(','))}")
|
|
355
|
+
return response.get("data")
|
|
356
|
+
|
|
@@ -299,3 +299,52 @@ class MasterData(ApiClient):
|
|
|
299
299
|
response = self._get(url=self.base_url, endpoint=self.urls["broker_partner_mapping_details"].format(broker_partner, broker))
|
|
300
300
|
logger.info(f"Broker partner mapping {response =}")
|
|
301
301
|
return response.get('data', {})
|
|
302
|
+
|
|
303
|
+
async def get_broker_data_async(self, symbols, broker, username=None):
|
|
304
|
+
"""
|
|
305
|
+
Asynchronously retrieve broker-specific data for specified symbols.
|
|
306
|
+
|
|
307
|
+
This function fetches broker-specific details such as margin and leverage
|
|
308
|
+
for a list of provided trading symbols from the broker master service.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
username (str) [Optional]: The user_id for whom data is requested.
|
|
312
|
+
symbols (str): A comma-separated string of trading symbols.
|
|
313
|
+
broker (str): The broker identifier for which data is requested.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
list: A list of dictionaries containing broker data for each symbol
|
|
317
|
+
or an empty list if no data is available.
|
|
318
|
+
"""
|
|
319
|
+
logger.info(f"In - get_broker_data_async {broker =}")
|
|
320
|
+
broker_data_response = await self._async_get(
|
|
321
|
+
url=f"{self.base_url}",
|
|
322
|
+
endpoint = self.urls["broker_details"].format(broker),
|
|
323
|
+
params={"symbols": symbols, "user_id": username},
|
|
324
|
+
headers={}
|
|
325
|
+
)
|
|
326
|
+
return broker_data_response.get('data', [])
|
|
327
|
+
|
|
328
|
+
def get_broker_data_all(self, symbols, broker, username=None):
|
|
329
|
+
"""
|
|
330
|
+
Retrieve broker-specific data for specified symbols.
|
|
331
|
+
|
|
332
|
+
This function fetches broker-specific details such as margin and leverage
|
|
333
|
+
for a list of provided trading symbols from the broker master service.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
username (str) [Optional]: The user_id for whom data is requested.
|
|
337
|
+
symbols (str): A comma-separated string of trading symbols.
|
|
338
|
+
broker (str): The broker identifier for which data is requested.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
list: A dictionary containing broker data for the first symbol or
|
|
342
|
+
an empty dictionary if no data is available.
|
|
343
|
+
"""
|
|
344
|
+
logger.info(f"In - get_broker_data {broker =}")
|
|
345
|
+
broker_data_response = self._get(
|
|
346
|
+
url=f"{self.base_url}",
|
|
347
|
+
endpoint=self.urls['broker_details'].format(broker),
|
|
348
|
+
params={"symbols": symbols, "user_id": username},
|
|
349
|
+
)
|
|
350
|
+
return broker_data_response.get('data', [])
|
|
@@ -33,7 +33,8 @@ class ModelPortfolioReporting(ApiClient):
|
|
|
33
33
|
self.object = None
|
|
34
34
|
self.urls = {
|
|
35
35
|
"portfolio": "portfolio",
|
|
36
|
-
"portfolio_performance": "portfolio/%s/performance"
|
|
36
|
+
"portfolio_performance": "portfolio/%s/performance",
|
|
37
|
+
"index_performance": "index/%s/performance/"
|
|
37
38
|
}
|
|
38
39
|
self.name = Services.MODEL_PORTFOLIO.value
|
|
39
40
|
self.base_url = self.get_base_url(Services.MODEL_PORTFOLIO.value)
|
|
@@ -79,3 +80,31 @@ class ModelPortfolioReporting(ApiClient):
|
|
|
79
80
|
)
|
|
80
81
|
logger.info(f"Received response from get_portfolio_performance_by_dates: {performance}")
|
|
81
82
|
return performance.get("data")
|
|
83
|
+
|
|
84
|
+
def get_index_performance_by_dates(
|
|
85
|
+
self,
|
|
86
|
+
index: str,
|
|
87
|
+
start_date: str,
|
|
88
|
+
end_date: str
|
|
89
|
+
) -> Optional[Dict[str, Any]]:
|
|
90
|
+
"""
|
|
91
|
+
Fetch Index performance for a given date range.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
index (str): The index identifier.
|
|
95
|
+
start_date (str): The start date for performance metrics (format: YYYY-MM-DD).
|
|
96
|
+
end_date (str): The end date for performance metrics (format: YYYY-MM-DD).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
dict | None: The performance data within the date range.
|
|
100
|
+
"""
|
|
101
|
+
logger.info(f"In - get_index_performance_by_dates with index={index}, start_date={start_date}, end_date={end_date}")
|
|
102
|
+
endpoint = self.urls['index_performance'] % index
|
|
103
|
+
params = {"start_date": start_date, "end_date": end_date}
|
|
104
|
+
performance = self._get(
|
|
105
|
+
url=self.base_url,
|
|
106
|
+
endpoint=endpoint,
|
|
107
|
+
params=params
|
|
108
|
+
)
|
|
109
|
+
logger.info(f"Received response from get_index_performance_by_dates: {performance}")
|
|
110
|
+
return performance.get("data")
|