tradingapi 0.3.8__tar.gz → 0.3.10__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 (34) hide show
  1. {tradingapi-0.3.8 → tradingapi-0.3.10}/PKG-INFO +1 -1
  2. {tradingapi-0.3.8 → tradingapi-0.3.10}/pyproject.toml +1 -1
  3. tradingapi-0.3.10/tests/test_broker_side_terminal_order.py +30 -0
  4. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/broker_base.py +130 -0
  5. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/config/config_sample.yaml +7 -1
  6. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/dhan.py +48 -17
  7. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/fivepaisa.py +227 -100
  8. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/flattrade.py +24 -3
  9. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/icicidirect.py +34 -4
  10. tradingapi-0.3.10/tradingapi/proxy_utils.py +294 -0
  11. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/shoonya.py +22 -3
  12. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/utils.py +129 -14
  13. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi.egg-info/PKG-INFO +1 -1
  14. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi.egg-info/SOURCES.txt +1 -0
  15. tradingapi-0.3.8/tradingapi/proxy_utils.py +0 -130
  16. {tradingapi-0.3.8 → tradingapi-0.3.10}/README.md +0 -0
  17. {tradingapi-0.3.8 → tradingapi-0.3.10}/setup.cfg +0 -0
  18. {tradingapi-0.3.8 → tradingapi-0.3.10}/tests/test_calculate_delta_realtime_quotes.py +0 -0
  19. {tradingapi-0.3.8 → tradingapi-0.3.10}/tests/test_find_option_with_delta.py +0 -0
  20. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/__init__.py +0 -0
  21. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/allocation.py +0 -0
  22. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/attribution.py +0 -0
  23. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/config/commissions_20241216.yaml +0 -0
  24. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/config.py +0 -0
  25. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/error_handling.py +0 -0
  26. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/exceptions.py +0 -0
  27. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/globals.py +0 -0
  28. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/icicidirect_generate_session.py +0 -0
  29. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/market_data_exchanges.py +0 -0
  30. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi/span.py +0 -0
  31. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi.egg-info/dependency_links.txt +0 -0
  32. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi.egg-info/entry_points.txt +0 -0
  33. {tradingapi-0.3.8 → tradingapi-0.3.10}/tradingapi.egg-info/requires.txt +0 -0
  34. {tradingapi-0.3.8 → tradingapi-0.3.10}/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.8
3
+ Version: 0.3.10
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.8"
31
+ version = "0.3.10"
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.
@@ -528,6 +571,7 @@ class Price:
528
571
  high: float = float("nan"),
529
572
  low: float = float("nan"),
530
573
  volume: int = 0,
574
+ oi: int = 0,
531
575
  symbol: str = "",
532
576
  exchange: str = "",
533
577
  src: str = "",
@@ -546,6 +590,7 @@ class Price:
546
590
  high: High price
547
591
  low: Low price
548
592
  volume: Total volume
593
+ oi: Open interest
549
594
  symbol: Trading symbol
550
595
  exchange: Exchange name
551
596
  src: Source of the price data
@@ -563,6 +608,7 @@ class Price:
563
608
  self.high = high
564
609
  self.low = low
565
610
  self.volume = volume
611
+ self.oi = oi
566
612
  self.symbol = symbol
567
613
  self.exchange = exchange
568
614
  self.src = src
@@ -590,6 +636,7 @@ class Price:
590
636
  high=safe_add(self.high, other.high),
591
637
  low=safe_add(self.low, other.low),
592
638
  volume=safe_add_volume(self.volume, other.volume),
639
+ oi=safe_add_volume(self.oi, other.oi),
593
640
  )
594
641
  # dont change symbol
595
642
 
@@ -603,6 +650,7 @@ class Price:
603
650
  self.high = other.high * size if other.high * size is not float("nan") else self.high
604
651
  self.low = other.low * size if other.low * size is not float("nan") else self.low
605
652
  self.volume = other.volume if other.volume is not float("nan") else self.volume
653
+ self.oi = other.oi if other.oi is not float("nan") else self.oi
606
654
  self.symbol = other.symbol
607
655
  self.exchange = other.exchange
608
656
  self.src = other.src
@@ -619,6 +667,7 @@ class Price:
619
667
  "high": self.high,
