tradingapi 0.3.7__tar.gz → 0.3.9__tar.gz

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.
Files changed (33) hide show
  1. {tradingapi-0.3.7 → tradingapi-0.3.9}/PKG-INFO +1 -1
  2. {tradingapi-0.3.7 → tradingapi-0.3.9}/pyproject.toml +1 -1
  3. tradingapi-0.3.9/tests/test_broker_side_terminal_order.py +30 -0
  4. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/broker_base.py +123 -0
  5. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/dhan.py +20 -10
  6. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/fivepaisa.py +120 -68
  7. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/flattrade.py +5 -1
  8. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/icicidirect.py +24 -1
  9. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/shoonya.py +5 -1
  10. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/utils.py +128 -14
  11. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi.egg-info/PKG-INFO +1 -1
  12. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi.egg-info/SOURCES.txt +1 -0
  13. {tradingapi-0.3.7 → tradingapi-0.3.9}/README.md +0 -0
  14. {tradingapi-0.3.7 → tradingapi-0.3.9}/setup.cfg +0 -0
  15. {tradingapi-0.3.7 → tradingapi-0.3.9}/tests/test_calculate_delta_realtime_quotes.py +0 -0
  16. {tradingapi-0.3.7 → tradingapi-0.3.9}/tests/test_find_option_with_delta.py +0 -0
  17. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/__init__.py +0 -0
  18. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/allocation.py +0 -0
  19. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/attribution.py +0 -0
  20. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/config/commissions_20241216.yaml +0 -0
  21. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/config/config_sample.yaml +0 -0
  22. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/config.py +0 -0
  23. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/error_handling.py +0 -0
  24. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/exceptions.py +0 -0
  25. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/globals.py +0 -0
  26. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/icicidirect_generate_session.py +0 -0
  27. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/market_data_exchanges.py +0 -0
  28. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/proxy_utils.py +0 -0
  29. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi/span.py +0 -0
  30. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi.egg-info/dependency_links.txt +0 -0
  31. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi.egg-info/entry_points.txt +0 -0
  32. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi.egg-info/requires.txt +0 -0
  33. {tradingapi-0.3.7 → tradingapi-0.3.9}/tradingapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradingapi
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: Trade integration with brokers
5
5
  Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
6
  License-Expression: MIT
@@ -28,7 +28,7 @@ packages = ["tradingapi"]
28
28
 
29
29
  [project]
30
30
  name = "tradingapi"
31
- version = "0.3.7"
31
+ version = "0.3.9"
32
32
  description = "Trade integration with brokers"
33
33
  readme = "README.md"
34
34
  license = "MIT"
@@ -0,0 +1,30 @@
1
+ from tradingapi.broker_base import (
2
+ is_broker_side_terminal_message,
3
+ is_broker_side_terminal_order,
4
+ is_missing_exchange_order_id,
5
+ )
6
+
7
+
8
+ def test_is_missing_exchange_order_id():
9
+ assert is_missing_exchange_order_id("0")
10
+ assert is_missing_exchange_order_id(0)
11
+ assert is_missing_exchange_order_id(None)
12
+ assert is_missing_exchange_order_id("")
13
+ assert not is_missing_exchange_order_id("2700000201461104")
14
+
15
+
16
+ def test_is_broker_side_terminal_message():
17
+ assert is_broker_side_terminal_message("Trading not allowed in illiquid contract")
18
+ assert is_broker_side_terminal_message("Order rejected by RMS")
19
+ assert is_broker_side_terminal_message(
20
+ "RMS:23226063020407:You have insufficient funds. Please add Rs.71301.37 to trade."
21
+ )
22
+ assert is_broker_side_terminal_message("Cancelled by user")
23
+ assert not is_broker_side_terminal_message("")
24
+ assert not is_broker_side_terminal_message("Success")
25
+
26
+
27
+ def test_is_broker_side_terminal_order_requires_missing_exchange_id():
28
+ msg = "Trading not allowed in illiquid contract"
29
+ assert is_broker_side_terminal_order("0", msg)
30
+ assert not is_broker_side_terminal_order("12345", msg)
@@ -95,6 +95,49 @@ class OrderStatus(Enum):
95
95
  CANCELLED = 7 # Cancelled by Exchange
96
96
 
97
97
 
98
+ _BROKER_SIDE_TERMINAL_KEYWORDS = (
99
+ "illiquid",
100
+ "not allowed",
101
+ "not permitted",
102
+ "barred",
103
+ "freeze",
104
+ "frozen",
105
+ "restricted",
106
+ "invalid contract",
107
+ "invalid scrip",
108
+ "security not available",
109
+ "order rejected",
110
+ "rejected by",
111
+ "cannot place",
112
+ "unable to place",
113
+ "trading disabled",
114
+ "not tradable",
115
+ "not tradeable",
116
+ "margin shortfall",
117
+ "insufficient margin",
118
+ "insufficient funds",
119
+ "rms reject",
120
+ )
121
+
122
+
123
+ def is_missing_exchange_order_id(exch_order_id) -> bool:
124
+ value = str(exch_order_id or "").strip()
125
+ return value in {"", "0", "None", "NONE", "null", "nan", "NaN"}
126
+
127
+
128
+ def is_broker_side_terminal_message(message: str) -> bool:
129
+ msg = str(message or "").strip().lower()
130
+ if not msg:
131
+ return False
132
+ if "reject" in msg or "cancel" in msg:
133
+ return True
134
+ return any(keyword in msg for keyword in _BROKER_SIDE_TERMINAL_KEYWORDS)
135
+
136
+
137
+ def is_broker_side_terminal_order(exch_order_id, message: str) -> bool:
138
+ return is_missing_exchange_order_id(exch_order_id) and is_broker_side_terminal_message(message)
139
+
140
+
98
141
  def _validate_quantity(quantity):
