tradingapi 0.3.0__tar.gz → 0.3.1__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.0 → tradingapi-0.3.1}/PKG-INFO +1 -1
- {tradingapi-0.3.0 → tradingapi-0.3.1}/pyproject.toml +1 -1
- tradingapi-0.3.1/tests/test_find_option_with_delta.py +122 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/dhan.py +99 -48
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/fivepaisa.py +28 -23
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/utils.py +101 -85
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi.egg-info/PKG-INFO +1 -1
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi.egg-info/SOURCES.txt +1 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/README.md +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/setup.cfg +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/__init__.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/allocation.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/attribution.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/broker_base.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/config/config_sample.yaml +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/config.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/flattrade.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/globals.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/icicidirect.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/market_data_exchanges.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/proxy_utils.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/shoonya.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi/span.py +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi.egg-info/requires.txt +0 -0
- {tradingapi-0.3.0 → tradingapi-0.3.1}/tradingapi.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Standalone test for find_option_with_delta NaN-robustness fix.
|
|
2
|
+
|
|
3
|
+
Run: /home/psharma/envs/base/bin/python3 tradingapi2/tests/test_find_option_with_delta.py
|
|
4
|
+
"""
|
|
5
|
+
import math
|
|
6
|
+
import sys
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
# Force import from source repo, not site-packages installed copy.
|
|
10
|
+
sys.path.insert(0, "/home/psharma/onedrive/code/tradingapi2")
|
|
11
|
+
|
|
12
|
+
from tradingapi import utils
|
|
13
|
+
|
|
14
|
+
assert utils.__file__.startswith("/home/psharma/onedrive/code/tradingapi2/"), (
|
|
15
|
+
f"Wrong tradingapi loaded: {utils.__file__}"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_chain(strikes, expiry="20260529", opt_type="CALL", underlying="NIFTY"):
|
|
20
|
+
# Symbol format expected by _strike() and calculate_delta: "<u>_<x>_<expiry>_<type>_<strike>_..."
|
|
21
|
+
return [f"{underlying}_X_{expiry}_{opt_type}_{int(s) if s == int(s) else s}_X" for s in strikes]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bs_like_delta(strike, spot, opt_type="CALL"):
|
|
25
|
+
"""Smooth monotone proxy for |delta| as a function of strike. Just needs to be monotone."""
|
|
26
|
+
# CALL delta ~ N(d1); approximate with logistic in (spot - strike)
|
|
27
|
+
x = (spot - strike) / (spot * 0.05)
|
|
28
|
+
sig = 1.0 / (1.0 + math.exp(-x))
|
|
29
|
+
return sig if opt_type == "CALL" else -(1.0 - sig)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_case(name, chain, price_f, target_delta, nan_strikes=(), return_lower=True, expect_index=True):
|
|
33
|
+
nan_set = set(nan_strikes)
|
|
34
|
+
call_log = []
|
|
35
|
+
|
|
36
|
+
def fake_calc_delta(brokers, sym, pf, *a, **kw):
|
|
37
|
+
strike = float(sym.split("_")[4])
|
|
38
|
+
call_log.append(strike)
|
|
39
|
+
if strike in nan_set:
|
|
40
|
+
return float("nan")
|
|
41
|
+
return bs_like_delta(strike, pf, opt_type=sym.split("_")[3])
|
|
42
|
+
|
|
43
|
+
with patch.object(utils, "calculate_delta", side_effect=fake_calc_delta):
|
|
44
|
+
idx = utils.find_option_with_delta(
|
|
45
|
+
brokers=[],
|
|
46
|
+
price_f=price_f,
|
|
47
|
+
option_chain=chain,
|
|
48
|
+
target_delta=target_delta,
|
|
49
|
+
return_lower_delta=return_lower,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
found = idx >= 0
|
|
53
|
+
status = "PASS" if found == expect_index else "FAIL"
|
|
54
|
+
chosen_strike = float(chain[idx].split("_")[4]) if found else None
|
|
55
|
+
chosen_delta = bs_like_delta(chosen_strike, price_f) if chosen_strike is not None else None
|
|
56
|
+
delta_str = f"{abs(chosen_delta):.4f}" if chosen_delta is not None else "n/a"
|
|
57
|
+
print(
|
|
58
|
+
f"[{status}] {name}: idx={idx} strike={chosen_strike} |delta|≈{delta_str} "
|
|
59
|
+
f"calls={len(call_log)} unique={len(set(call_log))} target={target_delta}"
|
|
60
|
+
)
|
|
61
|
+
return status == "PASS", idx, len(call_log)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
results = []
|
|
66
|
+
|
|
67
|
+
# Case 1 — clean chain, no NaN. Should land near target_delta=0.3 and be efficient.
|
|
68
|
+
strikes = [64900 + 100 * i for i in range(215)] # 64900..86300
|
|
69
|
+
chain = make_chain(strikes)
|
|
70
|
+
ok, idx, calls = run_case("clean chain, target=0.3", chain, 76956.7, 0.3)
|
|
71
|
+
results.append(ok)
|
|
72
|
+
assert calls < 30, f"clean chain should be cheap, got {calls} calls"
|
|
73
|
+
|
|
74
|
+
# Case 2 — exact repro of warning: NaN at deep ITM call strikes.
|
|
75
|
+
nan_strikes = [64900.0, 65100.0, 65400.0, 66100.0, 67500.0]
|
|
76
|
+
ok, idx, calls = run_case(
|
|
77
|
+
"warning repro: 5 NaN deep-ITM, target=0.3", chain, 76956.7, 0.3, nan_strikes=nan_strikes
|
|
78
|
+
)
|
|
79
|
+
results.append(ok)
|
|
80
|
+
|
|
81
|
+
# Case 3 — NaN cluster centered on mid (chain mid ~ index 107, strike ~75600).
|
|
82
|
+
mid_nans = [strikes[i] for i in range(100, 115)]
|
|
83
|
+
ok, _, _ = run_case("NaN cluster around mid", chain, 76956.7, 0.3, nan_strikes=mid_nans)
|
|
84
|
+
results.append(ok)
|
|
85
|
+
|
|
86
|
+
# Case 4 — NaN around the seeding fractions (0.1, 0.25, 0.5, 0.75, 0.9).
|
|
87
|
+
seed_idxs = [int(round(f * 214)) for f in [0.1, 0.25, 0.5, 0.75]] # leave 0.9 valid
|
|
88
|
+
seed_nans = [strikes[i] for i in seed_idxs]
|
|
89
|
+
ok, _, _ = run_case("NaN at most seed fractions", chain, 76956.7, 0.3, nan_strikes=seed_nans)
|
|
90
|
+
results.append(ok)
|
|
91
|
+
|
|
92
|
+
# Case 5 — all but two strikes NaN; should still find via linear fallback.
|
|
93
|
+
valid_strikes = {strikes[50], strikes[160]}
|
|
94
|
+
all_nan = [s for s in strikes if s not in valid_strikes]
|
|
95
|
+
ok, _, _ = run_case(
|
|
96
|
+
"only 2 valid strikes (linear fallback)", chain, 76956.7, 0.3, nan_strikes=all_nan
|
|
97
|
+
)
|
|
98
|
+
results.append(ok)
|
|
99
|
+
|
|
100
|
+
# Case 6 — entire chain NaN: must return -1 with warning, not crash.
|
|
101
|
+
ok, idx, _ = run_case(
|
|
102
|
+
"all NaN -> expect -1", chain, 76956.7, 0.3, nan_strikes=strikes, expect_index=False
|
|
103
|
+
)
|
|
104
|
+
results.append(ok)
|
|
105
|
+
|
|
106
|
+
# Case 7 — PUT chain (decreasing |delta| with strike).
|
|
107
|
+
put_chain = make_chain(strikes, opt_type="PUT")
|
|
108
|
+
ok, _, _ = run_case("PUT chain target=0.3", put_chain, 76956.7, 0.3)
|
|
109
|
+
results.append(ok)
|
|
110
|
+
|
|
111
|
+
# Case 8 — empty chain.
|
|
112
|
+
ok, idx, _ = run_case("empty chain -> -1", [], 76956.7, 0.3, expect_index=False)
|
|
113
|
+
results.append(ok)
|
|
114
|
+
|
|
115
|
+
passed = sum(results)
|
|
116
|
+
total = len(results)
|
|
117
|
+
print(f"\n{passed}/{total} passed")
|
|
118
|
+
sys.exit(0 if passed == total else 1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -402,6 +402,10 @@ class Dhan(BrokerBase):
|
|
|
402
402
|
self._global_market_data_rate_limit_lock_key = "dhan:market_data:lock"
|
|
403
403
|
self._historical_audit_redis = None
|
|
404
404
|
self._historical_audit_redis_db = 1
|
|
405
|
+
self._audit_enabled_cache: bool = False
|
|
406
|
+
self._audit_enabled_cache_ts: float = 0.0
|
|
407
|
+
self._audit_pid: str = str(os.getpid())
|
|
408
|
+
self._audit_proc_name: str = os.path.basename(sys.argv[0]) if sys.argv and sys.argv[0] else ""
|
|
405
409
|
atexit.register(self._mark_process_shutting_down)
|
|
406
410
|
|
|
407
411
|
trading_logger.log_info(
|
|
@@ -979,53 +983,51 @@ class Dhan(BrokerBase):
|
|
|
979
983
|
parsed = parsed.replace(hour=23, minute=59, second=59, microsecond=999000)
|
|
980
984
|
return parsed.isoformat(timespec="milliseconds")
|
|
981
985
|
|
|
982
|
-
def
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
986
|
+
def _audit_dhan_call(self, call_type: str, fields: dict) -> None:
|
|
987
|
+
"""Unified Redis stream audit for all Dhan API call types and errors.
|
|
988
|
+
|
|
989
|
+
call_type: "historical" | "quote" | "stream" | "error" | "stream_disconnect"
|
|
990
|
+
Toggle live (no restart): redis-cli -n 1 SET audit:dhan:enabled 1|0
|
|
991
|
+
"""
|
|
992
|
+
now = time.monotonic()
|
|
993
|
+
if now - self._audit_enabled_cache_ts < 5.0:
|
|
994
|
+
if not self._audit_enabled_cache:
|
|
995
|
+
return
|
|
996
|
+
else:
|
|
997
|
+
try:
|
|
998
|
+
val = self._get_historical_audit_redis().get("audit:dhan:enabled")
|
|
999
|
+
self._audit_enabled_cache = val in (b"1", "1")
|
|
1000
|
+
except Exception:
|
|
1001
|
+
self._audit_enabled_cache = False
|
|
1002
|
+
self._audit_enabled_cache_ts = now
|
|
1003
|
+
if not self._audit_enabled_cache:
|
|
1004
|
+
return
|
|
990
1005
|
try:
|
|
991
1006
|
audit_redis = self._get_historical_audit_redis()
|
|
992
1007
|
tz_ist = pytz.timezone("Asia/Kolkata")
|
|
993
1008
|
now_ist = dt.datetime.now(tz_ist)
|
|
994
1009
|
day_key = now_ist.strftime("%Y%m%d")
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1010
|
+
ttl = 60 * 60 * 24 * 45
|
|
1011
|
+
stream_key = f"audit:dhan:{call_type}:req:v1:{day_key}"
|
|
1012
|
+
combined_key = f"audit:dhan:all:req:v1:{day_key}"
|
|
1013
|
+
count_key = f"audit:dhan:{call_type}:req:count:day:{day_key}"
|
|
1014
|
+
entry = {
|
|
1015
|
+
"ts_ist": now_ist.isoformat(timespec="milliseconds"),
|
|
1016
|
+
"call_type": call_type,
|
|
1017
|
+
"pid": self._audit_pid,
|
|
1018
|
+
"proc": self._audit_proc_name,
|
|
1019
|
+
**{k: str(v) for k, v in fields.items()},
|
|
1020
|
+
}
|
|
1006
1021
|
pipe = audit_redis.pipeline()
|
|
1007
|
-
pipe.incr(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
pipe.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
"ts_ist": ts_ist,
|
|
1014
|
-
"symbol": symbol_str,
|
|
1015
|
-
"security_id": str(security_id),
|
|
1016
|
-
"interval": str(interval),
|
|
1017
|
-
"from_date": from_dt_ist,
|
|
1018
|
-
"to_date": to_dt_ist,
|
|
1019
|
-
},
|
|
1020
|
-
maxlen=500000,
|
|
1021
|
-
approximate=True,
|
|
1022
|
-
)
|
|
1023
|
-
pipe.expire(total_key, ttl_seconds)
|
|
1024
|
-
pipe.expire(per_symbol_key, ttl_seconds)
|
|
1025
|
-
pipe.expire(stream_key, ttl_seconds)
|
|
1022
|
+
pipe.incr(count_key)
|
|
1023
|
+
pipe.xadd(stream_key, entry, maxlen=500000, approximate=True)
|
|
1024
|
+
pipe.xadd(combined_key, entry, maxlen=1000000, approximate=True)
|
|
1025
|
+
pipe.expire(count_key, ttl)
|
|
1026
|
+
pipe.expire(stream_key, ttl)
|
|
1027
|
+
pipe.expire(combined_key, ttl)
|
|
1026
1028
|
pipe.execute()
|
|
1027
1029
|
except Exception as e:
|
|
1028
|
-
trading_logger.log_warning("Failed to audit Dhan
|
|
1030
|
+
trading_logger.log_warning(f"Failed to audit Dhan {call_type} call", {"error": str(e)})
|
|
1029
1031
|
|
|
1030
1032
|
def _fetch_dhan_historical(
|
|
1031
1033
|
self,
|
|
@@ -1060,13 +1062,13 @@ class Dhan(BrokerBase):
|
|
|
1060
1062
|
url = self.api.base_url + "/charts/intraday"
|
|
1061
1063
|
|
|
1062
1064
|
self._wait_for_historical_rate_limit()
|
|
1063
|
-
self.
|
|
1064
|
-
symbol
|
|
1065
|
-
security_id
|
|
1066
|
-
interval
|
|
1067
|
-
from_date
|
|
1068
|
-
to_date
|
|
1069
|
-
)
|
|
1065
|
+
self._audit_dhan_call("historical", {
|
|
1066
|
+
"symbol": symbol or "",
|
|
1067
|
+
"security_id": security_id,
|
|
1068
|
+
"interval": periodicity,
|
|
1069
|
+
"from_date": from_date,
|
|
1070
|
+
"to_date": to_date,
|
|
1071
|
+
})
|
|
1070
1072
|
response = self.api.session.post(url, headers=self.api.header, timeout=self.api.timeout, data=json.dumps(payload))
|
|
1071
1073
|
return self.api._parse_response(response)
|
|
1072
1074
|
|
|
@@ -1809,14 +1811,22 @@ class Dhan(BrokerBase):
|
|
|
1809
1811
|
continue
|
|
1810
1812
|
historical_data_list.sort(key=lambda x: x.date)
|
|
1811
1813
|
else:
|
|
1814
|
+
_hist_err = _extract_dhan_error_details(data)
|
|
1812
1815
|
trading_logger.log_error(
|
|
1813
1816
|
"No data from Dhan historical API",
|
|
1814
1817
|
None,
|
|
1815
1818
|
{
|
|
1816
1819
|
"symbol": long_symbol,
|
|
1817
|
-
**
|
|
1820
|
+
**_hist_err,
|
|
1818
1821
|
},
|
|
1819
1822
|
)
|
|
1823
|
+
self._audit_dhan_call("error", {
|
|
1824
|
+
"call_type": "historical",
|
|
1825
|
+
"symbol": long_symbol,
|
|
1826
|
+
"security_id": security_id,
|
|
1827
|
+
"exchange_segment": exchange_segment,
|
|
1828
|
+
**_hist_err,
|
|
1829
|
+
})
|
|
1820
1830
|
|
|
1821
1831
|
if periodicity in {"1d", "d"} and date_end_dt.date() == get_tradingapi_now().date():
|
|
1822
1832
|
# Refresh today's daily bar from 1m data (up-to-date partial OHLC), matching Shoonya-style behavior.
|
|
@@ -1949,6 +1959,11 @@ class Dhan(BrokerBase):
|
|
|
1949
1959
|
raise BrokerConnectionError("Dhan API not initialized")
|
|
1950
1960
|
|
|
1951
1961
|
self._wait_for_quote_rate_limit()
|
|
1962
|
+
self._audit_dhan_call("quote", {
|
|
1963
|
+
"symbol": long_symbol,
|
|
1964
|
+
"security_id": security_id,
|
|
1965
|
+
"exchange_segment": exchange_segment,
|
|
1966
|
+
})
|
|
1952
1967
|
out = self.api.quote_data(securities={exchange_segment: [int(security_id)]})
|
|
1953
1968
|
if out and out.get("status") == "success":
|
|
1954
1969
|
response_data = out.get("data", {}) or {}
|
|
@@ -1971,9 +1986,17 @@ class Dhan(BrokerBase):
|
|
|
1971
1986
|
market_feed.ask_volume = int(sell_qty[0].get("quantity", 0) or 0)
|
|
1972
1987
|
return market_feed
|
|
1973
1988
|
|
|
1989
|
+
_quote_err = _extract_dhan_error_details(out)
|
|
1990
|
+
self._audit_dhan_call("error", {
|
|
1991
|
+
"call_type": "quote",
|
|
1992
|
+
"symbol": long_symbol,
|
|
1993
|
+
"security_id": security_id,
|
|
1994
|
+
"exchange_segment": exchange_segment,
|
|
1995
|
+
**_quote_err,
|
|
1996
|
+
})
|
|
1974
1997
|
trading_logger.log_warning(
|
|
1975
1998
|
"Dhan quote_data unavailable",
|
|
1976
|
-
{"long_symbol": long_symbol, **
|
|
1999
|
+
{"long_symbol": long_symbol, **_quote_err},
|
|
1977
2000
|
)
|
|
1978
2001
|
|
|
1979
2002
|
return market_feed
|
|
@@ -2438,6 +2461,18 @@ class Dhan(BrokerBase):
|
|
|
2438
2461
|
# Call synchronously; avoids RuntimeError from asyncio.to_thread()
|
|
2439
2462
|
# when concurrent.futures thread pool is torn down by another component.
|
|
2440
2463
|
self._wait_for_stream_request_rate_limit()
|
|
2464
|
+
self._audit_dhan_call("stream", {
|
|
2465
|
+
"request_code": message.get("RequestCode", ""),
|
|
2466
|
+
"instrument_count": len(message.get("InstrumentList", [])),
|
|
2467
|
+
"operation": "subscribe",
|
|
2468
|
+
"symbols": ",".join(
|
|
2469
|
+
stream_symbol_map.get(
|
|
2470
|
+
(_DHAN_FEED_SEGMENT_MAP.get(instr.get("ExchangeSegment", ""), ("", -1))[1], int(instr.get("SecurityId", -1))),
|
|
2471
|
+
(instr.get("SecurityId", ""), ""),
|
|
2472
|
+
)[0]
|
|
2473
|
+
for instr in message.get("InstrumentList", [])
|
|
2474
|
+
),
|
|
2475
|
+
})
|
|
2441
2476
|
await ws.send(json.dumps(message))
|
|
2442
2477
|
while self._streaming:
|
|
2443
2478
|
raw = await ws.recv()
|
|
@@ -2462,6 +2497,10 @@ class Dhan(BrokerBase):
|
|
|
2462
2497
|
trading_logger.log_info("Dhan streaming thread closed cleanly")
|
|
2463
2498
|
else:
|
|
2464
2499
|
trading_logger.log_error("Dhan streaming thread error", e, {})
|
|
2500
|
+
self._audit_dhan_call("stream_disconnect", {
|
|
2501
|
+
"error": str(e),
|
|
2502
|
+
"error_type": type(e).__name__,
|
|
2503
|
+
})
|
|
2465
2504
|
finally:
|
|
2466
2505
|
try:
|
|
2467
2506
|
if loop is not None and not loop.is_closed():
|
|
@@ -2500,6 +2539,18 @@ class Dhan(BrokerBase):
|
|
|
2500
2539
|
async def _send_incremental_messages():
|
|
2501
2540
|
for message in delta_messages:
|
|
2502
2541
|
self._wait_for_stream_request_rate_limit()
|
|
2542
|
+
self._audit_dhan_call("stream", {
|
|
2543
|
+
"request_code": message.get("RequestCode", ""),
|
|
2544
|
+
"instrument_count": len(message.get("InstrumentList", [])),
|
|
2545
|
+
"operation": "subscribe_delta",
|
|
2546
|
+
"symbols": ",".join(
|
|
2547
|
+
stream_symbol_map.get(
|
|
2548
|
+
(_DHAN_FEED_SEGMENT_MAP.get(instr.get("ExchangeSegment", ""), ("", -1))[1], int(instr.get("SecurityId", -1))),
|
|
2549
|
+
(instr.get("SecurityId", ""), ""),
|
|
2550
|
+
)[0]
|
|
2551
|
+
for instr in message.get("InstrumentList", [])
|
|
2552
|
+
),
|
|
2553
|
+
})
|
|
2503
2554
|
await ws.send(json.dumps(message))
|
|
2504
2555
|
|
|
2505
2556
|
future = asyncio.run_coroutine_threadsafe(_send_incremental_messages(), loop)
|
|
@@ -45,13 +45,11 @@ def _patch_py5paisa_for_mom(api) -> None:
|
|
|
45
45
|
call accumulates stale body keys across calls; the polluted body
|
|
46
46
|
makes the 5paisa server hang past 5s. Reset to a fresh deep copy
|
|
47
47
|
before each MOM call.
|
|
48
|
-
3. order_request() does
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
the duration of the MOM call so the subscript succeeds and
|
|
54
|
-
res["body"] is returned normally.
|
|
48
|
+
3. order_request() does res["body"]["Message"] for MOM, but the MOM
|
|
49
|
+
response has no "Message" key — KeyError is raised at the subscript,
|
|
50
|
+
swallowed by the bare except, and the function silently returns None.
|
|
51
|
+
Replace order_request for the MOM case to extract res["body"] directly
|
|
52
|
+
without touching "Message".
|
|
55
53
|
"""
|
|
56
54
|
if getattr(api, "_tradingapi_patched", False):
|
|
57
55
|
return
|
|
@@ -64,28 +62,35 @@ def _patch_py5paisa_for_mom(api) -> None:
|
|
|
64
62
|
except Exception:
|
|
65
63
|
pass
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
original_post = api.session.post
|
|
65
|
+
original_order_request = api.order_request
|
|
69
66
|
|
|
70
|
-
def
|
|
71
|
-
|
|
67
|
+
def safe_order_request(req_type):
|
|
68
|
+
if req_type != "MOM":
|
|
69
|
+
return original_order_request(req_type)
|
|
72
70
|
try:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
api.payload["body"]["ClientCode"] = api.client_code
|
|
72
|
+
api.payload["head"]["key"] = api.USER_KEY
|
|
73
|
+
from py5paisa.const import HEADERS
|
|
74
|
+
token = api.access_token or api.Jwt_token
|
|
75
|
+
headers = dict(HEADERS)
|
|
76
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
77
|
+
headers["5Paisa-API-Uid"] = api.APIUID
|
|
78
|
+
res = api.session.post(api.MULTIORDERMARGIN_ROUTE, json=api.payload, headers=headers).json()
|
|
79
|
+
api.payload = copy.deepcopy(GENERIC_PAYLOAD)
|
|
80
|
+
return res.get("body")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
from . import trading_logger
|
|
83
|
+
trading_logger.log_warning("py5paisa MOM request failed", {"error": repr(e)})
|
|
84
|
+
api.payload = copy.deepcopy(GENERIC_PAYLOAD)
|
|
85
|
+
return None
|
|
80
86
|
|
|
81
87
|
def fixed_multi_order_Margin(**order):
|
|
82
88
|
api.payload = copy.deepcopy(GENERIC_PAYLOAD)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
finally:
|
|
87
|
-
api.session.post = original_post # type: ignore[method-assign]
|
|
89
|
+
for k, v in order.items():
|
|
90
|
+
api.payload["body"][k] = v
|
|
91
|
+
return safe_order_request("MOM")
|
|
88
92
|
|
|
93
|
+
api.order_request = safe_order_request
|
|
89
94
|
api.multi_order_Margin = fixed_multi_order_Margin
|
|
90
95
|
api._tradingapi_patched = True
|
|
91
96
|
|
|
@@ -3242,116 +3242,132 @@ def find_option_with_delta(
|
|
|
3242
3242
|
chain_strikes = [_strike(s) for s in option_chain] if option_chain else []
|
|
3243
3243
|
strike_lo = min(chain_strikes) if chain_strikes else float("nan")
|
|
3244
3244
|
strike_hi = max(chain_strikes) if chain_strikes else float("nan")
|
|
3245
|
+
n = len(option_chain)
|
|
3245
3246
|
total_delta_checks = 0
|
|
3246
3247
|
valid_delta_count = 0
|
|
3247
3248
|
nan_delta_count = 0
|
|
3248
|
-
#
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
brokers, option_chain[
|
|
3249
|
+
nan_delta_strikes = [] # track strikes where delta could not be calculated
|
|
3250
|
+
# Cache so seeding sweep + binary search + linear fallback never recompute the same strike.
|
|
3251
|
+
delta_cache: dict[int, float] = {}
|
|
3252
|
+
|
|
3253
|
+
def _delta_at(idx: int) -> float:
|
|
3254
|
+
nonlocal total_delta_checks, valid_delta_count, nan_delta_count
|
|
3255
|
+
if idx in delta_cache:
|
|
3256
|
+
return delta_cache[idx]
|
|
3257
|
+
d = calculate_delta(
|
|
3258
|
+
brokers, option_chain[idx], price_f, market_close_time, exchange=opt_exchange, mds=mds, as_of=as_of
|
|
3258
3259
|
)
|
|
3259
|
-
|
|
3260
|
+
total_delta_checks += 1
|
|
3261
|
+
if d is None or math.isnan(d):
|
|
3262
|
+
d = float("nan")
|
|
3260
3263
|
nan_delta_count += 1
|
|
3264
|
+
nan_delta_strikes.append(_strike(option_chain[idx]))
|
|
3261
3265
|
else:
|
|
3262
3266
|
valid_delta_count += 1
|
|
3263
|
-
|
|
3267
|
+
delta_cache[idx] = d
|
|
3268
|
+
return d
|
|
3264
3269
|
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
)
|
|
3272
|
-
if math.isnan(delta_2):
|
|
3273
|
-
nan_delta_count += 1
|
|
3270
|
+
def _consider(idx: int, abs_delta: float) -> None:
|
|
3271
|
+
nonlocal best_index, best_delta
|
|
3272
|
+
if return_lower_delta:
|
|
3273
|
+
if abs_delta <= target_delta and abs_delta > best_delta:
|
|
3274
|
+
best_delta = abs_delta
|
|
3275
|
+
best_index = idx
|
|
3274
3276
|
else:
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
+
if abs_delta >= target_delta and abs_delta < best_delta:
|
|
3278
|
+
best_delta = abs_delta
|
|
3279
|
+
best_index = idx
|
|
3277
3280
|
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
f"find_option_with_delta: no valid option strike found (direction inference failed). "
|
|
3282
|
-
f"price_f={price_f} target_delta={target_delta} return_lower_delta={return_lower_delta} "
|
|
3283
|
-
f"chain_len={len(option_chain)} strike_range=[{strike_lo}, {strike_hi}] "
|
|
3284
|
-
f"delta_1={delta_1} delta_2={delta_2} "
|
|
3285
|
-
f"mid_strike={_strike(option_chain[mid]) if option_chain and mid < len(option_chain) else None} "
|
|
3286
|
-
f"valid_delta_count={valid_delta_count} nan_delta_count={nan_delta_count} total_delta_checks={total_delta_checks}"
|
|
3281
|
+
if n == 0:
|
|
3282
|
+
trading_logger.log_warning(
|
|
3283
|
+
f"find_option_with_delta: empty option_chain. price_f={price_f} target_delta={target_delta}"
|
|
3287
3284
|
)
|
|
3285
|
+
return -1
|
|
3286
|
+
|
|
3287
|
+
# --- Change 1: robust direction seeding via spread-out probes ---
|
|
3288
|
+
seed_fractions = [0.1, 0.25, 0.5, 0.75, 0.9]
|
|
3289
|
+
seed_indices = sorted({min(n - 1, max(0, int(round(f * (n - 1))))) for f in seed_fractions})
|
|
3290
|
+
seed_samples: list[tuple[int, float]] = [] # (idx, |delta|)
|
|
3291
|
+
for idx in seed_indices:
|
|
3292
|
+
d = _delta_at(idx)
|
|
3293
|
+
if not math.isnan(d):
|
|
3294
|
+
ad = abs(d)
|
|
3295
|
+
seed_samples.append((idx, ad))
|
|
3296
|
+
_consider(idx, ad)
|
|
3297
|
+
|
|
3298
|
+
if len(seed_samples) < 2:
|
|
3299
|
+
# Too few seeds; fall through to linear scan over the whole chain.
|
|
3300
|
+
for idx in range(n):
|
|
3301
|
+
d = _delta_at(idx)
|
|
3302
|
+
if math.isnan(d):
|
|
3303
|
+
continue
|
|
3304
|
+
_consider(idx, abs(d))
|
|
3305
|
+
if best_index < 0:
|
|
3306
|
+
trading_logger.log_warning(
|
|
3307
|
+
f"find_option_with_delta: no valid option strike found (linear fallback after seed failure). "
|
|
3308
|
+
f"price_f={price_f} target_delta={target_delta} return_lower_delta={return_lower_delta} "
|
|
3309
|
+
f"chain_len={n} strike_range=[{strike_lo}, {strike_hi}] "
|
|
3310
|
+
f"valid_delta_count={valid_delta_count} nan_delta_count={nan_delta_count} "
|
|
3311
|
+
f"total_delta_checks={total_delta_checks} nan_delta_strikes={sorted(set(nan_delta_strikes))}"
|
|
3312
|
+
)
|
|
3288
3313
|
return best_index
|
|
3289
3314
|
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
# and use delta_2's position vis_a-vis target_delta to determine the direction of move to identity next best delta.
|
|
3293
|
-
move_right_on_nan_delta = True
|
|
3294
|
-
if abs(delta_2) > target_delta and increasing:
|
|
3295
|
-
move_right_on_nan_delta = False
|
|
3296
|
-
elif abs(delta_2) > target_delta and not increasing:
|
|
3297
|
-
move_right_on_nan_delta = True
|
|
3298
|
-
elif abs(delta_2) < target_delta and increasing:
|
|
3299
|
-
move_right_on_nan_delta = True
|
|
3300
|
-
elif abs(delta_2) < target_delta and not increasing:
|
|
3301
|
-
move_right_on_nan_delta = False
|
|
3315
|
+
# Determine monotonicity from lowest- vs. highest-strike valid seeds.
|
|
3316
|
+
increasing = seed_samples[-1][1] > seed_samples[0][1]
|
|
3302
3317
|
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
delta = calculate_delta(
|
|
3306
|
-
brokers, option_chain[mid], price_f, market_close_time, exchange=opt_exchange, mds=mds, as_of=as_of
|
|
3307
|
-
)
|
|
3308
|
-
total_delta_checks += 1
|
|
3309
|
-
delta = abs(delta) # always select an option using the absolute value of delta.
|
|
3318
|
+
# --- Change 2: NaN at mid -> scan to nearest valid neighbor instead of shrinking range ---
|
|
3319
|
+
NAN_SCAN_CAP = 5
|
|
3310
3320
|
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
else:
|
|
3319
|
-
right = mid - 1 # Keep [left, mid-1]
|
|
3320
|
-
else:
|
|
3321
|
-
if move_right_on_nan_delta:
|
|
3322
|
-
left = mid + 1
|
|
3323
|
-
else:
|
|
3324
|
-
right = mid - 1
|
|
3325
|
-
continue
|
|
3326
|
-
valid_delta_count += 1
|
|
3321
|
+
def _nearest_valid(mid: int, lo: int, hi: int) -> int:
|
|
3322
|
+
# Returns idx of nearest valid delta within [lo, hi], or -1 if none within cap.
|
|
3323
|
+
for step in range(1, NAN_SCAN_CAP + 1):
|
|
3324
|
+
for cand in (mid - step, mid + step):
|
|
3325
|
+
if lo <= cand <= hi and not math.isnan(_delta_at(cand)):
|
|
3326
|
+
return cand
|
|
3327
|
+
return -1
|
|
3327
3328
|
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
if
|
|
3335
|
-
|
|
3336
|
-
|
|
3329
|
+
while left <= right:
|
|
3330
|
+
mid = (left + right) // 2
|
|
3331
|
+
d = _delta_at(mid)
|
|
3332
|
+
eff_idx = mid
|
|
3333
|
+
if math.isnan(d):
|
|
3334
|
+
alt = _nearest_valid(mid, left, right)
|
|
3335
|
+
if alt < 0:
|
|
3336
|
+
# Wide NaN band — bail to linear fallback below.
|
|
3337
|
+
break
|
|
3338
|
+
eff_idx = alt
|
|
3339
|
+
d = delta_cache[alt]
|
|
3340
|
+
ad = abs(d)
|
|
3341
|
+
_consider(eff_idx, ad)
|
|
3337
3342
|
|
|
3338
|
-
# Adjust binary search range based on delta trend
|
|
3339
3343
|
if increasing:
|
|
3340
|
-
if
|
|
3341
|
-
right =
|
|
3344
|
+
if ad > target_delta:
|
|
3345
|
+
right = eff_idx - 1
|
|
3342
3346
|
else:
|
|
3343
|
-
left =
|
|
3347
|
+
left = eff_idx + 1
|
|
3344
3348
|
else:
|
|
3345
|
-
if
|
|
3346
|
-
left =
|
|
3349
|
+
if ad > target_delta:
|
|
3350
|
+
left = eff_idx + 1
|
|
3347
3351
|
else:
|
|
3348
|
-
right =
|
|
3352
|
+
right = eff_idx - 1
|
|
3353
|
+
|
|
3354
|
+
# --- Change 3: linear-scan fallback if binary search failed to land a best ---
|
|
3355
|
+
if best_index < 0:
|
|
3356
|
+
for idx in range(n):
|
|
3357
|
+
if idx in delta_cache:
|
|
3358
|
+
d = delta_cache[idx]
|
|
3359
|
+
else:
|
|
3360
|
+
d = _delta_at(idx)
|
|
3361
|
+
if math.isnan(d):
|
|
3362
|
+
continue
|
|
3363
|
+
_consider(idx, abs(d))
|
|
3349
3364
|
|
|
3350
3365
|
if best_index < 0:
|
|
3351
3366
|
trading_logger.log_warning(
|
|
3352
3367
|
f"find_option_with_delta: no valid option strike found. "
|
|
3353
|
-
f"price_f={price_f} chain_len={
|
|
3354
|
-
f"valid_delta_count={valid_delta_count} nan_delta_count={nan_delta_count} total_delta_checks={total_delta_checks}"
|
|
3368
|
+
f"price_f={price_f} chain_len={n} strike_range=[{strike_lo}, {strike_hi}] "
|
|
3369
|
+
f"valid_delta_count={valid_delta_count} nan_delta_count={nan_delta_count} total_delta_checks={total_delta_checks} "
|
|
3370
|
+
f"nan_delta_strikes={sorted(set(nan_delta_strikes))}"
|
|
3355
3371
|
)
|
|
3356
3372
|
return best_index
|
|
3357
3373
|
|
|
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
|
|
File without changes
|
|
File without changes
|