620
668
  "low": self.low,
621
669
  "volume": self.volume,
670
+ "oi": self.oi,
622
671
  "symbol": self.symbol,
623
672
  "exchange": self.exchange,
624
673
  "src": self.src,
@@ -637,6 +686,7 @@ class Price:
637
686
  high=data.get("high", float("nan")),
638
687
  low=data.get("low", float("nan")),
639
688
  volume=data.get("volume", 0),
689
+ oi=data.get("oi", 0),
640
690
  symbol=data.get("symbol", ""),
641
691
  exchange=data.get("exchange", ""),
642
692
  src=data.get("src", ""),
@@ -756,6 +806,86 @@ class BrokerBase(ABC):
756
806
  "symbol_map_reversed": {},
757
807
  }
758
808
 
809
+ def _request_cancel_broker_side_terminal_order(self, order: "Order") -> bool:
810
+ """Send an explicit broker cancel when the order may still be live."""
811
+ broker_order_id = str(getattr(order, "broker_order_id", "") or "").strip()
812
+ if not broker_order_id or broker_order_id == "0" or broker_order_id.upper().endswith("P"):
813
+ return False
814
+ if getattr(order, "paper", False) in (True, "True", "true"):
815
+ return False
816
+ try:
817
+ self.cancel_order(broker_order_id=broker_order_id, resolve_terminal=False)
818
+ return True
819
+ except Exception as e:
820
+ _get_trading_logger().log_warning(
821
+ "Cancel request failed for broker-side terminal order",
822
+ {"broker_order_id": broker_order_id, "error": str(e)},
823
+ )
824
+ return False
825
+
826
+ def _resolve_broker_side_terminal_order(
827
+ self,
828
+ order: "Order",
829
+ message: str,
830
+ *,
831
+ order_size=None,
832
+ order_price=None,
833
+ ) -> OrderInfo:
834
+ """Cancel if possible and return a zero-fill CANCELLED OrderInfo."""
835
+ broker_order_id = str(getattr(order, "broker_order_id", "") or "").strip()
836
+ if message:
837
+ order.message = message
838
+ if broker_order_id:
839
+ try:
840
+ self.redis_o.hset(broker_order_id, "message", message)
841
+ except Exception:
842
+ pass
843
+
844
+ cancel_requested = self._request_cancel_broker_side_terminal_order(order)
845
+ order.status = OrderStatus.CANCELLED
846
+ if broker_order_id:
847
+ try:
848
+ self.redis_o.hset(broker_order_id, "status", OrderStatus.CANCELLED.name)
849
+ except Exception:
850
+ pass
851
+
852
+ _get_trading_logger().log_info(
853
+ "Broker-side terminal order resolved as cancelled",
854
+ {
855
+ "broker_order_id": broker_order_id,
856
+ "long_symbol": getattr(order, "long_symbol", ""),
857
+ "message": message,
858
+ "cancel_requested": cancel_requested,
859
+ },
860
+ )
861
+ return OrderInfo(
862
+ order_size=order_size if order_size is not None else order.quantity,
863
+ order_price=order_price if order_price is not None else order.price,
864
+ fill_size=0,
865
+ fill_price=0,
866
+ status=OrderStatus.CANCELLED,
867
+ broker_order_id=broker_order_id,
868
+ exchange_order_id=getattr(order, "exch_order_id", ""),
869
+ broker=getattr(order, "broker", None) or self.broker,
870
+ )
871
+
872
+ def _apply_broker_side_terminal_resolution(
873
+ self,
874
+ order: "Order",
875
+ order_info: OrderInfo,
876
+ message: Optional[str] = None,
877
+ ) -> OrderInfo:
878
+ msg = str(message if message is not None else getattr(order, "message", "") or "").strip()
879
+ exch_id = order_info.exchange_order_id or getattr(order, "exch_order_id", "")
880
+ if not is_broker_side_terminal_order(exch_id, msg):
881
+ return order_info
882
+ return self._resolve_broker_side_terminal_order(
883
+ order,
884
+ msg,
885
+ order_size=order_info.order_size,
886
+ order_price=order_info.order_price,
887
+ )
888
+
759
889
  @abstractmethod
760
890
  def update_symbology(self, **kwargs):
