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,257 @@
1
+ """
2
+ Module to make structured API calls to the Master Data Service.
3
+
4
+ This module extends the generic ApiClient to provide typed, reusable
5
+ endpoints for accessing Master Data APIs like holidays, security details,
6
+ broker configurations, and constants.
7
+ """
8
+
9
+ import logging
10
+ from datetime import datetime
11
+ from typing import Optional, List, Dict, Any
12
+
13
+ import pytz
14
+
15
+ from bw_essentials.constants.services import Services
16
+ from bw_essentials.services.api_client import ApiClient
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class MasterData(ApiClient):
22
+ """
23
+ API wrapper for Master Data Service.
24
+
25
+ Inherits from ApiClient and provides domain-specific methods to fetch
26
+ holidays, constants, company details, and broker configurations.
27
+
28
+ Args:
29
+ service_user (str): The user initiating the request (e.g., system/username).
30
+ """
31
+ TEST = 'test'
32
+ SATURDAY = 'Saturday'
33
+ SUNDAY = 'Sunday'
34
+
35
+ def __init__(self, service_user: str):
36
+ logger.info(f"Initializing MasterData client for user: {service_user}")
37
+ super().__init__(user=service_user)
38
+ self.base_url = self.get_base_url(Services.MASTER_DATA.value)
39
+ self.name = Services.MASTER_DATA.value
40
+ self.urls = {
41
+ "holidays": "holidays",
42
+ "securities": "securities",
43
+ "details": "company/details",
44
+ "constant": "constants",
45
+ "broker_config": "broker/config/keys",
46
+ "broker_details": "securities/{}/details",
47
+ "isin_details": "company/details/isin"
48
+ }
49
+
50
+ def get_security_details(self, securities: List[str]) -> Optional[Dict[str, Any]]:
51
+ """
52
+ Fetches company security details by symbols.
53
+
54
+ Args:
55
+ securities (List[str]): List of security symbols.
56
+ Returns:
57
+ Optional[Dict[str, Any]]: Security detail data or None.
58
+ """
59
+ logger.info(f"Fetching security details for: {securities}")
60
+ securities = ','.join(securities)
61
+ response = self._get(url=self.base_url,
62
+ endpoint=self.urls["details"],
63
+ params={"symbols": securities})
64
+ return response.get("data")
65
+
66
+ def get_constants_data(self, key: str) -> Optional[Dict[str, Any]]:
67
+ """
68
+ Fetches constants from master data using a key.
69
+
70
+ Args:
71
+ key (str): Constant key to query.
72
+
73
+ Returns:
74
+ Optional[Dict[str, Any]]: Constant values or None.
75
+ """
76
+ logger.info(f"Fetching constants data for key: {key}")
77
+ response = self._get(url=self.base_url,
78
+ endpoint=self.urls["constant"],
79
+ params={"key": key})
80
+ return response.get("data")
81
+
82
+ def get_trading_holidays(self, year: int) -> Optional[List[Dict[str, Any]]]:
83
+ """
84
+ Gets trading holidays for a specific year.
85
+
86
+ Args:
87
+ year (int): Year for which holidays are to be fetched.
88
+ Returns:
89
+ Optional[List[Dict[str, Any]]]: List of holiday data.
90
+ """
91
+ logger.info(f"Fetching trading holidays for year: {year}")
92
+ response = self._get(url=self.base_url,
93
+ endpoint=self.urls["holidays"],
94
+ params={"year": year})
95
+ return response.get("data")
96
+
97
+ def get_broker_config_keys(self, broker: str,
98
+ product_type: Optional[str] = None,
99
+ category: Optional[str] = None) -> Optional[Dict[str, Any]]:
100
+ """
101
+ Retrieves broker configuration keys based on filters.
102
+
103
+ Args:
104
+ broker (str): Broker name.
105
+ product_type (Optional[str]): Type of product (e.g., equity, MTF).
106
+ category (Optional[str]): Optional configuration category.
107
+ Returns:
108
+ Optional[Dict[str, Any]]: Configuration key data or None.
109
+ """
110
+ logger.info(f"Fetching broker config keys for broker={broker}, product_type={product_type}, category={category}")
111
+ params = {k: v for k, v in {'broker': broker, 'product': product_type, 'category': category}.items() if v}
112
+ response = self._get(url=self.base_url,
113
+ endpoint=self.urls["broker_config"],
114
+ params=params)
115
+ return response.get("data")
116
+
117
+ def get_broker_security_details(self, securities: List[str], broker: str) -> Dict[str, Any]:
118
+ """
119
+ Retrieves broker-specific security details.
120
+
121
+ Args:
122
+ securities (List[str]): List of security symbols.
123
+ broker (str): Broker name.
124
+
125
+ Returns:
126
+ Dict[str, Any]: Security detail data.
127
+ """
128
+ logger.info(f"Fetching broker security details for securities={securities}, broker={broker}")
129
+ endpoint = self.urls["broker_details"].format(broker)
130
+ params = {"symbols": securities}
131
+ response = self._get(url=self.base_url,
132
+ endpoint=endpoint,
133
+ params=params)
134
+ return response.get("data", {})
135
+
136
+ def get_company_details(self, isin_data: List[str]) -> Dict[str, Any]:
137
+ """
138
+ Fetches company details using ISIN codes.
139
+
140
+ Args:
141
+ isin_data (List[str]): List of ISIN codes.
142
+
143
+ Returns:
144
+ Dict[str, Any]: Company detail data.
145
+ """
146
+ logger.info(f"Fetching company details for ISINs: {isin_data}")
147
+ response = self._get(url=self.base_url,
148
+ endpoint=self.urls["isin_details"],
149
+ params={"isin": isin_data})
150
+ return response.get("data", {})
151
+
152
+ def get_active_securities(self) -> List[Dict[str, Any]]:
153
+ """
154
+ Retrieves all available active securities.
155
+
156
+ Returns:
157
+ List[Dict[str, Any]]: List of securities.
158
+ """
159
+ logger.info("Fetching list of all securities")
160
+ response = self._get(url=self.base_url,
161
+ endpoint=self.urls["securities"])
162
+ return response.get("data", [])
163
+
164
+ def is_trading_hours(self, open_time: str, close_time: str, timezone: str) -> bool:
165
+ """
166
+ Determines if the current time is within the specified trading hours.
167
+
168
+ Args:
169
+ open_time (str): Trading start time in HH:MM format (e.g., "09:15").
170
+ close_time (str): Trading end time in HH:MM format (e.g., "15:30").
171
+ timezone (str): Timezone string (e.g., "Asia/Kolkata").
172
+
173
+ Returns:
174
+ bool: True if current time is within trading hours, False otherwise.
175
+ """
176
+ logger.info(f"In - is_trading_hours | open_time={open_time}, close_time={close_time}, timezone={timezone}")
177
+
178
+ current_time = datetime.now(pytz.timezone(timezone))
179
+ logger.info(f"Current time in {timezone} = {current_time}")
180
+
181
+ open_time_obj = datetime.strptime(open_time, "%H:%M").time()
182
+ close_time_obj = datetime.strptime(close_time, "%H:%M").time()
183
+
184
+ if open_time_obj <= current_time.time() <= close_time_obj:
185
+ logger.info("Market is open for trading.")
186
+ return True
187
+
188
+ logger.info("Market is closed for trading.")
189
+ return False
190
+
191
+ def is_trading_holiday(self, current_date: datetime.date) -> bool:
192
+ """
193
+ Determines whether the given date is a trading holiday or weekend.
194
+
195
+ Args:
196
+ current_date (datetime.date): The date to check for trading holiday.
197
+
198
+ Returns:
199
+ bool: True if it's a trading holiday or weekend, False otherwise.
200
+ """
201
+ logger.info("In - is_trading_holiday")
202
+ logger.info(f"Get holidays list for year={current_date.year}")
203
+
204
+ master_data_holidays = self.get_trading_holidays(current_date.year)
205
+ logger.info(f"Fetched trading holidays: {master_data_holidays}")
206
+
207
+ current_date_str = current_date.strftime('%Y-%m-%d')
208
+ is_current_date_in_list = any(item['date'] == current_date_str for item in master_data_holidays)
209
+ logger.info(f"Is current date in holiday list: {is_current_date_in_list}")
210
+
211
+ is_current_day_weekend = current_date.strftime("%A") in [self.SATURDAY, self.SUNDAY]
212
+ logger.info(f"Is current date a weekend: {is_current_day_weekend}")
213
+
214
+ is_holiday = is_current_date_in_list or is_current_day_weekend
215
+ logger.info(f"Final holiday check result: {is_holiday}")
216
+
217
+ return is_holiday
218
+
219
+ def is_market_open(self, domain: str, broker: str) -> bool:
220
+ """
221
+ Checks if the market is open based on the trading hours, holidays, and broker type.
222
+
223
+ Args:
224
+ domain (str): The domain name or identifier to fetch configuration data.
225
+ broker (str): The broker identifier to determine if it's a test broker.
226
+
227
+ Returns:
228
+ bool: True if market is open, False otherwise.
229
+ """
230
+ logger.info(f"In - is_market_open | domain={domain}, broker={broker}")
231
+
232
+ constants_data = self.get_constants_data(domain)
233
+ logger.info(f"Retrieved constants data: {constants_data}")
234
+
235
+ open_time = constants_data.get("market_hours", {}).get("start")
236
+ close_time = constants_data.get("market_hours", {}).get("end")
237
+ timezone = constants_data.get("timezone")
238
+ brokers = constants_data.get("broker", {})
239
+ broker_type = brokers.get(broker, {}).get('type')
240
+
241
+ logger.info(f"Broker type for {broker}: {broker_type}")
242
+ if broker_type == self.TEST:
243
+ logger.info("Broker type is TEST. Market is considered open.")
244
+ return True
245
+
246
+ current_date = datetime.now().date()
247
+ logger.info(f"Evaluating trading hours and holiday for date: {current_date}")
248
+
249
+ is_open = self.is_trading_hours(open_time, close_time, timezone)
250
+ is_holiday = self.is_trading_holiday(current_date)
251
+
252
+ if is_open and not is_holiday:
253
+ logger.info("Market is open based on hours and holiday check.")
254
+ return True
255
+
256
+ logger.info("Market is closed based on hours and/or holiday check.")
257
+ return False
@@ -0,0 +1,81 @@
1
+ """
2
+ Module: model_portfolio_reporting.py
3
+
4
+ Provides a client for interacting with the Model Portfolio microservice.
5
+ Inherits from the reusable ApiClient to enable consistent request handling,
6
+ header management, tracing, and logging for service-to-service communication.
7
+ """
8
+
9
+ import logging
10
+ from typing import Optional, Any, Dict
11
+
12
+ from bw_essentials.constants.services import Services
13
+ from bw_essentials.services.api_client import ApiClient
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ModelPortfolioReporting(ApiClient):
19
+ """
20
+ API client for the Model Portfolio Reporting microservice.
21
+
22
+ This class provides methods to interact with portfolio rebalance and
23
+ performance endpoints. It uses the shared ApiClient base for consistent
24
+ HTTP request handling, correlation ID injection, and logging.
25
+
26
+ Args:
27
+ service_user (str): Name of the system or user initiating the requests.
28
+ """
29
+
30
+ def __init__(self, service_user: str):
31
+ logger.info(f"Initializing ModelPortfolioReporting client for user: {service_user}")
32
+ super().__init__(user=service_user)
33
+ self.object = None
34
+ self.urls = {
35
+ "portfolio": "portfolio",
36
+ "portfolio_performance": "portfolio/%s/performance"
37
+ }
38
+ self.name = Services.MODEL_PORTFOLIO.value
39
+ self.base_url = self.get_base_url(Services.MODEL_PORTFOLIO.value)
40
+ logger.info(f"ModelPortfolioReporting initialized with base_url: {self.base_url}")
41
+
42
+ def get_active_rebalance(self, portfolio_id: str) -> Optional[Dict[str, Any]]:
43
+ """
44
+ Retrieve the active rebalance data for a given portfolio ID.
45
+
46
+ Args:
47
+ portfolio_id (str): The unique identifier of the portfolio.
48
+
49
+ Returns:
50
+ dict | None: The rebalance data if found, else None.
51
+ """
52
+ logger.info(f"In - get_active_rebalance with portfolio_id={portfolio_id}")
53
+ data = self._get(
54
+ url=self.base_url,
55
+ endpoint=f"{self.urls['portfolio']}/{portfolio_id}/rebalance/active/"
56
+ )
57
+ logger.info(f"Received response from get_active_rebalance: {data}")
58
+ return data.get("data")
59
+
60
+ def get_portfolio_performance_by_dates(self, portfolio_id: str, start_date: str, end_date: str) -> Optional[Dict[str, Any]]:
61
+ """
62
+ Fetch portfolio performance for a given date range.
63
+
64
+ Args:
65
+ portfolio_id (str): The portfolio identifier.
66
+ start_date (str): The start date for performance metrics (format: YYYY-MM-DD).
67
+ end_date (str): The end date for performance metrics (format: YYYY-MM-DD).
68
+
69
+ Returns:
70
+ dict | None: The performance data within the date range.
71
+ """
72
+ logger.info(f"In - get_portfolio_performance_by_dates with portfolio_id={portfolio_id}, start_date={start_date}, end_date={end_date}")
73
+ endpoint = self.urls['portfolio_performance'] % portfolio_id
74
+ params = {"start_date": start_date, "end_date": end_date}
75
+ performance = self._get(
76
+ url=self.base_url,
77
+ endpoint=endpoint,
78
+ params=params
79
+ )
80
+ logger.info(f"Received response from get_portfolio_performance_by_dates: {performance}")
81
+ return performance.get("data")