tradingapi 0.3.9__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.9 → tradingapi-0.3.10}/PKG-INFO +1 -1
  2. {tradingapi-0.3.9 → tradingapi-0.3.10}/pyproject.toml +1 -1
  3. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/broker_base.py +7 -0
  4. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/config/config_sample.yaml +7 -1
  5. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/dhan.py +28 -7
  6. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/fivepaisa.py +119 -32
  7. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/flattrade.py +19 -2
  8. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/icicidirect.py +10 -3
  9. tradingapi-0.3.10/tradingapi/proxy_utils.py +294 -0
  10. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/shoonya.py +17 -2
  11. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/utils.py +1 -0
  12. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/PKG-INFO +1 -1
  13. tradingapi-0.3.9/tradingapi/proxy_utils.py +0 -130
  14. {tradingapi-0.3.9 → tradingapi-0.3.10}/README.md +0 -0
  15. {tradingapi-0.3.9 → tradingapi-0.3.10}/setup.cfg +0 -0
  16. {tradingapi-0.3.9 → tradingapi-0.3.10}/tests/test_broker_side_terminal_order.py +0 -0
  17. {tradingapi-0.3.9 → tradingapi-0.3.10}/tests/test_calculate_delta_realtime_quotes.py +0 -0
  18. {tradingapi-0.3.9 → tradingapi-0.3.10}/tests/test_find_option_with_delta.py +0 -0
  19. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/__init__.py +0 -0
  20. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/allocation.py +0 -0
  21. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/attribution.py +0 -0
  22. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/config/commissions_20241216.yaml +0 -0
  23. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/config.py +0 -0
  24. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/error_handling.py +0 -0
  25. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/exceptions.py +0 -0
  26. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/globals.py +0 -0
  27. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/icicidirect_generate_session.py +0 -0
  28. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/market_data_exchanges.py +0 -0
  29. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/span.py +0 -0
  30. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/SOURCES.txt +0 -0
  31. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/dependency_links.txt +0 -0
  32. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/entry_points.txt +0 -0
  33. {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/requires.txt +0 -0
  34. {tradingapi-0.3.9 → 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.9
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.9"
31
+ version = "0.3.10"
32
32
  description = "Trade integration with brokers"
33
33
  readme = "README.md"
34
34
  license = "MIT"
@@ -571,6 +571,7 @@ class Price:
571
571
  high: float = float("nan"),
572
572
  low: float = float("nan"),
573
573
  volume: int = 0,
574
+ oi: int = 0,
574
575
  symbol: str = "",
575
576
  exchange: str = "",
576
577
  src: str = "",
@@ -589,6 +590,7 @@ class Price:
589
590
  high: High price
590
591
  low: Low price
591
592
  volume: Total volume
593
+ oi: Open interest
592
594
  symbol: Trading symbol
593
595
  exchange: Exchange name
594
596
  src: Source of the price data
@@ -606,6 +608,7 @@ class Price:
606
608
  self.high = high
607
609
  self.low = low
608
610
  self.volume = volume
611
+ self.oi = oi
609
612
  self.symbol = symbol
610
613
  self.exchange = exchange
611
614
  self.src = src
@@ -633,6 +636,7 @@ class Price:
633
636
  high=safe_add(self.high, other.high),
634
637
  low=safe_add(self.low, other.low),
635
638
  volume=safe_add_volume(self.volume, other.volume),
639
+ oi=safe_add_volume(self.oi, other.oi),
636
640
  )
637
641
  # dont change symbol
638
642
 
@@ -646,6 +650,7 @@ class Price:
646
650
  self.high = other.high * size if other.high * size is not float("nan") else self.high
647
651
  self.low = other.low * size if other.low * size is not float("nan") else self.low
648
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
649
654
  self.symbol = other.symbol
650
655
  self.exchange = other.exchange
651
656
  self.src = other.src
@@ -662,6 +667,7 @@ class Price:
662
667
  "high": self.high,
663
668
  "low": self.low,
664
669
  "volume": self.volume,
670
+ "oi": self.oi,
665
671
  "symbol": self.symbol,
666
672
  "exchange": self.exchange,
667
673
  "src": self.src,
@@ -680,6 +686,7 @@ class Price:
680
686
  high=data.get("high", float("nan")),
681
687
  low=data.get("low", float("nan")),
682
688
  volume=data.get("volume", 0),
689
+ oi=data.get("oi", 0),
683
690
  symbol=data.get("symbol", ""),
684
691
  exchange=data.get("exchange", ""),
685
692
  src=data.get("src", ""),
@@ -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:
@@ -1985,6 +1995,11 @@ class Dhan(BrokerBase):
1985
1995
  market_feed.low = float(ohlc.get("low", data.get("dayLow", float("nan"))) or float("nan"))
1986
1996
  market_feed.prior_close = float(ohlc.get("close", data.get("previousClosePrice", float("nan"))) or float("nan"))
1987
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))
1988
2003
  depth = data.get("depth", {})
1989
2004
  buy_qty = depth.get("buy", [{}])
1990
2005
  sell_qty = depth.get("sell", [{}])
@@ -2340,9 +2355,12 @@ class Dhan(BrokerBase):
2340
2355
  if value in (None, "", "nan"):
2341
2356
  return 0
2342
2357
  try:
2343
- return int(float(value))
2358
+ return int(str(value).strip())
2344
2359
  except (TypeError, ValueError):
2345
- return 0
2360
+ try:
2361
+ return int(float(value))
2362
+ except (TypeError, ValueError):
2363
+ return 0
2346
2364
 
2347
2365
  def _apply_if_valid(current: float, value: Any) -> float:
2348
2366
  parsed = _to_float(value)
@@ -2391,6 +2409,9 @@ class Dhan(BrokerBase):
2391
2409
  volume = _to_int(response.get("volume"))
2392
2410
  if volume:
2393
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
2394
2415
  price.bid = _apply_if_valid(price.bid, response.get("bidPrice"))
2395
2416
  price.ask = _apply_if_valid(price.ask, response.get("askPrice"))
2396
2417
  bid_qty = _to_int(response.get("bidQty"))
@@ -1,4 +1,5 @@
1
1
  import datetime as dt
2
+ import copy
2
3
  import inspect
3
4
  import io
