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.
- {tradingapi-0.3.9 → tradingapi-0.3.10}/PKG-INFO +1 -1
- {tradingapi-0.3.9 → tradingapi-0.3.10}/pyproject.toml +1 -1
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/broker_base.py +7 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/config/config_sample.yaml +7 -1
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/dhan.py +28 -7
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/fivepaisa.py +119 -32
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/flattrade.py +19 -2
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/icicidirect.py +10 -3
- tradingapi-0.3.10/tradingapi/proxy_utils.py +294 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/shoonya.py +17 -2
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/utils.py +1 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/PKG-INFO +1 -1
- tradingapi-0.3.9/tradingapi/proxy_utils.py +0 -130
- {tradingapi-0.3.9 → tradingapi-0.3.10}/README.md +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/setup.cfg +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tests/test_broker_side_terminal_order.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tests/test_calculate_delta_realtime_quotes.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tests/test_find_option_with_delta.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/__init__.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/allocation.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/attribution.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/config.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/globals.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/market_data_exchanges.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi/span.py +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/SOURCES.txt +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/requires.txt +0 -0
- {tradingapi-0.3.9 → tradingapi-0.3.10}/tradingapi.egg-info/top_level.txt +0 -0
|
@@ -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
|
-
#
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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(
|
|
2358
|
+
return int(str(value).strip())
|
|
2344
2359
|
except (TypeError, ValueError):
|
|
2345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
3559
|
-
market_feed.high = snapshot
|
|
3560
|
-
market_feed.low = snapshot
|
|
3561
|
-
market_feed.last = snapshot
|
|
3562
|
-
market_feed.prior_close = snapshot
|
|
3563
|
-
market_feed.volume = snapshot
|
|
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
|
-
|
|
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
|
|
3861
|
-
and getattr(
|
|
3862
|
-
and
|
|
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(
|
|
1556
|
+
return int(str(cast(Union[int, float, str], v)).strip())
|
|
1557
1557
|
except Exception:
|
|
1558
|
-
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|