99
142
  """
100
143
  Validate quantity parameter that can be int, float, or string representation.
@@ -756,6 +799,86 @@ class BrokerBase(ABC):
756
799
  "symbol_map_reversed": {},
757
800
  }
758
801
 
802
+ def _request_cancel_broker_side_terminal_order(self, order: "Order") -> bool:
803
+ """Send an explicit broker cancel when the order may still be live."""
804
+ broker_order_id = str(getattr(order, "broker_order_id", "") or "").strip()
805
+ if not broker_order_id or broker_order_id == "0" or broker_order_id.upper().endswith("P"):
806
+ return False
807
+ if getattr(order, "paper", False) in (True, "True", "true"):
808
+ return False
809
+ try:
810
+ self.cancel_order(broker_order_id=broker_order_id, resolve_terminal=False)
811
+ return True
812
+ except Exception as e:
813
+ _get_trading_logger().log_warning(
814
+ "Cancel request failed for broker-side terminal order",
815
+ {"broker_order_id": broker_order_id, "error": str(e)},
816
+ )
817
+ return False
818
+
819
+ def _resolve_broker_side_terminal_order(
820
+ self,
821
+ order: "Order",
822
+ message: str,
823
+ *,
824
+ order_size=None,
825
+ order_price=None,
826
+ ) -> OrderInfo:
827
+ """Cancel if possible and return a zero-fill CANCELLED OrderInfo."""
828
+ broker_order_id = str(getattr(order, "broker_order_id", "") or "").strip()
829
+ if message:
830
+ order.message = message
831
+ if broker_order_id:
832
+ try:
833
+ self.redis_o.hset(broker_order_id, "message", message)
834
+ except Exception:
835
+ pass
836
+
837
+ cancel_requested = self._request_cancel_broker_side_terminal_order(order)
838
+ order.status = OrderStatus.CANCELLED
839
+ if broker_order_id:
840
+ try:
841
+ self.redis_o.hset(broker_order_id, "status", OrderStatus.CANCELLED.name)
842
+ except Exception:
843
+ pass
844
+
845
+ _get_trading_logger().log_info(
846
+ "Broker-side terminal order resolved as cancelled",
847
+ {
848
+ "broker_order_id": broker_order_id,
849
+ "long_symbol": getattr(order, "long_symbol", ""),
850
+ "message": message,
851
+ "cancel_requested": cancel_requested,
852
+ },
853
+ )
854
+ return OrderInfo(
855
+ order_size=order_size if order_size is not None else order.quantity,
856
+ order_price=order_price if order_price is not None else order.price,
857
+ fill_size=0,
858
+ fill_price=0,
859
+ status=OrderStatus.CANCELLED,
860
+ broker_order_id=broker_order_id,
861
+ exchange_order_id=getattr(order, "exch_order_id", ""),
862
+ broker=getattr(order, "broker", None) or self.broker,
863
+ )
864
+
865
+ def _apply_broker_side_terminal_resolution(
866
+ self,
867
+ order: "Order",
868
+ order_info: OrderInfo,
869
+ message: Optional[str] = None,
870
+ ) -> OrderInfo:
871
+ msg = str(message if message is not None else getattr(order, "message", "") or "").strip()
872
+ exch_id = order_info.exchange_order_id or getattr(order, "exch_order_id", "")
873
+ if not is_broker_side_terminal_order(exch_id, msg):
874
+ return order_info
875
+ return self._resolve_broker_side_terminal_order(
876
+ order,
877
+ msg,
878
+ order_size=order_info.order_size,
879
+ order_price=order_info.order_price,
880
+ )
881
+
759
882
  @abstractmethod
760
883
  def update_symbology(self, **kwargs):
761
884
  """
@@ -1476,7 +1476,11 @@ class Dhan(BrokerBase):
1476
1476
 
1477
1477
  fills = None
1478
1478
  try:
1479
- fills = self.get_order_info(broker_order_id=broker_order_id, order=order)
1479
+ fills = self.get_order_info(
1480
+ broker_order_id=broker_order_id,
1481
+ order=order,
1482
+ resolve_terminal=kwargs.get("resolve_terminal", False),
1483
+ )
1480
1484
  except Exception:
1481
1485
  pass
1482
1486
 
@@ -1617,15 +1621,7 @@ class Dhan(BrokerBase):
1617
1621
  if order is not None and error_message:
1618
1622
  order.message = error_message
1619
1623
 