4
5
  import json
@@ -157,7 +158,7 @@ def save_symbol_data(saveToFolder: bool = False):
157
158
  _proxies = None
158
159
  try:
159
160
  from .proxy_utils import get_proxies_for_broker
160
- _proxies = get_proxies_for_broker("FIVEPAISA")
161
+ _proxies = get_proxies_for_broker("FIVEPAISA", purpose="symbol_download")
161
162
  except Exception:
162
163
  pass
163
164
  headers = {
@@ -2020,7 +2021,8 @@ class FivePaisa(BrokerBase):
2020
2021
  if self.api is None:
2021
2022
  raise BrokerConnectionError("API client not initialized")
2022
2023
  out = self.api.cancel_order(exch_order_id=order.exch_order_id)
2023
- self.log_and_return(out)
2024
+ if out is not None:
2025
+ self.log_and_return(out)
2024
2026
 
2025
2027
  if out and "BrokerOrderID" in out:
2026
2028
  # Convert broker_order_id to string for consistency
@@ -3482,10 +3484,14 @@ class FivePaisa(BrokerBase):
3482
3484
  raise MarketDataError(f"Exchange mapping not found: {str(e)}", context)
3483
3485
 
3484
3486
  if scrip_code is None:
3487
+ symbol_map = self.exchange_mappings[mapped_exchange]["symbol_map"]
3488
+ symbol_prefix = str(long_symbol).split("_", 1)[0]
3489
+ matching_symbols = [symbol for symbol in symbol_map if str(symbol).startswith(f"{symbol_prefix}_")]
3485
3490
  context = create_error_context(
3486
3491
  long_symbol=long_symbol,
3487
3492
  mapped_exchange=mapped_exchange,
3488
- available_symbols=list(self.exchange_mappings[mapped_exchange]["symbol_map"].keys()),
3493
+ available_symbol_count=len(symbol_map),
3494
+ matching_symbol_sample=matching_symbols[:20],
3489
3495
  )
3490
3496
  trading_logger.log_warning("No scrip code found for symbol", context)
3491
3497
  return market_feed # Return default Price object if no scrip code is found
@@ -3495,11 +3501,32 @@ class FivePaisa(BrokerBase):
3495
3501
  ]
3496
3502
 
3497
3503
  try:
3498
- # Fetch market feed
3499
3504
  if self.api is None:
3500
3505
  raise BrokerConnectionError("API client not initialized")
3506
+
3507
+ def safe_float(value, default=float("nan")):
3508
+ if value in [None, "", 0, "0", "0.00", float("nan")]:
3509
+ return default
3510
+ try:
3511
+ return float(value)
3512
+ except (TypeError, ValueError):
3513
+ return default
3514
+
3515
+ def safe_int(value, default=0):
3516
+ if value in [None, "", 0, "0", "0.00", float("nan")]:
3517
+ return default
3518
+ try:
3519
+ return int(str(value).strip())
3520
+ except (TypeError, ValueError):
3521
+ try:
3522
+ return int(float(cast(Union[int, float, str], value)))
3523
+ except (TypeError, ValueError):
3524
+ return default
3525
+
3501
3526
  self._wait_for_quote_rate_limit()
3502
- out = self.api.fetch_market_feed_scrip(req_list)
3527
+ out = self.api.fetch_market_snapshot(
3528
+ [{"Exchange": mapped_exchange, "ExchangeType": exch_type, "ScripCode": str(scrip_code)}]
3529
+ )
3503
3530
  if not out or "Data" not in out or len(out["Data"]) == 0:
3504
3531
  context = create_error_context(
3505
3532
  long_symbol=long_symbol,
@@ -3507,7 +3534,7 @@ class FivePaisa(BrokerBase):
3507
3534
  scrip_code=scrip_code,
3508
3535
  api_response=out,
3509
3536
  )
3510
- raise MarketDataError("No market feed data received", context)
3537
+ raise MarketDataError("No market snapshot data received", context)
3511
3538
 
3512
3539
  snapshot = out["Data"][0]
3513
3540
 
@@ -3555,12 +3582,13 @@ class FivePaisa(BrokerBase):
3555
3582
  if best_ask_quantity is not None and best_ask_quantity > 0
3556
3583
  else market_feed.ask_volume
3557
3584
  )
3558
- market_feed.exchange = snapshot["Exch"]
3559
- market_feed.high = snapshot["High"] if snapshot["High"] > 0 else market_feed.high
3560
- market_feed.low = snapshot["Low"] if snapshot["Low"] > 0 else market_feed.low
3561
- market_feed.last = snapshot["LastRate"] if snapshot["LastRate"] > 0 else market_feed.last
3562
- market_feed.prior_close = snapshot["PClose"] if snapshot["PClose"] > 0 else market_feed.prior_close
3563
- market_feed.volume = snapshot["TotalQty"] if snapshot["TotalQty"] > 0 else market_feed.volume
3585
+ market_feed.exchange = snapshot.get("Exchange", mapped_exchange)
3586
+ market_feed.high = safe_float(snapshot.get("High"), market_feed.high)
3587
+ market_feed.low = safe_float(snapshot.get("Low"), market_feed.low)
3588
+ market_feed.last = safe_float(snapshot.get("LastTradedPrice"), market_feed.last)
3589
+ market_feed.prior_close = safe_float(snapshot.get("PClose"), market_feed.prior_close)
3590
+ market_feed.volume = safe_int(snapshot.get("Volume"), market_feed.volume)
3591
+ market_feed.oi = safe_int(snapshot.get("OpenInterest"), market_feed.oi)
3564
3592
  market_feed.timestamp = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
3565
3593
 