761
891
  """
@@ -2,13 +2,14 @@
2
2
  bhavcopy_folder : /home/psharma/onedrive/rfiles/data/bhavcopy
3
3
 
4
4
  # Optional: proxy for broker HTTP requests (e.g. connect, save_symbol_data).
5
- # When a broker has USE_PROXY: true, proxies are fetched from the source (e.g. webshare).
5
+ # USE_PROXY: trading API calls. USE_PROXY_SYMBOL_DOWNLOAD: symbol master download only.
6
6
  proxy:
7
7
  source: webshare
8
8
  api_key: # Webshare API key (or set WEBSHARE_PROXY_API_KEY env)
9
9
  username: # Optional: override per-proxy username from API
10
10
  password: # Optional: override per-proxy password from API
11
11
  country_code: # Optional: e.g. IN
12
+ max_proxies: 30 # NordVPN servers to try when rotating (symbol download)
12
13
  mode: direct
13
14
 
14
15
  commissions:
@@ -22,6 +23,7 @@ commissions:
22
23
  FIVEPAISA:
23
24
  EXCHANGES: [NSE, BSE, MCX]
24
25
  USE_PROXY: false
26
+ USE_PROXY_SYMBOL_DOWNLOAD: false
25
27
  APP_NAME:
26
28
  APP_SOURCE:
27
29
  USER_ID:
@@ -41,6 +43,7 @@ FIVEPAISA:
41
43
  SHOONYA:
42
44
  EXCHANGES: [NSE, BSE, MCX]
43
45
  USE_PROXY: false
46
+ USE_PROXY_SYMBOL_DOWNLOAD: false
44
47
  USER:
45
48
  PWD:
46
49
  VC:
@@ -56,6 +59,7 @@ SHOONYA:
56
59
  ICICIDIRECT:
57
60
  EXCHANGES: [NSE, BSE, MCX]
58
61
  USE_PROXY: false
62
+ USE_PROXY_SYMBOL_DOWNLOAD: false
59
63
  API_KEY:
60
64
  API_SECRET:
61
65
  USER_ID: # Optional: used by icicidirect-generate-session (AUTO_SESSION_TOKEN_CMD)
@@ -91,6 +95,7 @@ ICICIDIRECT:
91
95
  DHAN:
92
96
  EXCHANGES: [NSE, BSE]
93
97
  USE_PROXY: false
98
+ USE_PROXY_SYMBOL_DOWNLOAD: false
94
99
  CLIENT_ID:
95
100
  ACCESS_TOKEN: # Optional fallback if TOTP/PIN flow is not used
96
101
  TOTP_TOKEN: # Optional: used for auto token refresh
@@ -107,6 +112,7 @@ DHAN:
107
112
  DHAN_ACCOUNT2:
108
113
  EXCHANGES: [NSE, BSE]
109
114
  USE_PROXY: false
115
+ USE_PROXY_SYMBOL_DOWNLOAD: false
110
116
  CLIENT_ID:
111
117
  ACCESS_TOKEN:
112
118
  TOTP_TOKEN:
@@ -287,11 +287,21 @@ def save_symbol_data(saveToFolder: bool = False):
287
287
  dest_file = f"{bhavcopyfolder}/{dt.datetime.today().strftime('%Y%m%d')}_dhan_codes.csv"
288
288
 
289
289
  headers = {"User-Agent": "Mozilla/5.0", "Accept": "*/*"}
290
- response = requests.get(DHAN_SECURITY_LIST_URL, headers=headers, timeout=(10, 300))
291
- if response.status_code != 200:
292
- raise Exception(f"Failed to fetch Dhan symbol data. Status: {response.status_code}")
293
-
294
- df = pd.read_csv(io.BytesIO(response.content), low_memory=False)
290
+ if os.path.exists(dest_file):
291
+ df = pd.read_csv(dest_file, low_memory=False)
292
+ else:
293
+ from .proxy_utils import request_get_with_broker_proxy
294
+
295
+ response = request_get_with_broker_proxy(
296
+ DHAN_SECURITY_LIST_URL,
297
+ "DHAN",
298
+ purpose="symbol_download",
299
+ headers=headers,
300
+ timeout=(10, 300),
301
+ )
302
+ with open(dest_file, "wb") as f:
303
+ f.write(response.content)
304
+ df = pd.read_csv(dest_file, low_memory=False)
295
305
  df.columns = [col.strip() for col in df.columns]
296
306
  object_cols = df.select_dtypes(include=["object"]).columns
297
307
  for col in object_cols:
@@ -1476,7 +1486,11 @@ class Dhan(BrokerBase):
1476
1486
 
1477
1487
  fills = None
1478
1488
  try:
1479
- fills = self.get_order_info(broker_order_id=broker_order_id, order=order)
1489
+ fills = self.get_order_info(
1490
+ broker_order_id=broker_order_id,
1491
+ order=order,
1492
+ resolve_terminal=kwargs.get("resolve_terminal", False),
1493
+ )
1480
1494
  except Exception:
1481
1495
  pass
1482
1496
 
@@ -1617,15 +1631,7 @@ class Dhan(BrokerBase):
1617
1631
  if order is not None and error_message:
1618
1632
  order.message = error_message
1619
1633
 
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(
1634
+ order_info = OrderInfo(
1629
1635
  order_size=order_qty,
1630
1636
  order_price=order_price,
1631
1637
  fill_size=fill_size,
@@ -1635,6 +1641,20 @@ class Dhan(BrokerBase):
1635
1641
  exchange_order_id=exch_order_id,
1636
1642
  broker=self.broker,
1637
1643
  )
1644
+ if kwargs.get("resolve_terminal", True):
1645
+ order_info = self._apply_broker_side_terminal_resolution(
1646
+ order, order_info, message=error_message or order.message
1647
+ )
1648
+
1649
+ if order_info.status == OrderStatus.REJECTED:
1650
+ try:
1651
+ internal_order_id = self.redis_o.hget(broker_order_id, "orderRef")
1652
+ if internal_order_id:
1653
+ delete_broker_order_id(self, internal_order_id, broker_order_id)
1654
+ except Exception:
1655
+ pass
1656
+
1657
+ return order_info
1638
1658
 
1639
1659
  except (ValidationError, OrderError, BrokerConnectionError):
1640
1660
  raise
@@ -1975,6 +1995,11 @@ class Dhan(BrokerBase):
1975
1995
  market_feed.low = float(ohlc.get("low", data.get("dayLow", float("nan"))) or float("nan"))
1976
1996
  market_feed.prior_close = float(ohlc.get("close", data.get("previousClosePrice", float("nan"))) or float("nan"))
1977
1997
  market_feed.volume = int(data.get("volume", data.get("totalTradedVolume", 0)) or 0)
1998
+ raw_oi = data.get("OI", data.get("oi", data.get("open_interest", data.get("openInterest", 0))))
1999
+ try:
2000
+ market_feed.oi = int(str(raw_oi).strip() or 0)
2001
+ except (TypeError, ValueError):
2002
+ market_feed.oi = int(float(raw_oi or 0))
1978
2003
  depth = data.get("depth", {})
1979
2004
  buy_qty = depth.get("buy", [{}])
1980
2005
  sell_qty = depth.get("sell", [{}])
@@ -2330,9 +2355,12 @@ class Dhan(BrokerBase):
2330
2355
  if value in (None, "", "nan"):
2331
2356
  return 0
2332
2357
  try:
2333
- return int(float(value))
2358
+ return int(str(value).strip())
2334
2359
  except (TypeError, ValueError):
2335
- return 0
2360
+ try:
2361
+ return int(float(value))
2362
+ except (TypeError, ValueError):
2363
+ return 0
2336
2364
 
2337
2365
  def _apply_if_valid(current: float, value: Any) -> float:
2338
2366
  parsed = _to_float(value)
@@ -2381,6 +2409,9 @@ class Dhan(BrokerBase):
2381
2409
  volume = _to_int(response.get("volume"))
2382
2410
  if volume:
2383
2411
  price.volume = volume
2412
+ oi = _to_int(response.get("OI") or response.get("oi") or response.get("open_interest") or response.get("openInterest"))
2413
+ if oi:
2414
+ price.oi = oi
2384
2415
  price.bid = _apply_if_valid(price.bid, response.get("bidPrice"))
2385
2416
  price.ask = _apply_if_valid(price.ask, response.get("askPrice"))
2386
2417
  bid_qty = _to_int(response.get("bidQty"))