bw-essentials-core 0.1.17__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 +1 -0
- bw_essentials/email_client/email_client.py +31 -18
- bw_essentials/services/api_client.py +36 -0
- bw_essentials/services/broker.py +8 -3
- bw_essentials/services/compliance.py +78 -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 +46 -1
- bw_essentials/services/portfolio_catalogue.py +61 -2
- bw_essentials/services/portfolio_content.py +1 -1
- bw_essentials/services/trade_placement.py +262 -9
- bw_essentials/services/user_portfolio.py +398 -1
- bw_essentials/services/user_portfolio_reporting.py +20 -1
- {bw_essentials_core-0.1.17.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.17.dist-info/RECORD +0 -31
- {bw_essentials_core-0.1.17.dist-info → bw_essentials_core-0.1.33.dist-info}/WHEEL +0 -0
- {bw_essentials_core-0.1.17.dist-info → bw_essentials_core-0.1.33.dist-info}/top_level.txt +0 -0
|
@@ -80,7 +80,8 @@ class PortfolioCatalogue(ApiClient):
|
|
|
80
80
|
"rebalance": "rebalance",
|
|
81
81
|
"get_rebalance": "rebalance",
|
|
82
82
|
"subscriptions_by_ids": "subscription-plan/list",
|
|
83
|
-
"otp_subscriptions_by_ids": "one-time-plans/list"
|
|
83
|
+
"otp_subscriptions_by_ids": "one-time-plans/list",
|
|
84
|
+
"option_recommendations": "options/recommendations"
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
def create_rebalance(self, json: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
@@ -210,4 +211,62 @@ class PortfolioCatalogue(ApiClient):
|
|
|
210
211
|
}
|
|
211
212
|
response = self._post(url=url, endpoint=self.urls.get('otp_subscriptions_by_ids'), json=payload)
|
|
212
213
|
logger.info(f"Successfully fetched otp subscriptions by ids data. {response =}")
|
|
213
|
-
return response.get('data', [])
|
|
214
|
+
return response.get('data', [])
|
|
215
|
+
|
|
216
|
+
def get_options_recommendations(self,
|
|
217
|
+
strike_price_gt: float = None,
|
|
218
|
+
segment_in: str = None,
|
|
219
|
+
date_gte: str = None,
|
|
220
|
+
limit: int = None,
|
|
221
|
+
broker: str = None,
|
|
222
|
+
underlying_symbol: str = None,
|
|
223
|
+
status: str = None,
|
|
224
|
+
isin_code: str = None):
|
|
225
|
+
"""
|
|
226
|
+
Fetch options trading recommendations based on filtering parameters.
|
|
227
|
+
|
|
228
|
+
This method sends a GET request to the Portfolio-Catalogue service's
|
|
229
|
+
`/options/recommendations` endpoint with optional query parameters
|
|
230
|
+
for filtering by strike price, trading segment, date, broker, and limit.
|
|
231
|
+
|
|
232
|
+
Endpoint:
|
|
233
|
+
GET /options/recommendations
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
strike_price_gt (float, optional): Fetch recommendations where strike price > given value.
|
|
237
|
+
segment_in (str, optional): Trading segment to filter by (e.g., 'fno').
|
|
238
|
+
date_gte (str, optional): Minimum date (ISO format, e.g., '2025-10-28').
|
|
239
|
+
limit (int, optional): Limit the number of records returned.
|
|
240
|
+
broker (str, optional): Broker name to filter recommendations (e.g., 'zerodha').
|
|
241
|
+
underlying_symbol (str, optional): Trading symbol to filter recommendations (e.g., 'RELIANCE').
|
|
242
|
+
status (str, optional): Status of the recommendation. (e.g., 'active', 'inactive')
|
|
243
|
+
isin_code (str, optional): ISIN code to filter recommendations).
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
list[dict]: A list of recommendation records if available, otherwise an empty list.
|
|
247
|
+
"""
|
|
248
|
+
logger.info(
|
|
249
|
+
"In - get_options_recommendations strike_price_gt=%s, segment_in=%s, date_gte=%s, limit=%s, "
|
|
250
|
+
"broker=%s, underlying_symbol=%s, status=%s, isin_code=%s",
|
|
251
|
+
strike_price_gt, segment_in, date_gte, limit, broker, underlying_symbol, status, isin_code
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
params = {k: v for k, v in {
|
|
255
|
+
"strike_price__gt": strike_price_gt,
|
|
256
|
+
"segment__in": segment_in,
|
|
257
|
+
"date__gte": date_gte,
|
|
258
|
+
"limit": limit,
|
|
259
|
+
"broker": broker,
|
|
260
|
+
"underlying_symbol": underlying_symbol,
|
|
261
|
+
"status": status,
|
|
262
|
+
"isin_code": isin_code
|
|
263
|
+
}.items() if v is not None}
|
|
264
|
+
|
|
265
|
+
response = self._get(
|
|
266
|
+
url=self.base_url,
|
|
267
|
+
endpoint=self.urls.get('option_recommendations'),
|
|
268
|
+
params=params
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
logger.info(f"Successfully fetched options recommendations. {response =}")
|
|
272
|
+
return response.get("data", [])
|
|
@@ -53,6 +53,7 @@ class TradePlacement(ApiClient):
|
|
|
53
53
|
|
|
54
54
|
EXECUTABLE = 'executable'
|
|
55
55
|
READY_ONLY = 'read_only'
|
|
56
|
+
PUBLISHER = 'publisher'
|
|
56
57
|
BROKER_CONFIG_MAPPER = {
|
|
57
58
|
DEALER: READY_ONLY,
|
|
58
59
|
USER: EXECUTABLE
|
|
@@ -78,7 +79,8 @@ class TradePlacement(ApiClient):
|
|
|
78
79
|
self.name = Services.TRADE_PLACEMENT.value
|
|
79
80
|
self.urls = {
|
|
80
81
|
"order": "orders/order",
|
|
81
|
-
"update_orders": "orders/order/update"
|
|
82
|
+
"update_orders": "orders/order/update",
|
|
83
|
+
"update_order_status": "orders/order/status/update"
|
|
82
84
|
}
|
|
83
85
|
if self.user_info.get("broker_name") in [self.PAPER_TRADE]:
|
|
84
86
|
self._authenticate()
|
|
@@ -146,8 +148,8 @@ class TradePlacement(ApiClient):
|
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
def _place_order(self, trading_symbol, qty, side, order_tag, meta_data, product, order_type, proxy=None,
|
|
150
|
-
asm_consent=None, asm_reason=None):
|
|
151
|
+
def _place_order(self, trading_symbol, qty, side, order_tag, price, meta_data, product, order_type, proxy=None,
|
|
152
|
+
asm_consent=None, asm_reason=None, publisher=False):
|
|
151
153
|
"""
|
|
152
154
|
Places an order with the provided metadata and order details.
|
|
153
155
|
|
|
@@ -156,6 +158,7 @@ class TradePlacement(ApiClient):
|
|
|
156
158
|
qty (int): Quantity to trade.
|
|
157
159
|
side (str): Order side ('buy' or 'sell').
|
|
158
160
|
order_tag (str): Identifier tag for the order.
|
|
161
|
+
price (float): Price of the order.
|
|
159
162
|
meta_data (dict): Metadata about the order context.
|
|
160
163
|
product (str): Type of product ('cnc' or 'mtf').
|
|
161
164
|
order_type (str): Type of order ('market', 'limit').
|
|
@@ -171,15 +174,16 @@ class TradePlacement(ApiClient):
|
|
|
171
174
|
broker_config = self.EXECUTABLE
|
|
172
175
|
if proxy == self.READY_ONLY:
|
|
173
176
|
broker_config = self.READY_ONLY
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
if publisher:
|
|
178
|
+
broker_config = self.PUBLISHER
|
|
179
|
+
payload = {
|
|
176
180
|
"user_id": self.user_info.get("user_id"),
|
|
177
181
|
"entity_id": self.user_info.get("entity_id"),
|
|
178
182
|
"symbol": trading_symbol,
|
|
179
183
|
"quantity": abs(qty),
|
|
180
184
|
"broker": self.user_info.get("broker_name"),
|
|
181
185
|
"broker_config": broker_config,
|
|
182
|
-
"price":
|
|
186
|
+
"price": price,
|
|
183
187
|
"product_type": product,
|
|
184
188
|
"order_type": order_type,
|
|
185
189
|
"side": side,
|
|
@@ -187,10 +191,10 @@ class TradePlacement(ApiClient):
|
|
|
187
191
|
"metadata": meta_data,
|
|
188
192
|
"asm_consent": asm_consent,
|
|
189
193
|
"asm_reason": asm_reason
|
|
190
|
-
}
|
|
194
|
+
}
|
|
191
195
|
placed_orders_data = self._post(url=self.base_url,
|
|
192
196
|
endpoint=self.urls.get("order"),
|
|
193
|
-
|
|
197
|
+
json=payload)
|
|
194
198
|
return placed_orders_data.get("data")
|
|
195
199
|
|
|
196
200
|
def _market_buy_cnc(self, trading_symbol, qty, generate_order_tag,
|
|
@@ -494,6 +498,255 @@ class TradePlacement(ApiClient):
|
|
|
494
498
|
response = self._put(
|
|
495
499
|
url=self.base_url,
|
|
496
500
|
endpoint=f'{self.urls.get("update_orders")}/{tag}',
|
|
497
|
-
|
|
501
|
+
json=instruction
|
|
498
502
|
)
|
|
499
503
|
return response
|
|
504
|
+
|
|
505
|
+
def update_order_status(self, broker_config, product_type, tag):
|
|
506
|
+
"""
|
|
507
|
+
Update the status of an existing order.
|
|
508
|
+
|
|
509
|
+
This method sends a PUT request to the Trade Placement Service to update
|
|
510
|
+
the status of a specific order identified by its tag. The update includes
|
|
511
|
+
user, broker, and product configuration details.
|
|
512
|
+
|
|
513
|
+
Endpoint:
|
|
514
|
+
PUT /orders/order/status/update
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
broker_config (str): Broker configuration (e.g., 'publisher', 'executable').
|
|
518
|
+
product_type (str): Type of product (e.g., 'fno', 'cnc', 'mtf').
|
|
519
|
+
tag (str): Unique tag identifying the order.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
dict: API response from the Trade Placement Service.
|
|
523
|
+
|
|
524
|
+
Raises:
|
|
525
|
+
HTTPError: If the PUT request fails.
|
|
526
|
+
"""
|
|
527
|
+
logger.info(f"Updating order status for {tag =}, {self.broker =}, {product_type =}")
|
|
528
|
+
|
|
529
|
+
if not self.user_id or not self.entity_id:
|
|
530
|
+
raise ValueError("user_id and entity_id must be set to update order status.")
|
|
531
|
+
|
|
532
|
+
payload = {
|
|
533
|
+
"user_id": self.user_id,
|
|
534
|
+
"entity_id": self.entity_id,
|
|
535
|
+
"broker": self.broker,
|
|
536
|
+
"broker_config": broker_config,
|
|
537
|
+
"product_type": product_type,
|
|
538
|
+
"tag": tag
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
response = self._put(
|
|
542
|
+
url=self.base_url,
|
|
543
|
+
endpoint=self.urls.get("update_order_status"),
|
|
544
|
+
json=payload
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
logger.info(f"Order status update response: {response}")
|
|
548
|
+
return response
|
|
549
|
+
|
|
550
|
+
def _get_fno_metadata(self):
|
|
551
|
+
"""
|
|
552
|
+
Build metadata dictionary for FNO order.
|
|
553
|
+
Returns:
|
|
554
|
+
dict: Metadata for FNO order.
|
|
555
|
+
"""
|
|
556
|
+
user_info = self.user_info or {}
|
|
557
|
+
return {
|
|
558
|
+
"user_id": user_info.get("user_id"),
|
|
559
|
+
"date": str(datetime.now().date()),
|
|
560
|
+
"user_info": {
|
|
561
|
+
"current_broker": user_info.get("current_broker"),
|
|
562
|
+
"broker_name": user_info.get("broker_name"),
|
|
563
|
+
"entity_id": user_info.get("entity_id")
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
def _market_buy_fno(self, trading_symbol, qty, generate_order_tag, price, publisher, proxy=None):
|
|
568
|
+
"""
|
|
569
|
+
Places a market buy order for FNO.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
trading_symbol (str): Symbol to buy.
|
|
573
|
+
qty (int): Quantity to buy.
|
|
574
|
+
generate_order_tag (str): Order tag.
|
|
575
|
+
proxy (str, optional): Proxy config.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
dict: Order response data.
|
|
579
|
+
"""
|
|
580
|
+
logger.info(f"Placing FNO market buy order for {trading_symbol=}, {qty=}")
|
|
581
|
+
meta_data = self._get_fno_metadata()
|
|
582
|
+
return self._place_order(trading_symbol, qty, self.BUY, generate_order_tag, price,
|
|
583
|
+
meta_data, "fno", self.MARKET, proxy=proxy, publisher=publisher)
|
|
584
|
+
|
|
585
|
+
def _market_sell_fno(self, trading_symbol, qty, generate_order_tag, price, publisher, proxy=None):
|
|
586
|
+
"""
|
|
587
|
+
Places a market sell order for FNO.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
trading_symbol (str): Symbol to sell.
|
|
591
|
+
qty (int): Quantity to sell.
|
|
592
|
+
generate_order_tag (str): Order tag.
|
|
593
|
+
price (float): Price of the order.
|
|
594
|
+
publisher (bool): Whether to use publisher config. Defaults to False.
|
|
595
|
+
proxy (str, optional): Proxy config.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
dict: Order response data.
|
|
599
|
+
"""
|
|
600
|
+
logger.info(f"Placing FNO market sell order for {trading_symbol=}, {qty=}")
|
|
601
|
+
meta_data = self._get_fno_metadata()
|
|
602
|
+
return self._place_order(trading_symbol, qty, self.SELL, generate_order_tag, price,
|
|
603
|
+
meta_data, "fno", self.MARKET, proxy=proxy, publisher=publisher)
|
|
604
|
+
|
|
605
|
+
def execute_market_buy_orders_fno(self, instructions, publisher):
|
|
606
|
+
"""
|
|
607
|
+
Execute FNO Market buy orders in bulk.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
instructions (list): List of buy order dicts (symbol, qty, tag, instruction_id).
|
|
611
|
+
publisher (bool): Whether to use publisher config. Defaults to False.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
None
|
|
615
|
+
"""
|
|
616
|
+
logger.info(f"In execute_market_buy_orders_fno - {instructions =}")
|
|
617
|
+
for order in instructions:
|
|
618
|
+
data = self._market_buy_fno(
|
|
619
|
+
trading_symbol=order.get("symbol"),
|
|
620
|
+
qty=order.get("qty"),
|
|
621
|
+
generate_order_tag=order.get("tag"),
|
|
622
|
+
price=order.get("price"),
|
|
623
|
+
publisher=publisher
|
|
624
|
+
)
|
|
625
|
+
logger.info(f"execute_market_buy_orders_fno {data = }")
|
|
626
|
+
|
|
627
|
+
def execute_market_sell_orders_fno(self, instructions, publisher):
|
|
628
|
+
"""
|
|
629
|
+
Execute FNO Market sell orders in bulk.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
instructions (list): List of sell order dicts (symbol, qty, tag, instruction_id).
|
|
633
|
+
publisher (bool): Whether to use publisher config. Defaults to False.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
None
|
|
637
|
+
"""
|
|
638
|
+
logger.info(f"In execute_market_sell_orders_fno - {instructions =}, {publisher =}")
|
|
639
|
+
for order in instructions:
|
|
640
|
+
data = self._market_sell_fno(
|
|
641
|
+
trading_symbol=order.get("symbol"),
|
|
642
|
+
qty=order.get("qty"),
|
|
643
|
+
generate_order_tag=order.get("tag"),
|
|
644
|
+
price=order.get("price"),
|
|
645
|
+
publisher=publisher
|
|
646
|
+
)
|
|
647
|
+
logger.info(f"execute_market_sell_orders_fno {data = }")
|
|
648
|
+
|
|
649
|
+
def _limit_buy_fno(self, trading_symbol, qty, generate_order_tag, price, publisher, proxy=None):
|
|
650
|
+
"""
|
|
651
|
+
Places a limit buy order for FNO.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
trading_symbol (str): Symbol to buy.
|
|
655
|
+
qty (int): Quantity to buy.
|
|
656
|
+
generate_order_tag (str): Order tag.
|
|
657
|
+
price (float): Limit price.
|
|
658
|
+
publisher (bool): Whether to use publisher config.
|
|
659
|
+
proxy (str, optional): Proxy config.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
dict: Order response data.
|
|
663
|
+
"""
|
|
664
|
+
logger.info(f"Placing FNO limit buy order for {trading_symbol=}, {qty=}, {price=}")
|
|
665
|
+
meta_data = self._get_fno_metadata()
|
|
666
|
+
return self._place_order(
|
|
667
|
+
trading_symbol,
|
|
668
|
+
qty,
|
|
669
|
+
self.BUY,
|
|
670
|
+
generate_order_tag,
|
|
671
|
+
price,
|
|
672
|
+
meta_data,
|
|
673
|
+
"fno",
|
|
674
|
+
self.LIMIT,
|
|
675
|
+
proxy=proxy,
|
|
676
|
+
publisher=publisher
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def _limit_sell_fno(self, trading_symbol, qty, generate_order_tag, price, publisher, proxy=None):
|
|
680
|
+
"""
|
|
681
|
+
Places a limit sell order for FNO.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
trading_symbol (str): Symbol to sell.
|
|
685
|
+
qty (int): Quantity to sell.
|
|
686
|
+
generate_order_tag (str): Order tag.
|
|
687
|
+
price (float): Limit price.
|
|
688
|
+
publisher (bool): Whether to use publisher config.
|
|
689
|
+
proxy (str, optional): Proxy config.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
dict: Order response data.
|
|
693
|
+
"""
|
|
694
|
+
logger.info(f"Placing FNO limit sell order for {trading_symbol=}, {qty=}, {price=}")
|
|
695
|
+
meta_data = self._get_fno_metadata()
|
|
696
|
+
return self._place_order(
|
|
697
|
+
trading_symbol,
|
|
698
|
+
qty,
|
|
699
|
+
self.SELL,
|
|
700
|
+
generate_order_tag,
|
|
701
|
+
price,
|
|
702
|
+
meta_data,
|
|
703
|
+
"fno",
|
|
704
|
+
self.LIMIT,
|
|
705
|
+
proxy=proxy,
|
|
706
|
+
publisher=publisher
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
def execute_limit_buy_orders_fno(self, instructions, publisher):
|
|
710
|
+
"""
|
|
711
|
+
Execute FNO limit buy orders in bulk.
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
instructions (list): List of buy order dicts (symbol, qty, tag, price).
|
|
715
|
+
publisher (bool): Whether to use publisher config.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
None
|
|
719
|
+
"""
|
|
720
|
+
logger.info(f"In execute_limit_buy_orders_fno - {instructions =}")
|
|
721
|
+
for order in instructions:
|
|
722
|
+
data = self._limit_buy_fno(
|
|
723
|
+
trading_symbol=order.get("symbol"),
|
|
724
|
+
qty=order.get("qty"),
|
|
725
|
+
generate_order_tag=order.get("tag"),
|
|
726
|
+
price=order.get("price"),
|
|
727
|
+
publisher=publisher
|
|
728
|
+
)
|
|
729
|
+
logger.info(f"execute_limit_buy_orders_fno {data = }")
|
|
730
|
+
|
|
731
|
+
def execute_limit_sell_orders_fno(self, instructions, publisher):
|
|
732
|
+
"""
|
|
733
|
+
Execute FNO limit sell orders in bulk.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
instructions (list): List of sell order dicts (symbol, qty, tag, price).
|
|
737
|
+
publisher (bool): Whether to use publisher config.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
None
|
|
741
|
+
"""
|
|
742
|
+
logger.info(f"In execute_limit_sell_orders_fno - {instructions =}, {publisher =}")
|
|
743
|
+
for order in instructions:
|
|
744
|
+
data = self._limit_sell_fno(
|
|
745
|
+
trading_symbol=order.get("symbol"),
|
|
746
|
+
qty=order.get("qty"),
|
|
747
|
+
generate_order_tag=order.get("tag"),
|
|
748
|
+
price=order.get("price"),
|
|
749
|
+
publisher=publisher
|
|
750
|
+
)
|
|
751
|
+
logger.info(f"execute_limit_sell_orders_fno {data = }")
|
|
752
|
+
|