3566
3594
  trading_logger.log_debug(
@@ -3629,9 +3657,22 @@ class FivePaisa(BrokerBase):
3629
3657
  context = create_error_context(symbols=symbols, exchange=exchange, error=str(e))
3630
3658
  raise MarketDataError(f"Failed to map exchange: {str(e)}", context)
3631
3659
 
3660
+ prices: Dict[str, Price] = {}
3661
+
3662
+ def safe_int(value, default=0) -> int:
3663
+ if value in [None, "", 0, "0", "0.00", float("nan")]:
3664
+ return default
3665
+ try:
3666
+ return int(str(value).strip())
3667
+ except (TypeError, ValueError):
3668
+ try:
3669
+ return int(float(cast(Union[int, float, str], value)))
3670
+ except (TypeError, ValueError):
3671
+ return default
3672
+
3632
3673
  def resolve_symbol_and_exchange(json_data):
3633
3674
  """Resolve a token to the correct symbol/exchange even when the tick exchange key is incomplete."""
3634
- token = json_data.get("Token")
3675
+ token = json_data.get("Token", json_data.get("ScripCode"))
3635
3676
  try:
3636
3677
  token_int = int(token) if token is not None else None
3637
3678
  except (TypeError, ValueError):
@@ -3671,22 +3712,13 @@ class FivePaisa(BrokerBase):
3671
3712
  return False
3672
3713
  if data.get("ReqType"):
3673
3714
  return False
3674
- return any(k in data for k in ("Token", "LastRate", "BidRate", "TickDt"))
3715
+ return any(k in data for k in ("Token", "ScripCode", "LastRate", "BidRate", "TickDt", "OpenInterest"))
3675
3716
 
3676
3717
  def map_to_price(json_data):
3677
3718
  """Map JSON data to Price object."""
3678
3719
  try:
3679
3720
  price = Price()
3680
3721
  price.src = "fp"
3681
- price.bid = json_data.get("BidRate", float("nan"))
3682
- price.ask = json_data.get("OffRate", float("nan"))
3683
- price.bid_volume = json_data.get("BidQty", float("nan"))
3684
- price.ask_volume = json_data.get("OffQty", float("nan"))
3685
- price.last = json_data.get("LastRate", float("nan"))
3686
- price.prior_close = json_data.get("PClose", float("nan"))
3687
- price.high = json_data.get("High", float("nan"))
3688
- price.low = json_data.get("Low", float("nan"))
3689
- price.volume = json_data.get("TotalQty", float("nan"))
3690
3722
  price.symbol, resolved_exchange = resolve_symbol_and_exchange(json_data)
3691
3723
  if price.symbol is None:
3692
3724
  trading_logger.log_warning(
@@ -3700,8 +3732,36 @@ class FivePaisa(BrokerBase):
3700
3732
  },
3701
3733
  )
3702
3734
  return None
3735
+ resolved_symbol = price.symbol
3736
+ price = prices.get(resolved_symbol, price)
3737
+ price.src = "fp"
3738
+ if "BidRate" in json_data:
3739
+ price.bid = json_data.get("BidRate", price.bid)
3740
+ if "OffRate" in json_data:
3741
+ price.ask = json_data.get("OffRate", price.ask)
3742
+ if "BidQty" in json_data:
3743
+ price.bid_volume = json_data.get("BidQty", price.bid_volume)
3744
+ if "OffQty" in json_data:
3745
+ price.ask_volume = json_data.get("OffQty", price.ask_volume)
3746
+ if "LastRate" in json_data:
3747
+ price.last = json_data.get("LastRate", price.last)
3748
+ if "PClose" in json_data:
3749
+ price.prior_close = json_data.get("PClose", price.prior_close)
3750
+ if "High" in json_data:
3751
+ price.high = json_data.get("High", price.high)
3752
+ if "Low" in json_data:
3753
+ price.low = json_data.get("Low", price.low)
3754
+ if "TotalQty" in json_data:
3755
+ price.volume = json_data.get("TotalQty", price.volume)
3756
+ if "OpenInterest" in json_data:
3757
+ price.oi = safe_int(json_data.get("OpenInterest"), price.oi)
3758
+ price.symbol = resolved_symbol
3703
3759
  price.exchange = self.map_exchange_for_db(price.symbol, resolved_exchange)
3704
- price.timestamp = self.convert_to_ist(json_data["TickDt"])
3760
+ if json_data.get("TickDt"):
3761
+ price.timestamp = self.convert_to_ist(json_data["TickDt"])
3762
+ else:
3763
+ price.timestamp = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
3764
+ prices[resolved_symbol] = price
3705
3765
  return price
3706
3766
  except Exception as e:
3707
3767
  trading_logger.log_error("Error mapping price data", e, {"json_data": json_data})
@@ -3837,14 +3897,26 @@ class FivePaisa(BrokerBase):
3837
3897
  with self._stream_reconnect_lock:
3838
3898
  self._stream_reconnect_active = False
3839
3899
 
3840
- def connect_and_receive(req_data):
3900
+ def connect_and_receive(req_data, extra_req_data=None):
3841
3901
  """Connect and receive data."""
3842
3902
  try:
3843
3903
  if self.api is None:
3844
3904
  raise BrokerConnectionError("API client not initialized")
3845
3905
  self.api.connect(req_data)
3846
- self.api.error_data(error_data)
3847
3906
  ws = getattr(self.api, "ws", None)
3907
+ if ws is not None and extra_req_data:
3908
+ original_on_open = getattr(ws, "on_open", None)
3909
+
3910
+ def on_open_with_extra_requests(opened_ws):
3911
+ if callable(original_on_open):
3912
+ original_on_open(opened_ws)
3913
+ for extra_req in extra_req_data or []:
3914
+ if extra_req:
3915
+ self._wait_for_stream_request_rate_limit()
3916
+ opened_ws.send(json.dumps(extra_req))
3917
+
3918
+ ws.on_open = on_open_with_extra_requests
3919
+ self.api.error_data(error_data)
3848
3920
  if ws is not None:
3849
3921
  ws.on_close = close_data
3850
3922
  self.api.receive_data(on_message)
@@ -3852,14 +3924,19 @@ class FivePaisa(BrokerBase):
3852
3924
  trading_logger.log_error("Error in connect_and_receive", e, {"req_data": req_data})
3853
3925
  raise
3854
3926
 
3927
+ def is_ws_connected(ws) -> bool:
3928
+ sock = getattr(ws, "sock", None)
3929
+ return bool(sock is not None and getattr(sock, "connected", False))
3930
+
3855
3931
  def has_live_stream():
3856
3932
  """Return True only when the streaming thread and websocket are both usable."""