1620
- if status == OrderStatus.REJECTED:
1621
- try:
1622
- internal_order_id = self.redis_o.hget(broker_order_id, "orderRef")
1623
- if internal_order_id:
1624
- delete_broker_order_id(self, internal_order_id, broker_order_id)
1625
- except Exception:
1626
- pass
1627
-
1628
- return OrderInfo(
1624
+ order_info = OrderInfo(
1629
1625
  order_size=order_qty,
1630
1626
  order_price=order_price,
1631
1627
  fill_size=fill_size,
@@ -1635,6 +1631,20 @@ class Dhan(BrokerBase):
1635
1631
  exchange_order_id=exch_order_id,
1636
1632
  broker=self.broker,
1637
1633
  )
1634
+ if kwargs.get("resolve_terminal", True):
1635
+ order_info = self._apply_broker_side_terminal_resolution(
1636
+ order, order_info, message=error_message or order.message
1637
+ )
1638
+
1639
+ if order_info.status == OrderStatus.REJECTED:
1640
+ try:
1641
+ internal_order_id = self.redis_o.hget(broker_order_id, "orderRef")
1642
+ if internal_order_id:
1643
+ delete_broker_order_id(self, internal_order_id, broker_order_id)
1644
+ except Exception:
1645
+ pass
1646
+
1647
+ return order_info
1638
1648
 
1639
1649
  except (ValidationError, OrderError, BrokerConnectionError):
1640
1650
  raise
@@ -85,7 +85,19 @@ def _patch_py5paisa_for_mom(api) -> None:
85
85
  api.multi_order_Margin = fixed_multi_order_Margin
86
86
  api._tradingapi_patched = True
87
87
 
88
- from .broker_base import BrokerBase, Brokers, HistoricalData, Order, OrderInfo, OrderStatus, Price, _normalize_as_of_date
88
+ from .broker_base import (
89
+ BrokerBase,
90
+ Brokers,
91
+ HistoricalData,
92
+ Order,
93
+ OrderInfo,
94
+ OrderStatus,
95
+ Price,
96
+ _normalize_as_of_date,
97
+ is_broker_side_terminal_message,
98
+ is_broker_side_terminal_order,
99
+ is_missing_exchange_order_id,
100
+ )
89
101
  from .config import get_config
90
102
  from .utils import (
91
103
  delete_broker_order_id,
@@ -1914,6 +1926,17 @@ class FivePaisa(BrokerBase):
1914
1926
  context = create_error_context(kwargs=kwargs, error=str(e))
1915
1927
  raise OrderError(f"Unexpected error modifying order: {str(e)}", context)
1916
1928
 
1929
+ @log_execution_time
1930
+ @validate_inputs(broker_order_id=lambda x: isinstance(x, str) and len(x.strip()) > 0)
1931
+ def _request_cancel_broker_side_terminal_order(self, order: Order) -> bool:
1932
+ if is_missing_exchange_order_id(getattr(order, "exch_order_id", None)):
1933
+ trading_logger.log_info(
1934
+ "FivePaisa order blocked before exchange; marking cancelled locally",
1935
+ {"broker_order_id": order.broker_order_id, "message": order.message},
1936
+ )
1937
+ return False
1938
+ return super()._request_cancel_broker_side_terminal_order(order)
1939
+
1917
1940
  @log_execution_time
1918
1941
  @validate_inputs(broker_order_id=lambda x: isinstance(x, str) and len(x.strip()) > 0)
1919
1942
  def cancel_order(self, **kwargs) -> Order:
@@ -1968,7 +1991,11 @@ class FivePaisa(BrokerBase):
1968
1991
  if date_matches:
1969
1992
  # Get current order info
1970
1993
  try:
1971
- fills = self.get_order_info(broker_order_id=broker_order_id, order=order)
1994
+ fills = self.get_order_info(
1995
+ broker_order_id=broker_order_id,
1996
+ order=order,
1997
+ resolve_terminal=kwargs.get("resolve_terminal", False),
1998
+ )
1972
1999
  except Exception as e:
1973
2000
  trading_logger.log_error(
1974
2001
  "Failed to get order info for cancellation",
@@ -2247,6 +2274,7 @@ class FivePaisa(BrokerBase):
2247
2274
  """
2248
2275
  try:
2249
2276
  trading_logger.log_debug("Getting order info", {"broker_order_id": kwargs.get("broker_order_id")})
2277
+ resolve_terminal = kwargs.get("resolve_terminal", True)
2250
2278
 
2251
2279
  def return_db_as_fills(order: Order):
2252
2280
  """Return order info from database for historical orders."""
@@ -2272,6 +2300,11 @@ class FivePaisa(BrokerBase):
2272
2300
  trading_logger.log_error("Error in return_db_as_fills", e, {"order": str(order)})
2273
2301
  raise
2274
2302
 
2303
+ def _finalize_order_book_info(order: Order, order_info: OrderInfo, reason: str = "") -> OrderInfo:
2304
+ if not resolve_terminal:
2305
+ return order_info
2306
+ return self._apply_broker_side_terminal_resolution(order, order_info, message=reason or order.message)
2307
+
2275
2308
  def get_orderinfo_from_orders(exch_order_id: str, order: Order, broker_order_id: str) -> OrderInfo:
2276
2309
  """Get order info from order book."""
2277
2310
  try:
@@ -2281,21 +2314,18 @@ class FivePaisa(BrokerBase):
2281
2314
  if len(orders) > 0:
2282
2315
  fivepaisa_order = orders[orders.BrokerOrderId.astype(str) == str(broker_order_id)]
2283
2316
  if len(fivepaisa_order) == 1:
2284
- if fivepaisa_order.OrderStatus.str.lower().str.contains("rejected").iloc[0] is True:
2285
- # order cancelled by broker before reaching exchange
2286
- trading_logger.log_info(
2287
- "Order rejected by broker",
2288
- {"broker_order_id": broker_order_id, "reason": str(fivepaisa_order.Reason.iloc[0])},
2289
- )
2290
- broker_order_id = str(fivepaisa_order.BrokerOrderId.iloc[0])
2291
- try:
2292
- internal_order_id = self.redis_o.hget(broker_order_id, "orderRef")
2293
- if internal_order_id is not None:
2294
- delete_broker_order_id(self, internal_order_id, broker_order_id)
2295
- except Exception as e:
2296
- trading_logger.log_warning(
2297
- "Failed to delete broker order ID",
2298
- {"broker_order_id": broker_order_id, "error": str(e)},
2317
+ reason = str(fivepaisa_order.Reason.iloc[0] if "Reason" in fivepaisa_order.columns else "")
2318
+ exch_id = fivepaisa_order.ExchOrderID.iloc[0]
2319
+ if (
2320
+ fivepaisa_order.OrderStatus.str.lower().str.contains("rejected").iloc[0] is True
2321
+ or is_broker_side_terminal_order(exch_id, reason or order.message)
2322
+ ):
2323
+ if resolve_terminal:
2324
+ return self._resolve_broker_side_terminal_order(
2325
+ order,
2326
+ reason or order.message,
2327
+ order_size=order.quantity,
2328
+ order_price=order.price,
2299
2329
  )
2300
2330
  return OrderInfo(
2301
2331
  order_size=order.quantity,
@@ -2304,7 +2334,7 @@ class FivePaisa(BrokerBase):
2304
2334
  fill_price=0,
2305
2335
  status=OrderStatus.REJECTED,
2306
2336
  broker_order_id=order.broker_order_id,
2307
- exchange_order_id=fivepaisa_order.ExchOrderID.iloc[0],
2337
+ exchange_order_id=exch_id,
2308
2338
  broker=self.broker,
2309
2339
  )
2310
2340
  else:
@@ -2332,15 +2362,19 @@ class FivePaisa(BrokerBase):
2332
2362
  contract_size = self.exchange_mappings[order.exchange]["contractsize_map"].get(
2333
2363
  long_symbol
2334
2364
  )
2335
- return OrderInfo(
2336
- order_size=order.quantity,
2337
- order_price=order.price,
2338
- fill_size=fill_size * contract_size,
2339
- fill_price=fill_price,
2340
- status=status,
2341
- broker_order_id=order.broker_order_id,
2342
- exchange_order_id=fivepaisa_order.ExchOrderID.iloc[0],
2343
- broker=self.broker,
2365
+ return _finalize_order_book_info(
2366
+ order,
2367
+ OrderInfo(
2368
+ order_size=order.quantity,
2369
+ order_price=order.price,
2370
+ fill_size=fill_size * contract_size,
2371
+ fill_price=fill_price,
2372
+ status=status,
2373
+ broker_order_id=order.broker_order_id,
2374
+ exchange_order_id=fivepaisa_order.ExchOrderID.iloc[0],
2375
+ broker=self.broker,
2376
+ ),
2377
+ reason,
2344
2378
  )
2345
2379
  except Exception as e:
2346
2380
  trading_logger.log_error(
@@ -2350,15 +2384,19 @@ class FivePaisa(BrokerBase):
2350
2384
  )
2351
2385
  raise
2352
2386
  else:
2353
- return OrderInfo(
2354
- order_size=order.quantity,
2355
- order_price=order.price,
2356
- fill_size=fill_size,
2357
- fill_price=fill_price,
2358
- status=status,
2359
- broker_order_id=order.broker_order_id,
2360
- exchange_order_id=fivepaisa_order.ExchOrderID.iloc[0],
2361
- broker=self.broker,
2387
+ return _finalize_order_book_info(
2388
+ order,
2389
+ OrderInfo(
2390
+ order_size=order.quantity,
2391
+ order_price=order.price,
2392
+ fill_size=fill_size,
2393
+ fill_price=fill_price,
2394
+ status=status,
2395
+ broker_order_id=order.broker_order_id,
2396
+ exchange_order_id=fivepaisa_order.ExchOrderID.iloc[0],
2397
+ broker=self.broker,
2398
+ ),
2399
+ reason,
2362
2400
  )
2363
2401
  else:
2364
2402
  trading_logger.log_debug(
@@ -2400,15 +2438,19 @@ class FivePaisa(BrokerBase):
2400
2438
  contract_size = self.exchange_mappings[order.exchange][
2401
2439
  "contractsize_map"
2402
2440
  ].get(long_symbol)
2403
- return OrderInfo(
2404
- order_size=order_size,
2405
- order_price=order_price,
2406
- fill_size=fill_size * contract_size,
2407
- fill_price=fill_price,
2408
- status=status,
2409
- broker_order_id=broker_order_id,
2410
- exchange_order_id=row["ExchOrderID"],
2411
- broker=self.broker,
2441
+ return _finalize_order_book_info(
2442
+ order,
2443
+ OrderInfo(
2444
+ order_size=order_size,
2445
+ order_price=order_price,
2446
+ fill_size=fill_size * contract_size,
2447
+ fill_price=fill_price,
2448
+ status=status,
2449
+ broker_order_id=broker_order_id,
2450
+ exchange_order_id=row["ExchOrderID"],
2451
+ broker=self.broker,
2452
+ ),
2453
+ str(row.get("Reason", "")),
2412
2454
  )
2413
2455
  except Exception as e:
2414
2456
  trading_logger.log_error(
@@ -2418,15 +2460,19 @@ class FivePaisa(BrokerBase):
2418
2460
  )
2419
2461
  raise
2420
2462
  else:
2421
- return OrderInfo(
2422
- order_size=order_size,
2423
- order_price=order_price,
2424
- fill_size=fill_size,
2425
- fill_price=fill_price,
2426
- status=status,
2427
- broker_order_id=broker_order_id,
2428
- exchange_order_id=row["ExchOrderID"],
2429
- broker=self.broker,
2463
+ return _finalize_order_book_info(
2464
+ order,
2465
+ OrderInfo(
2466
+ order_size=order_size,
2467
+ order_price=order_price,
2468
+ fill_size=fill_size,
2469
+ fill_price=fill_price,
2470
+ status=status,
2471
+ broker_order_id=broker_order_id,
2472
+ exchange_order_id=row["ExchOrderID"],
2473
+ broker=self.broker,
2474
+ ),
2475
+ str(row.get("Reason", "")),
2430
2476
  )
2431
2477
  return OrderInfo(
2432
2478
  order_size=order.quantity,
@@ -2534,20 +2580,14 @@ class FivePaisa(BrokerBase):
2534
2580
  )
2535
2581
 
2536
2582
  # Handle orders with no exchange order ID
2537
- if order.exch_order_id in [0, "0", None, "None"]:
2538
- if "reject" in order.message.lower() or "cancel" in order.message.lower():
2539
- fills = OrderInfo()
2540
- if "reject" in order.message.lower():
2541
- fills.status = OrderStatus.REJECTED
2542
- if "cancel" in order.message.lower():
2543
- fills.status = OrderStatus.CANCELLED
2544
- fills.fill_price = 0
2545
- fills.fill_size = 0
2546
- fills.broker = self.broker
2547
- fills.broker_order_id = order.broker_order_id
2548
- fills.order_price = order.price
2549
- fills.order_size = order.quantity
2550
- return fills
2583
+ if is_missing_exchange_order_id(order.exch_order_id):
2584
+ if resolve_terminal and is_broker_side_terminal_message(order.message):
2585
+ return self._resolve_broker_side_terminal_order(
2586
+ order,
2587
+ order.message,
2588
+ order_size=order.quantity,
2589
+ order_price=order.price,
2590
+ )
2551
2591
  return get_orderinfo_from_orders("0", order=order, broker_order_id=broker_order_id)
2552
2592
  else:
2553
2593
  # Handle orders with exchange order ID
@@ -3625,6 +3665,14 @@ class FivePaisa(BrokerBase):
3625
3665
 
3626
3666
  return None, tick_exchange
3627
3667
 
3668
+ def is_market_feed_payload(data):
3669
+ """True for MarketFeedV3 tick objects; false for trade/order pushes (ReqType) and subscribe acks."""
3670
+ if not isinstance(data, dict):
3671
+ return False
3672
+ if data.get("ReqType"):
3673
+ return False
3674
+ return any(k in data for k in ("Token", "LastRate", "BidRate", "TickDt"))
3675
+
3628
3676
  def map_to_price(json_data):
3629
3677
  """Map JSON data to Price object."""
3630
3678
  try:
@@ -3666,12 +3714,16 @@ class FivePaisa(BrokerBase):
3666
3714
  json_data = json.loads(data_str)
3667
3715
  if isinstance(json_data, list):
3668
3716
  for tick_data in json_data:
3717
+ if not is_market_feed_payload(tick_data):
3718
+ continue
3669
3719
  price = map_to_price(tick_data)
3670
3720
  if price is not None:
3671
3721
  self._last_stream_tick_ts = time.time()
3672
3722
  if price is not None and ext_callback:
3673
3723
  ext_callback(price)
3674
3724
  elif isinstance(json_data, dict):
3725
+ if not is_market_feed_payload(json_data):
3726
+ return
3675
3727
  price = map_to_price(json_data)
3676
3728
  if price is not None:
3677
3729
  self._last_stream_tick_ts = time.time()
@@ -1608,7 +1608,7 @@ class FlatTrade(BrokerBase):
1608
1608
  except (ValueError, TypeError):
1609
1609
  date_matches = False
1610
1610
  if date_matches:
1611
- fills = self.get_order_info(broker_order_id=broker_order_id)
1611
+ fills = self.get_order_info(broker_order_id=broker_order_id, resolve_terminal=kwargs.get("resolve_terminal", False))
1612
1612
  if fills.fill_size < round(float(order.quantity)):
1613
1613
  trading_logger.log_info(
1614
1614
  "Cancelling broker order",
@@ -1644,6 +1644,7 @@ class FlatTrade(BrokerBase):
1644
1644
  raise BrokerConnectionError("FlatTrade API is not initialized")
1645
1645
 
1646
1646
  trading_logger.log_debug("Getting order info", {"broker_order_id": kwargs.get("broker_order_id")})
1647
+ resolve_terminal = kwargs.get("resolve_terminal", True)
1647
1648
 
1648
1649
  def return_db_as_fills(order: Order) -> OrderInfo:
1649
1650
  order_info = OrderInfo()
@@ -1728,6 +1729,9 @@ class FlatTrade(BrokerBase):
1728
1729
  order_info.status = status_mapping[latest_status.get("status")]
1729
1730
  else:
1730
1731
  order_info.status = OrderStatus.UNDEFINED
1732
+ if resolve_terminal:
1733
+ terminal_message = latest_status.get("rejreason") or latest_status.get("emsg") or order.message
1734
+ order_info = self._apply_broker_side_terminal_resolution(order, order_info, message=terminal_message)
1731
1735
  return order_info
1732
1736
 
1733
1737
  # ------------------------------------------------------------------
@@ -2456,8 +2456,26 @@ class IciciDirect(BrokerBase):
2456
2456
  )
2457
2457
  status_raw = str(rec.get("status", rec.get("order_status", "")))
2458
2458
  status = self._normalize_order_status(status_raw, fill_size, order_size, exchange_order_id)
2459
+ terminal_message = str(
2460
+ rec.get("reason", rec.get("remarks", rec.get("message", rec.get("reject_reason", ""))))
2461
+ or ""
2462
+ )
2459
2463
 
2460
- return OrderInfo(
2464
+ order = kwargs.get("order")
2465
+ if order is None:
2466
+ order_data = self.redis_o.hgetall(order_id) if hasattr(self, "redis_o") else {}
2467
+ if order_data:
2468
+ order = Order(**order_data)
2469
+ else:
2470
+ order = Order(
2471
+ broker_order_id=order_id,
2472
+ broker=self.broker.name,
2473
+ exch_order_id=exchange_order_id,
2474
+ )
2475
+ if terminal_message:
2476
+ order.message = terminal_message
2477
+
2478
+ order_info = OrderInfo(
2461
2479
  order_size=order_size,
2462
2480
  order_price=order_price,
2463
2481
  fill_size=fill_size,
@@ -2467,6 +2485,11 @@ class IciciDirect(BrokerBase):
2467
2485
  exchange_order_id=exchange_order_id,
2468
2486
  broker=self.broker,
2469
2487
  )
2488
+ if kwargs.get("resolve_terminal", True):
2489
+ order_info = self._apply_broker_side_terminal_resolution(
2490
+ order, order_info, message=terminal_message or order.message
2491
+ )
2492
+ return order_info
2470
2493
  except (ValidationError, BrokerConnectionError, OrderError):
2471
2494
  raise
2472
2495
  except Exception as e:
@@ -1724,7 +1724,7 @@ class Shoonya(BrokerBase):
1724
1724
  except (ValueError, TypeError):
1725
1725
  date_matches = False
1726
1726
  if date_matches:
1727
- fills = self.get_order_info(broker_order_id=broker_order_id)
1727
+ fills = self.get_order_info(broker_order_id=broker_order_id, resolve_terminal=kwargs.get("resolve_terminal", False))
1728
1728
  if fills.fill_size < round(float(order.quantity)):
1729
1729
  trading_logger.log_info(
1730
1730
  "Cancelling broker order",
@@ -1760,6 +1760,7 @@ class Shoonya(BrokerBase):
1760
1760
  raise BrokerConnectionError("Shoonya API is not initialized")
1761
1761
 
1762
1762
  trading_logger.log_debug("Getting order info", {"broker_order_id": kwargs.get("broker_order_id")})
1763
+ resolve_terminal = kwargs.get("resolve_terminal", True)
1763
1764
 
1764
1765
  def return_db_as_fills(order: Order) -> OrderInfo:
1765
1766
  order_info = OrderInfo()
@@ -1844,6 +1845,9 @@ class Shoonya(BrokerBase):
1844
1845
  order_info.status = status_mapping[latest_status.get("status")]
1845
1846
  else:
1846
1847
  order_info.status = OrderStatus.UNDEFINED
1848
+ if resolve_terminal:
1849
+ terminal_message = latest_status.get("rejreason") or latest_status.get("emsg") or order.message
1850
+ order_info = self._apply_broker_side_terminal_resolution(order, order_info, message=terminal_message)
1847
1851
  return order_info
1848
1852
 
1849
1853
  # ------------------------------------------------------------------
@@ -27,7 +27,17 @@ from selenium.webdriver.common.keys import Keys
27
27
  from selenium.webdriver.support import expected_conditions as EC
28
28
  from selenium.webdriver.support.ui import WebDriverWait
29
29
 
30
- from .broker_base import BrokerBase, HistoricalData, Order, OrderInfo, OrderStatus, Position, Price
30
+ from .broker_base import (
31
+ BrokerBase,
32
+ HistoricalData,
33
+ Order,
34
+ OrderInfo,
35
+ OrderStatus,
36
+ Position,
37
+ Price,
38
+ is_broker_side_terminal_order,
39
+ is_missing_exchange_order_id,
40
+ )
31
41
  from .config import get_config, get_fno_freeze_limit
32
42
  from .exceptions import (
33
43
  SymbolError,
@@ -692,10 +702,8 @@ def get_pnl_table(
692
702
  # Refresh status if requested
693
703
  if refresh_status:
694
704
  try:
695
- for entry_key in entry_keys:
696
- order = Order(**cast(dict[str, Any], broker.redis_o.hgetall(entry_key)))
697
- broker_order_id = order.broker_order_id
698
- update_order_status(broker, int_order_id, broker_order_id, eod=eod)
705
+ _prune_terminal_entry_legs(broker, int_order_id, eod=eod)
706
+ symbol = cast(str, broker.redis_o.hget(int_order_id, "long_symbol") or symbol)
699
707
  except Exception as e:
700
708
  trading_logger.log_error(
701
709
  f"Error refreshing entry status for {int_order_id}",
@@ -721,9 +729,9 @@ def get_pnl_table(
721
729
  for leg_symbol, expected_qty in expected_legs.items()
722
730
  )
723
731
  if not fully_filled:
724
- derived_symbol = _build_effective_symbol_from_positions(entry_position)
725
- if derived_symbol:
726
- effective_symbol = derived_symbol
732
+ resolved_symbol = _resolve_effective_combo_symbol(symbol, entry_position)
733
+ if resolved_symbol:
734
+ effective_symbol = resolved_symbol
727
735
  if "?" not in effective_symbol and entry_order_type:
728
736
  side = entry_order_type
729
737
  base_position = parse_combo_symbol(effective_symbol)
@@ -1300,6 +1308,38 @@ def _build_effective_symbol_from_positions(positions: Dict[str, Position]) -> st
1300
1308
  return ":".join(f"{sym}?{int(size / norm)}" for sym, size in non_zero)
1301
1309
 
1302
1310
 
1311
+ def _resolve_effective_combo_symbol(
1312
+ original_combo: str,
1313
+ entry_position: Dict[str, Position],
1314
+ ) -> str:
1315
+ """Prefer subset of original combo with preserved leg ratios; GCD infer only as fallback."""
1316
+ active_legs = {sym for sym, pos in entry_position.items() if pos.size != 0}
1317
+ if not active_legs:
1318
+ return original_combo
1319
+ if "?" not in original_combo:
1320
+ if original_combo in active_legs and len(active_legs) == 1:
1321
+ return original_combo
1322
+ if len(active_legs) == 1:
1323
+ return next(iter(active_legs))
1324
+ return _build_effective_symbol_from_positions(entry_position)
1325
+
1326
+ try:
1327
+ original_legs = parse_combo_symbol(original_combo)
1328
+ except Exception:
1329
+ return _build_effective_symbol_from_positions(entry_position)
1330
+
1331
+ remaining_parts = [
1332
+ f"{leg_sym}?{ratio}" for leg_sym, ratio in original_legs.items() if leg_sym in active_legs
1333
+ ]
1334
+ if not remaining_parts:
1335
+ return _build_effective_symbol_from_positions(entry_position)
1336
+ if len(remaining_parts) == len(original_legs):
1337
+ return original_combo
1338
+ if len(remaining_parts) == 1:
1339
+ return remaining_parts[0]
1340
+ return ":".join(remaining_parts)
1341
+
1342
+
1303
1343
  @log_execution_time
1304
1344
  @validate_inputs(combo_symbol=lambda x: isinstance(x, str) and len(x.strip()) > 0)
1305
1345
  def parse_combo_symbol(combo_symbol):
@@ -1639,6 +1679,8 @@ def transmit_entry_order(
1639
1679
  temp_order.price = lmt_price
1640
1680
  out = broker.place_order(temp_order)
1641
1681
  int_order_id = _process_broker_order_update(broker, out, order.long_symbol)
1682
+ if int_order_id:
1683
+ _prune_terminal_entry_legs(broker, int_order_id)
1642
1684
  return int_order_id
1643
1685
 
1644
1686
 
@@ -1878,6 +1920,66 @@ def exit_is_expiration(broker: BrokerBase, internal_order_id: str) -> bool:
1878
1920
  return out
1879
1921
 
1880
1922
 
1923
+ def _order_info_fill_size(fills: OrderInfo) -> float:
1924
+ try:
1925
+ return float(fills.fill_size or 0)
1926
+ except (TypeError, ValueError):
1927
+ return 0.0
1928
+
1929
+
1930
+ def should_prune_terminal_zero_fill_leg(
1931
+ fills: OrderInfo,
1932
+ *,
1933
+ redis_order: Optional[Order] = None,
1934
+ ) -> bool:
1935
+ """True when a zero-fill leg is terminal and safe to remove from entry_keys."""
1936
+ if _order_info_fill_size(fills) > 0:
1937
+ return False
1938
+ if fills.status in (OrderStatus.CANCELLED, OrderStatus.REJECTED):
1939
+ return True
1940
+ message = str(getattr(fills, "message", "") or "")
1941
+ exch_order_id = getattr(fills, "exchange_order_id", "")
1942
+ if redis_order is not None:
1943
+ if not message.strip():
1944
+ message = str(redis_order.message or "")
1945
+ if is_missing_exchange_order_id(exch_order_id):
1946
+ exch_order_id = redis_order.exch_order_id
1947
+ return is_broker_side_terminal_order(exch_order_id, message)
1948
+
1949
+
1950
+ def _sync_internal_order_long_symbol(broker: BrokerBase, internal_order_id: str) -> None:
1951
+ """Rewrite parent long_symbol to match remaining filled entry legs after pruning."""
1952
+ if not internal_order_id or not cast(bool, broker.redis_o.exists(internal_order_id)):
1953
+ return
1954
+ long_symbol = cast(str, broker.redis_o.hget(internal_order_id, "long_symbol") or "")
1955
+ if "?" not in long_symbol:
1956
+ return
1957
+ entry_keys = hget_with_default(broker, internal_order_id, "entry_keys", "").split()
1958
+ if not entry_keys or not any(k.strip() for k in entry_keys):
1959
+ return
1960
+ positions = get_open_position_by_order(broker, internal_order_id, exclude_zero=True, side=["entry"])
1961
+ resolved = _resolve_effective_combo_symbol(long_symbol, positions)
1962
+ if resolved and resolved != long_symbol:
1963
+ cast(bool, broker.redis_o.hset(internal_order_id, "long_symbol", resolved))
1964
+ trading_logger.log_info(
1965
+ "Updated internal order long_symbol after terminal leg prune",
1966
+ {"internal_order_id": internal_order_id, "old_symbol": long_symbol, "new_symbol": resolved},
1967
+ )
1968
+
1969
+
1970
+ def _prune_terminal_entry_legs(broker: BrokerBase, internal_order_id: str, *, eod: bool = False) -> None:
1971
+ """Refresh entry legs and delete zero-fill terminal legs; sync combo long_symbol."""
1972
+ if not internal_order_id or not cast(bool, broker.redis_o.exists(internal_order_id)):
1973
+ return
1974
+ entry_keys = hget_with_default(broker, internal_order_id, "entry_keys", "").split()
1975
+ for entry_key in list(entry_keys):
1976
+ if not entry_key or not cast(bool, broker.redis_o.exists(entry_key)):
1977
+ continue
1978
+ order = Order(**cast(dict[str, Any], broker.redis_o.hgetall(entry_key)))
1979
+ update_order_status(broker, internal_order_id, str(order.broker_order_id), eod=eod)
1980
+ _sync_internal_order_long_symbol(broker, internal_order_id)
1981
+
1982
+
1881
1983
  def delete_broker_order_id(
1882
1984
  broker,
1883
1985
  internal_order_id: str,
@@ -1978,8 +2080,20 @@ def update_order_status(
1978
2080
  if not broker_order_id_str or broker_order_id_str == "0" or broker_order_id_str.upper().endswith("P"):
1979
2081
  return None
1980
2082
 
2083
+ redis_order: Optional[Order] = None
1981
2084
  try:
1982
- fills = broker.get_order_info(broker_order_id=broker_order_id_str)
2085
+ redis_order_data = cast(dict[str, Any], broker.redis_o.hgetall(broker_order_id_str))
2086
+ if redis_order_data:
2087
+ redis_order = Order(**redis_order_data)
2088
+ except Exception:
2089
+ redis_order = None
2090
+
2091
+ get_order_info_kwargs: dict[str, Any] = {"broker_order_id": broker_order_id_str}
2092
+ if redis_order is not None:
2093
+ get_order_info_kwargs["order"] = redis_order
2094
+
2095
+ try:
2096
+ fills = broker.get_order_info(**get_order_info_kwargs)
1983
2097
  except Exception as e:
1984
2098
  trading_logger.log_error(
1985
2099
  "Error calling get_order_info in update_order_status",
@@ -2006,22 +2120,22 @@ def update_order_status(
2006
2120
  )
2007
2121
  if attr == "exchange_order_id":
2008
2122
  trading_logger.log_info(msg)
2009
- else:
2010
- trading_logger.log_error(msg)
2123
+ continue
2124
+ trading_logger.log_error(msg)
2011
2125
  return fills
2012
2126
 
2013
2127
  if broker.broker != fills.broker:
2014
2128
  return fills
2015
2129
 
2016
- if fills.status == OrderStatus.CANCELLED and fills.fill_size > 0:
2130
+ if fills.status == OrderStatus.CANCELLED and _order_info_fill_size(fills) > 0:
2017
2131
  broker.redis_o.hset(broker_order_id, "price", str(fills.fill_price))
2018
2132
  broker.redis_o.hset(broker_order_id, "quantity", str(fills.fill_size))
2019
2133
  broker.redis_o.hset(broker_order_id, "status", fills.status.name)
2020
2134
  broker.redis_o.hset(broker_order_id, "exch_order_id", fills.exchange_order_id)
2021
- elif (fills.status == OrderStatus.CANCELLED and fills.fill_size == 0) or (fills.status == OrderStatus.REJECTED):
2135
+ elif should_prune_terminal_zero_fill_leg(fills, redis_order=redis_order):
2022
2136
  delete_broker_order_id(broker, internal_order_id, broker_order_id)
2023
2137
  elif eod:
2024
- if fills.fill_size > 0:
2138
+ if _order_info_fill_size(fills) > 0:
2025
2139
  broker.redis_o.hset(broker_order_id, "price", str(fills.fill_price))
2026
2140
  broker.redis_o.hset(broker_order_id, "quantity", str(fills.fill_size))
2027
2141
  broker.redis_o.hset(broker_order_id, "status", fills.status.name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradingapi
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: Trade integration with brokers
5
5
  Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,6 @@
1
1
  README.md
2
2
  pyproject.toml
3
+ tests/test_broker_side_terminal_order.py
3
4
  tests/test_calculate_delta_realtime_quotes.py
4
5
  tests/test_find_option_with_delta.py
5
6
  tradingapi/__init__.py
File without changes
File without changes