3933
+ ws = getattr(self.api, "ws", None) if self.api is not None else None
3857
3934
  return (
3858
3935
  self.subscribe_thread is not None
3859
3936
  and self.subscribe_thread.is_alive()
3860
- and self.api is not None
3861
- and getattr(self.api, "ws", None) is not None
3862
- and callable(getattr(self.api.ws, "send", None))
3937
+ and ws is not None
3938
+ and callable(getattr(ws, "send", None))
3939
+ and is_ws_connected(ws)
3863
3940
  )
3864
3941
 
3865
3942
  def reconnect_stream():
@@ -3899,11 +3976,15 @@ class FivePaisa(BrokerBase):
3899
3976
  raise MarketDataError("No symbols to reconnect after socket closure", context)
3900
3977
  if self.api is None:
3901
3978
  raise BrokerConnectionError("API client not initialized")
3902
- req_data_full = self.api.Request_Feed("mf", "s", req_list_full)
3979
+ req_data_full = copy.deepcopy(self.api.Request_Feed("mf", "s", req_list_full))
3980
+ oi_req_list_full = [dict(item) for item in req_list_full if item.get("ExchType") == "D"]
3981
+ extra_req_data_full = [
3982
+ copy.deepcopy(self.api.Request_Feed("oi", "s", oi_req_list_full))
3983
+ ] if oi_req_list_full else []
3903
3984
  reconnect_started_ts = time.time()
3904
3985
  self.subscribe_thread = threading.Thread(
3905
3986
  target=connect_and_receive,
3906
- args=(req_data_full,),
3987
+ args=(req_data_full, extra_req_data_full),
3907
3988
  name="MarketDataStreamer",
3908
3989
  )
3909
3990
  self.subscribe_thread.start()
@@ -3934,6 +4015,8 @@ class FivePaisa(BrokerBase):
3934
4015
  ws = getattr(self.api, "ws", None)
3935
4016
  if ws is None or not callable(getattr(ws, "send", None)):
3936
4017
  raise AttributeError("WebSocket client does not support send")
4018
+ if not is_ws_connected(ws):
4019
+ raise WebSocketConnectionClosedException("Connection is already closed.")
3937
4020
  self._wait_for_stream_request_rate_limit()
3938
4021
  ws.send(json.dumps(req_data))
3939
4022
 
@@ -4020,7 +4103,9 @@ class FivePaisa(BrokerBase):
4020
4103
  if req_list is not None and len(req_list) > 0:
4021
4104
  if self.api is None:
4022
4105
  raise BrokerConnectionError("API client not initialized")
4023
- req_data = self.api.Request_Feed("mf", operation, req_list)
4106
+ req_data = copy.deepcopy(self.api.Request_Feed("mf", operation, req_list))
4107
+ oi_req_list = [dict(item) for item in req_list if item.get("ExchType") == "D"]
4108
+ oi_req_data = copy.deepcopy(self.api.Request_Feed("oi", operation, oi_req_list)) if oi_req_list else None
4024
4109
 
4025
4110
  # Start the connection and receiving data in a separate thread
4026
4111
  if not has_live_stream():
@@ -4033,6 +4118,8 @@ class FivePaisa(BrokerBase):
4033
4118
  )
4034
4119
  try:
4035
4120
  send_stream_request(req_data)
4121
+ if oi_req_data:
4122
+ send_stream_request(oi_req_data)
4036
4123
  except (AttributeError, WebSocketConnectionClosedException):
4037
4124
  trading_logger.log_info(
4038
4125
  "WebSocket closed, reconnecting...",
@@ -108,7 +108,7 @@ def save_symbol_data(saveToFolder: bool = True):
108
108
  _proxies = None
109
109
  try:
110
110
  from .proxy_utils import get_proxies_for_broker
111
- _proxies = get_proxies_for_broker("FLATTRADE")
111
+ _proxies = get_proxies_for_broker("FLATTRADE", purpose="symbol_download")
112
112
  except Exception:
113
113
  pass
114
114
  url = "https://api.shoonya.com/NSE_symbols.txt.zip"
@@ -2398,6 +2398,7 @@ class FlatTrade(BrokerBase):
2398
2398
  market_feed.high = safe_float(tick_data.get("h"))
2399
2399
  market_feed.low = safe_float(tick_data.get("l"))
2400
2400
  market_feed.volume = safe_int(tick_data.get("v"))
2401
+ market_feed.oi = safe_int(tick_data.get("oi"))
2401
2402
 
2402
2403
  # Handle exchange mapping
2403
2404
  try:
@@ -2482,6 +2483,17 @@ class FlatTrade(BrokerBase):
2482
2483
  prices = {}
2483
2484
  mapped_exchange = self.map_exchange_for_api(symbols[0], exchange)
2484
2485
 
2486
+ def safe_int_value(value, default=0):
2487
+ if value in [None, "", 0, "0", "0.00", float("nan")]:
2488
+ return default
2489
+ try:
2490
+ return int(str(value).strip())
2491
+ except (TypeError, ValueError):
2492
+ try:
2493
+ return int(float(value))
2494
+ except (ValueError, TypeError):
2495
+ return default
2496
+
2485
2497
  # Function to map JSON data to a Price object
2486
2498
  def map_to_price(json_data):
2487
2499
  price = Price()
@@ -2527,6 +2539,7 @@ class FlatTrade(BrokerBase):
2527
2539
  else float(json_data.get("l"))
2528
2540
  )
2529
2541
  price.volume = float("nan") if json_data.get("v") in [None, float("nan")] else float(json_data.get("v"))
2542
+ price.oi = safe_int_value(json_data.get("oi"))
2530
2543
  symbol = self.exchange_mappings[json_data.get("e")]["symbol_map_reversed"].get(int(json_data.get("tk")))
2531
2544
  price.exchange = self.map_exchange_for_db(symbol, json_data.get("e"))
2532
2545
  price.timestamp = self.convert_ft_to_ist(int(json_data.get("ft", 0)))
@@ -2541,9 +2554,11 @@ class FlatTrade(BrokerBase):
2541
2554
  if ext_callback:
2542
2555
  ext_callback(price)
2543
2556
  elif message.get("t") == "tf":
2544
- required_keys = {"bp1", "sp1", "c", "lp", "bq1", "sq1", "h", "l"}
2557
+ required_keys = {"bp1", "sp1", "c", "lp", "bq1", "sq1", "h", "l", "oi"}
2545
2558
  if required_keys & message.keys():
2546
2559
  price = prices.get(message.get("tk"))
2560
+ if price is None:
2561
+ return
2547
2562
  if message.get("bp1"):
2548
2563
  price.bid = float(message.get("bp1"))
2549
2564
  if message.get("sp1"):
@@ -2562,6 +2577,8 @@ class FlatTrade(BrokerBase):
2562
2577
  price.low = float(message.get("l"))
2563
2578
  if message.get("v"):
2564
2579
  price.volume = float(message.get("v"))
2580
+ if message.get("oi") not in [None, "", float("nan")]:
2581
+ price.oi = safe_int_value(message.get("oi"), price.oi)
2565
2582
  price.timestamp = self.convert_ft_to_ist(int(message.get("ft", 0)))
2566
2583
  prices[message.get("tk")] = price
2567
2584
  if ext_callback:
@@ -135,7 +135,7 @@ def save_symbol_data(saveToFolder: bool = True) -> pd.DataFrame:
135
135
  try:
136
136
  from .proxy_utils import get_proxies_for_broker
137
137
 
138
- _proxies = get_proxies_for_broker("ICICIDIRECT")
138
+ _proxies = get_proxies_for_broker("ICICIDIRECT", purpose="symbol_download")
139
139
  except Exception:
140
140
  pass
141
141
  _prev_ipv6 = _temporarily_force_ipv4()
@@ -1553,9 +1553,12 @@ class IciciDirect(BrokerBase):
1553
1553
  if v in [None, ""]:
1554
1554
  return default
1555
1555
  try:
1556
- return int(float(cast(Union[int, float, str], v)))
1556
+ return int(str(cast(Union[int, float, str], v)).strip())
1557
1557
  except Exception:
1558
- return default
1558
+ try:
1559
+ return int(float(cast(Union[int, float, str], v)))
1560
+ except Exception:
1561
+ return default
1559
1562
 
1560
1563
  market_feed.bid = _f("bPrice")
1561
1564
  market_feed.ask = _f("sPrice")
@@ -1566,6 +1569,10 @@ class IciciDirect(BrokerBase):
1566
1569
  market_feed.low = _f("low")
1567
1570
  market_feed.prior_close = _f("close")
1568
1571
  market_feed.volume = _i("ttq")
1572
+ for oi_key in ("open_interest", "oi", "OpenInterest", "OI"):
1573
+ if tick.get(oi_key) not in [None, ""]:
1574
+ market_feed.oi = _i(oi_key)
1575
+ break
1569
1576
  if isinstance(tick.get("ltt"), str) and tick.get("ltt"):
1570
1577
  ltt = str(tick.get("ltt"))
1571
1578
  try:
@@ -0,0 +1,294 @@
1
+ """
2
+ Proxy utilities for broker HTTP requests.
3
+
4
+ When a broker has USE_PROXY=True (API) or USE_PROXY_SYMBOL_DOWNLOAD=True (symbol master),
5
+ configured, get_proxies_for_broker() returns a proxies dict for use with requests.
6
+ Used in connect() and save_symbol_data() for each broker.
7
+ """
8
+ import os
9
+ import random
10
+ from typing import Any, Dict, Optional, Tuple, Union
11
+ from urllib.parse import quote
12
+
13
+ import requests
14
+
15
+ from tradingapi import trading_logger
16
+
17
+ _WEBSHARE_LIST_URL = "https://proxy.webshare.io/api/v2/proxy/list/"
18
+
19
+
20
+ def _get_proxy_credentials(proxy_cfg: Dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
21
+ username = proxy_cfg.get("user") or proxy_cfg.get("username")
22
+ password = proxy_cfg.get("pass") or proxy_cfg.get("password")
23
+ return username, password
24
+
25
+
26
+ def _get_proxy_config(broker_name: str, purpose: str = "api") -> Optional[tuple[Any, Dict[str, Any]]]:
27
+ try:
28
+ from .config import get_config
29
+
30
+ config = get_config()
31
+ except Exception:
32
+ return None
33
+
34
+ if purpose == "symbol_download":
35
+ use_proxy = config.get(f"{broker_name}.USE_PROXY_SYMBOL_DOWNLOAD")
36
+ else:
37
+ use_proxy = config.get(f"{broker_name}.USE_PROXY")
38
+ if not use_proxy:
39
+ return None
40
+
41
+ proxy_cfg = config.configs.get("proxy")
42
+ if not isinstance(proxy_cfg, dict):
43
+ trading_logger.log_debug("Proxy not used: no proxy section in config", context={"broker": broker_name})
44
+ return None
45
+
46
+ return config, proxy_cfg
47
+
48
+
49
+ def get_proxy_verify_ssl(broker_name: str, purpose: str = "api") -> bool:
50
+ """Return False when broker proxy requires disabled SSL verify (e.g. NordVPN)."""
51
+ cfg = _get_proxy_config(broker_name, purpose=purpose)
52
+ if not cfg:
53
+ return True
54
+ _, proxy_cfg = cfg
55
+ source = (proxy_cfg.get("source") or "").strip().lower()
56
+ return source != "nordvpn"
57
+
58
+
59
+ def _nordvpn_max_proxies(proxy_cfg: Dict[str, Any]) -> int:
60
+ try:
61
+ return max(1, int(proxy_cfg.get("max_proxies") or 30))
62
+ except (TypeError, ValueError):
63
+ return 30
64
+
65
+
66
+ def _build_nordvpn_proxies(proxy_cfg: Dict[str, Any], broker_name: str) -> list[Dict[str, str]]:
67
+ username, password = _get_proxy_credentials(proxy_cfg)
68
+ if not username or not password:
69
+ trading_logger.log_warning(
70
+ "Proxy enabled but proxy user/pass not set for NordVPN",
71
+ context={"broker": broker_name},
72
+ )
73
+ return []
74
+ country_code = proxy_cfg.get("country_code") or "IN"
75
+ try:
76
+ from chameli.interactions import get_nordvpn_proxies
77
+
78
+ host_ports = get_nordvpn_proxies(country_code=country_code, max_proxies=_nordvpn_max_proxies(proxy_cfg))
79
+ u = quote(str(username), safe="")
80
+ p = quote(str(password), safe="")
81
+ return [
82
+ {"http": f"https://{u}:{p}@{host_port}", "https": f"https://{u}:{p}@{host_port}"}
83
+ for host_port in host_ports
84
+ ]
85
+ except Exception as e:
86
+ trading_logger.log_warning(
87
+ "Failed to fetch NordVPN proxy for broker",
88
+ context={"broker": broker_name, "error": str(e)},
89
+ )
90
+ return []
91
+
92
+
93
+ def iter_proxies_for_broker(broker_name: str, purpose: str = "api"):
94
+ """
95
+ Yield (proxies_dict, verify_ssl) tuples to try for HTTP requests.
96
+ NordVPN yields multiple servers; others yield at most one entry.
97
+ When proxy is disabled, yields a single direct request config.
98
+ """
99
+ cfg = _get_proxy_config(broker_name, purpose=purpose)
100
+ if not cfg:
101
+ yield {}, True
102
+ return
103
+
104
+ _, proxy_cfg = cfg
105
+ source = (proxy_cfg.get("source") or "webshare").strip().lower()
106
+ if source == "nordvpn":
107
+ proxies_list = _build_nordvpn_proxies(proxy_cfg, broker_name)
108
+ if not proxies_list:
109
+ yield {}, True
110
+ return
111
+ for proxies in proxies_list:
112
+ yield proxies, False
113
+ return
114
+
115
+ proxies = get_proxies_for_broker(broker_name, purpose=purpose)
116
+ if proxies:
117
+ yield proxies, get_proxy_verify_ssl(broker_name, purpose=purpose)
118
+ else:
119
+ yield {}, True
120
+
121
+
122
+ def suppress_insecure_request_warnings() -> None:
123
+ import urllib3
124
+
125
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
126
+
127
+
128
+ def request_get_with_broker_proxy(
129
+ url: str,
130
+ broker_name: str,
131
+ purpose: str = "symbol_download",
132
+ headers: Optional[Dict[str, str]] = None,
133
+ timeout: Union[float, Tuple[float, float]] = (10, 300),
134
+ min_content_bytes: int = 100_000,
135
+ ) -> requests.Response:
136
+ """
137
+ GET url using broker proxy settings, rotating NordVPN servers until a valid response.
138
+ Validates status 200 and minimum response size (Dhan 403 pages are ~919 bytes).
139
+ """
140
+ suppress_insecure_request_warnings()
141
+ response = None
142
+ last_status = None
143
+ last_error = None
144
+ for proxies, verify in iter_proxies_for_broker(broker_name, purpose=purpose):
145
+ proxy_host = (proxies or {}).get("https", "").split("@")[-1] if proxies else "direct"
146
+ try:
147
+ response = requests.get(
148
+ url,
149
+ headers=headers or {},
150
+ proxies=proxies or {},
151
+ verify=verify,
152
+ timeout=timeout,
153
+ )
154
+ last_status = response.status_code
155
+ if response.status_code == 200 and len(response.content) >= min_content_bytes:
156
+ if proxy_host and proxy_host != "direct":
157
+ trading_logger.log_info(
158
+ "HTTP request succeeded via proxy",
159
+ context={"broker": broker_name, "proxy": proxy_host, "url": url},
160
+ )
161
+ return response
162
+ trading_logger.log_warning(
163
+ "Proxy HTTP attempt rejected or too small",
164
+ context={
165
+ "broker": broker_name,
166
+ "proxy": proxy_host or "direct",
167
+ "status_code": response.status_code,
168
+ "content_bytes": len(response.content),
169
+ },
170
+ )
171
+ except requests.RequestException as exc:
172
+ last_error = str(exc)
173
+ trading_logger.log_warning(
174
+ "Proxy HTTP attempt failed",
175
+ context={"broker": broker_name, "proxy": proxy_host or "direct", "error": last_error},
176
+ )
177
+
178
+ detail = f"status={last_status}" if last_status is not None else last_error or "unknown error"
179
+ raise requests.RequestException(f"All proxy attempts failed for {broker_name}: {detail}")
180
+
181
+
182
+ def get_proxies_for_broker(broker_name: str, purpose: str = "api") -> Optional[Dict[str, str]]:
183
+ """
184
+ Return a proxies dict for use with requests if proxy is enabled for the given purpose
185
+ and proxy config (e.g. Webshare, NordVPN) is present. Otherwise return None.
186
+
187
+ purpose:
188
+ "api" – controlled by {broker}.USE_PROXY (connect, trading API calls)
189
+ "symbol_download" – controlled by {broker}.USE_PROXY_SYMBOL_DOWNLOAD (save_symbol_data)
190
+
191
+ Returns:
192
+ {"http": "http://user:pass@host:port", "https": "http://user:pass@host:port"}
193
+ or None if proxy should not be used.
194
+ """
195
+ cfg = _get_proxy_config(broker_name, purpose=purpose)
196
+ if not cfg:
197
+ return None
198
+ _, proxy_cfg = cfg
199
+
200
+ source = (proxy_cfg.get("source") or "webshare").strip().lower()
201
+ if source == "nordvpn":
202
+ proxies_list = _build_nordvpn_proxies(proxy_cfg, broker_name)
203
+ return proxies_list[0] if proxies_list else None
204
+
205
+ if source != "webshare":
206
+ trading_logger.log_debug(
207
+ "Proxy not used: unsupported proxy source",
208
+ context={"broker": broker_name, "source": source},
209
+ )
210
+ return None
211
+
212
+ api_key = proxy_cfg.get("api_key") or os.getenv("WEBSHARE_PROXY_API_KEY")
213
+ if not api_key:
214
+ trading_logger.log_warning("Proxy enabled but proxy.api_key not set", context={"broker": broker_name})
215
+ return None
216
+
217
+ mode = proxy_cfg.get("mode") or "direct"
218
+ country_code = proxy_cfg.get("country_code")
219
+ params: Dict[str, Any] = {"mode": mode, "page": 1, "page_size": 100}
220
+ if country_code:
221
+ params["country_code"] = country_code
222
+
223
+ try:
224
+ resp = requests.get(
225
+ _WEBSHARE_LIST_URL,
226
+ params=params,
227
+ headers={"Authorization": api_key},
228
+ timeout=15,
229
+ )
230
+ if resp.status_code != 200:
231
+ trading_logger.log_warning(
232
+ "Webshare proxy list failed",
233
+ context={"broker": broker_name, "status_code": resp.status_code},
234
+ )
235
+ return None
236
+ data = resp.json()
237
+ results = data.get("results") or []
238
+ valid_proxies = [p for p in results if p.get("valid")]
239
+ if not valid_proxies:
240
+ trading_logger.log_warning("No valid Webshare proxies in list", context={"broker": broker_name})
241
+ return None
242
+ proxy = random.choice(valid_proxies)
243
+ host = proxy.get("proxy_address", "")
244
+ port = proxy.get("port", 80)
245
+ username, password = _get_proxy_credentials(proxy_cfg)
246
+ username = username or proxy.get("username")
247
+ password = password or proxy.get("password")
248
+ if username and password:
249
+ proxy_url = f"http://{username}:{password}@{host}:{port}"
250
+ else:
251
+ proxy_url = f"http://{host}:{port}"
252
+ return {"http": proxy_url, "https": proxy_url}
253
+ except Exception as e:
254
+ trading_logger.log_warning(
255
+ "Failed to fetch proxy for broker",
256
+ context={"broker": broker_name, "error": str(e)},
257
+ )
258
+ return None
259
+
260
+
261
+ def set_proxy_env_for_broker(broker_name: str) -> Optional[Dict[str, Optional[str]]]:
262
+ """
263
+ If broker has USE_PROXY enabled, set HTTP_PROXY and HTTPS_PROXY in the environment
264
+ from Webshare and return the previous env values (so caller can restore).
265
+ Otherwise return None and do not change env.
266
+ """
267
+ proxies = get_proxies_for_broker(broker_name)
268
+ if not proxies:
269
+ return None
270
+ proxy_url = proxies.get("https") or proxies.get("http")
271
+ if not proxy_url:
272
+ return None
273
+ old = {
274
+ "HTTP_PROXY": os.environ.get("HTTP_PROXY"),
275
+ "HTTPS_PROXY": os.environ.get("HTTPS_PROXY"),
276
+ "http_proxy": os.environ.get("http_proxy"),
277
+ "https_proxy": os.environ.get("https_proxy"),
278
+ }
279
+ os.environ["HTTP_PROXY"] = proxy_url
280
+ os.environ["HTTPS_PROXY"] = proxy_url
281
+ os.environ["http_proxy"] = proxy_url
282
+ os.environ["https_proxy"] = proxy_url
283
+ return old
284
+
285
+
286
+ def restore_proxy_env(previous: Optional[Dict[str, Optional[str]]]) -> None:
287
+ """Restore HTTP_PROXY/HTTPS_PROXY env from a dict returned by set_proxy_env_for_broker."""
288
+ if not previous:
289
+ return
290
+ for key, value in previous.items():
291
+ if value is None:
292
+ os.environ.pop(key, None)
293
+ else:
294
+ os.environ[key] = value
@@ -129,7 +129,7 @@ def save_symbol_data(saveToFolder: bool = True) -> pd.DataFrame:
129
129
  _proxies = None
130
130
  try:
131
131
  from .proxy_utils import get_proxies_for_broker
132
- _proxies = get_proxies_for_broker("SHOONYA")
132
+ _proxies = get_proxies_for_broker("SHOONYA", purpose="symbol_download")
133
133
  except Exception:
134
134
  pass
135
135
  url = "https://api.shoonya.com/NSE_symbols.txt.zip"
@@ -2569,6 +2569,7 @@ class Shoonya(BrokerBase):
2569
2569
  market_feed.high = safe_float(tick_data.get("h"))
2570
2570
  market_feed.low = safe_float(tick_data.get("l"))
2571
2571
  market_feed.volume = safe_int(tick_data.get("v"))
2572
+ market_feed.oi = safe_int(tick_data.get("oi"))
2572
2573
 
2573
2574
  # Handle exchange mapping
2574
2575
  try:
@@ -2653,6 +2654,17 @@ class Shoonya(BrokerBase):
2653
2654
  prices: Dict[str, Price] = {}
2654
2655
  mapped_exchange = self.map_exchange_for_api(symbols[0], exchange)
2655
2656
 
2657
+ def safe_int_value(value, default=0):
2658
+ if value in [None, "", 0, "0", "0.00", float("nan")]:
2659
+ return default
2660
+ try:
2661
+ return int(str(value).strip())
2662
+ except (TypeError, ValueError):
2663
+ try:
2664
+ return int(float(value))
2665
+ except (ValueError, TypeError):
2666
+ return default
2667
+
2656
2668
  # Function to map JSON data to a Price object
2657
2669
  def map_to_price(json_data) -> Price:
2658
2670
  price = Price()
@@ -2698,6 +2710,7 @@ class Shoonya(BrokerBase):
2698
2710
  else float(json_data.get("l"))
2699
2711
  )
2700
2712
  price.volume = float("nan") if json_data.get("v") in [None, float("nan")] else float(json_data.get("v"))
2713
+ price.oi = safe_int_value(json_data.get("oi"))
2701
2714
  symbol = self.exchange_mappings[json_data.get("e")]["symbol_map_reversed"].get(int(json_data.get("tk")))
2702
2715
  price.exchange = self.map_exchange_for_db(symbol, json_data.get("e"))
2703
2716
  price.timestamp = self.convert_ft_to_ist(int(json_data.get("ft", 0)))
@@ -2719,7 +2732,7 @@ class Shoonya(BrokerBase):
2719
2732
  {"symbol": price.symbol, "src": price.src},
2720
2733
  )
2721
2734
  elif message.get("t") == "tf":
2722
- required_keys = {"bp1", "sp1", "c", "lp", "bq1", "sq1", "h", "l"}
2735
+ required_keys = {"bp1", "sp1", "c", "lp", "bq1", "sq1", "h", "l", "oi"}
2723
2736
  if required_keys & message.keys():
2724
2737
  price = prices.get(message.get("tk"))
2725
2738
  if price is not None:
@@ -2741,6 +2754,8 @@ class Shoonya(BrokerBase):
2741
2754
  price.low = float(message.get("l"))
2742
2755
  if message.get("v"):
2743
2756
  price.volume = float(message.get("v"))
2757
+ if message.get("oi") not in [None, "", float("nan")]:
2758
+ price.oi = safe_int_value(message.get("oi"), price.oi)
2744
2759
  price.timestamp = self.convert_ft_to_ist(int(message.get("ft", 0)))
2745
2760
  prices[message.get("tk")] = price
2746
2761
  if ext_callback is not None:
@@ -2790,6 +2790,7 @@ def get_price(
2790
2790
  ask_volume=sum(price.ask_volume for price in prices),
2791
2791
  prior_close=sum(price.prior_close for price in prices),
2792
2792
  last=sum(price.last for price in prices),
2793
+ oi=sum(price.oi for price in prices),
2793
2794
  symbol=prices[0].symbol, # Assuming symbol remains the same for all prices
2794
2795
  )
2795
2796
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradingapi
3
- Version: 0.3.9
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
@@ -1,130 +0,0 @@
1
- """
2
- Proxy utilities for broker HTTP requests.
3
-
4
- When a broker has USE_PROXY=True in config and proxy settings (e.g. Webshare) are
5
- configured, get_proxies_for_broker() returns a proxies dict for use with requests.
6
- Used in connect() and save_symbol_data() for each broker.
7
- """
8
- import os
9
- import random
10
- from typing import Any, Dict, Optional
11
-
12
- import requests
13
-
14
- from tradingapi import trading_logger
15
-
16
- _WEBSHARE_LIST_URL = "https://proxy.webshare.io/api/v2/proxy/list/"
17
-
18
-
19
- def get_proxies_for_broker(broker_name: str) -> Optional[Dict[str, str]]:
20
- """
21
- Return a proxies dict for use with requests if this broker has USE_PROXY enabled
22
- and proxy config (e.g. Webshare) is present. Otherwise return None.
23
-
24
- Returns:
25
- {"http": "http://user:pass@host:port", "https": "http://user:pass@host:port"}
26
- or None if proxy should not be used.
27
- """
28
- try:
29
- from .config import get_config
30
-
31
- config = get_config()
32
- except Exception:
33
- return None
34
-
35
- use_proxy = config.get(f"{broker_name}.USE_PROXY")
36
- if not use_proxy:
37
- return None
38
-
39
- proxy_cfg = config.configs.get("proxy")
40
- if not isinstance(proxy_cfg, dict):
41
- trading_logger.log_debug("Proxy not used: no proxy section in config", context={"broker": broker_name})
42
- return None
43
-
44
- source = (proxy_cfg.get("source") or "").strip().lower()
45
- if source != "webshare":
46
- trading_logger.log_debug("Proxy not used: only webshare source is supported", context={"broker": broker_name})
47
- return None
48
-
49
- api_key = proxy_cfg.get("api_key") or os.getenv("WEBSHARE_PROXY_API_KEY")
50
- if not api_key:
51
- trading_logger.log_warning("Proxy enabled but proxy.api_key not set", context={"broker": broker_name})
52
- return None
53
-
54
- mode = proxy_cfg.get("mode") or "direct"
55
- country_code = proxy_cfg.get("country_code")
56
- params: Dict[str, Any] = {"mode": mode, "page": 1, "page_size": 100}
57
- if country_code:
58
- params["country_code"] = country_code
59
-
60
- try:
61
- resp = requests.get(
62
- _WEBSHARE_LIST_URL,
63
- params=params,
64
- headers={"Authorization": api_key},
65
- timeout=15,
66
- )
67
- if resp.status_code != 200:
68
- trading_logger.log_warning(
69
- "Webshare proxy list failed",
70
- context={"broker": broker_name, "status_code": resp.status_code},
71
- )
72
- return None
73
- data = resp.json()
74
- results = data.get("results") or []
75
- valid_proxies = [p for p in results if p.get("valid")]
76
- if not valid_proxies:
77
- trading_logger.log_warning("No valid Webshare proxies in list", context={"broker": broker_name})
78
- return None
79
- proxy = random.choice(valid_proxies)
80
- host = proxy.get("proxy_address", "")
81
- port = proxy.get("port", 80)
82
- username = proxy.get("username") or proxy_cfg.get("username")
83
- password = proxy.get("password") or proxy_cfg.get("password")
84
- if username and password:
85
- proxy_url = f"http://{username}:{password}@{host}:{port}"
86
- else:
87
- proxy_url = f"http://{host}:{port}"
88
- return {"http": proxy_url, "https": proxy_url}
89
- except Exception as e:
90
- trading_logger.log_warning(
91
- "Failed to fetch proxy for broker",
92
- context={"broker": broker_name, "error": str(e)},
93
- )
94
- return None
95
-
96
-
97
- def set_proxy_env_for_broker(broker_name: str) -> Optional[Dict[str, Optional[str]]]:
98
- """
99
- If broker has USE_PROXY enabled, set HTTP_PROXY and HTTPS_PROXY in the environment
100
- from Webshare and return the previous env values (so caller can restore).
101
- Otherwise return None and do not change env.
102
- """
103
- proxies = get_proxies_for_broker(broker_name)
104
- if not proxies:
105
- return None
106
- proxy_url = proxies.get("https") or proxies.get("http")
107
- if not proxy_url:
108
- return None
109
- old = {
110
- "HTTP_PROXY": os.environ.get("HTTP_PROXY"),
111
- "HTTPS_PROXY": os.environ.get("HTTPS_PROXY"),
112
- "http_proxy": os.environ.get("http_proxy"),
113
- "https_proxy": os.environ.get("https_proxy"),
114
- }
115
- os.environ["HTTP_PROXY"] = proxy_url
116
- os.environ["HTTPS_PROXY"] = proxy_url
117
- os.environ["http_proxy"] = proxy_url
118
- os.environ["https_proxy"] = proxy_url
119
- return old
120
-
121
-
122
- def restore_proxy_env(previous: Optional[Dict[str, Optional[str]]]) -> None:
123
- """Restore HTTP_PROXY/HTTPS_PROXY env from a dict returned by set_proxy_env_for_broker."""
124
- if not previous:
125
- return
126
- for key, value in previous.items():
127
- if value is None:
128
- os.environ.pop(key, None)
129
- else:
130
- os.environ[key] = value
File without changes
File without changes