nsepython 2.97__py3-none-any.whl → 2.98__py3-none-any.whl
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.
- nsepython/rahu.py +1103 -311
- {nsepython-2.97.dist-info → nsepython-2.98.dist-info}/METADATA +4 -3
- nsepython-2.98.dist-info/RECORD +7 -0
- {nsepython-2.97.dist-info → nsepython-2.98.dist-info}/WHEEL +1 -1
- nsepython-2.97.dist-info/RECORD +0 -7
- {nsepython-2.97.dist-info → nsepython-2.98.dist-info}/licenses/LICENSE +0 -0
- {nsepython-2.97.dist-info → nsepython-2.98.dist-info}/top_level.txt +0 -0
nsepython/rahu.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os,sys
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
#os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
|
3
|
+
#sys.path.insert(1, os.path.join(sys.path[0], '..'))
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
import pandas as pd
|
|
@@ -9,84 +9,279 @@ import random
|
|
|
9
9
|
import datetime,time
|
|
10
10
|
import logging
|
|
11
11
|
import re
|
|
12
|
+
import io
|
|
12
13
|
import urllib.parse
|
|
13
14
|
|
|
14
15
|
mode ='local'
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Transport
|
|
19
|
+
#
|
|
20
|
+
# NSE's site is fronted by Akamai Bot Manager, which fingerprints the TLS/JA3
|
|
21
|
+
# handshake of the client. A plain `requests.Session()` (or a shelled-out
|
|
22
|
+
# plain `curl`) gets blocked outright (HTTP 403 on the homepage itself) --
|
|
23
|
+
# this is NOT "requests is blocked in India", it's a bot-detection block that
|
|
24
|
+
# has nothing to do with geography. curl_cffi is a requests-compatible
|
|
25
|
+
# Session that impersonates a real Chrome TLS fingerprint, which clears this
|
|
26
|
+
# wall while remaining a pure Python HTTP client (no shell-out, no browser).
|
|
27
|
+
#
|
|
28
|
+
# curl_cffi is therefore the one and only transport nsefetch() uses now. It
|
|
29
|
+
# is a hard dependency (see requirements.txt/setup.py) because it's what
|
|
30
|
+
# makes the large majority of this library's functions work at all against
|
|
31
|
+
# the live site today.
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from curl_cffi.requests import Session as _CurlSession
|
|
36
|
+
_CURL_CFFI_OK = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
_CURL_CFFI_OK = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NSEEndpointError(Exception):
|
|
42
|
+
"""Raised by nsefetch() when NSE's site cannot be reached, or responds
|
|
43
|
+
with something other than usable JSON (blocked, retired endpoint, rate
|
|
44
|
+
limited, server error, etc).
|
|
45
|
+
|
|
46
|
+
Older versions of this library silently swallowed these failures and
|
|
47
|
+
returned `{}`, which just pushed the problem one level down into a
|
|
48
|
+
confusing `KeyError`/`AttributeError` in whatever function called
|
|
49
|
+
nsefetch() (see github.com/aeron7/nsepython issues #74, #75, and
|
|
50
|
+
nsepythonserver #6). Raising a descriptive exception here instead makes
|
|
51
|
+
the real failure visible immediately instead of as a downstream KeyError.
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_nse_session = None
|
|
57
|
+
_nse_warmed = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_nse_session():
|
|
61
|
+
"""Return the shared, warmed-up curl_cffi session used by nsefetch().
|
|
62
|
+
|
|
63
|
+
The warm-up (visiting the homepage, then the option-chain page) is what
|
|
64
|
+
gets NSE's Akamai Bot Manager to hand out the `nsit`/`_abck`/`ak_bmsc`/
|
|
65
|
+
`bm_sv` cookies that most JSON API calls expect to see on the request.
|
|
66
|
+
"""
|
|
67
|
+
global _nse_session, _nse_warmed
|
|
68
|
+
|
|
69
|
+
if not _CURL_CFFI_OK:
|
|
70
|
+
raise ImportError(
|
|
71
|
+
"nsepython needs curl_cffi to talk to the real nseindia.com site. "
|
|
72
|
+
"A plain `requests` session (and plain `curl`) gets blocked by "
|
|
73
|
+
"NSE's Akamai Bot Manager purely on TLS fingerprint, regardless "
|
|
74
|
+
"of where you are. Install it with: pip install curl_cffi"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if _nse_session is None:
|
|
78
|
+
_nse_session = _CurlSession(impersonate="chrome124")
|
|
79
|
+
|
|
80
|
+
if not _nse_warmed:
|
|
81
|
+
try:
|
|
82
|
+
_nse_session.get("https://www.nseindia.com", headers=headers, timeout=20)
|
|
83
|
+
time.sleep(1.2)
|
|
84
|
+
_nse_session.get("https://www.nseindia.com/option-chain", headers=headers, timeout=20)
|
|
85
|
+
time.sleep(0.8)
|
|
86
|
+
_nse_warmed = True
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logging.warning("NSE session warm-up failed/partial: %s", e)
|
|
89
|
+
|
|
90
|
+
return _nse_session
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _equity_stockindices_fallback(session, api_headers):
|
|
94
|
+
"""`/api/equity-stockIndices?index=SECURITIES IN F%26O` -- the F&O
|
|
95
|
+
securities list used by fnolist()/nsetools_get_quote()/
|
|
96
|
+
nse_get_advances_declines()/nse_get_top_losers()/nse_get_top_gainers()/
|
|
97
|
+
nse_custom_function_secfno() -- is a retired route on the live site
|
|
98
|
+
(confirmed HTTP 404, NSE's own "Resource not found" page, with or
|
|
99
|
+
without a fully browser-solved Akamai cookie jar).
|
|
100
|
+
|
|
101
|
+
`/api/market-data-pre-open?key=FO` carries the same per-symbol pChange/
|
|
102
|
+
lastPrice/etc information for the F&O universe, so we transparently
|
|
103
|
+
rewrite the request to that endpoint and reshape its response back into
|
|
104
|
+
the old `{"data": [{"symbol":..., "pChange":..., ...}]}` shape every
|
|
105
|
+
existing caller above already expects -- they keep working unchanged.
|
|
106
|
+
"""
|
|
107
|
+
r = session.get(
|
|
108
|
+
"https://www.nseindia.com/api/market-data-pre-open?key=FO",
|
|
109
|
+
headers=api_headers, timeout=30,
|
|
110
|
+
)
|
|
111
|
+
if r.status_code != 200:
|
|
112
|
+
raise NSEEndpointError(
|
|
113
|
+
f"equity-stockIndices fallback (market-data-pre-open) failed: HTTP {r.status_code}"
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
raw = r.json()
|
|
117
|
+
except ValueError:
|
|
118
|
+
raise NSEEndpointError("equity-stockIndices fallback returned a non-JSON body")
|
|
119
|
+
|
|
120
|
+
reshaped = []
|
|
121
|
+
for item in raw.get("data", []):
|
|
122
|
+
m = item.get("metadata", {}) or {}
|
|
123
|
+
if not m.get("symbol"):
|
|
124
|
+
continue
|
|
125
|
+
reshaped.append({
|
|
126
|
+
"symbol": m.get("symbol", ""),
|
|
127
|
+
"pChange": m.get("pChange", 0),
|
|
128
|
+
"lastPrice": m.get("lastPrice", 0),
|
|
129
|
+
"change": m.get("change", 0),
|
|
130
|
+
"previousClose": m.get("previousClose", 0),
|
|
131
|
+
"yearHigh": m.get("yearHigh", 0),
|
|
132
|
+
"yearLow": m.get("yearLow", 0),
|
|
133
|
+
"totalTradedValue": m.get("totalTurnover", 0),
|
|
134
|
+
"totalTradedVolume": m.get("finalQuantity", 0),
|
|
135
|
+
})
|
|
136
|
+
return {"data": reshaped}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def nsefetch(payload: str):
|
|
140
|
+
"""Fetch a nseindia.com JSON API URL through a warmed-up curl_cffi
|
|
141
|
+
session, retrying once with a fresh warm-up if the first attempt looks
|
|
142
|
+
blocked (stale/expired Akamai cookies), and raising NSEEndpointError
|
|
143
|
+
(instead of silently returning `{}`) if it still can't get real JSON
|
|
144
|
+
back. `mode` is kept only for backwards compatibility with older
|
|
145
|
+
versions of this file; both 'local' and 'vpn' use this same transport
|
|
146
|
+
now, since the previous mode='vpn' plain-curl/os.popen() implementation
|
|
147
|
+
was both broken against the current Akamai wall *and* a command-injection
|
|
148
|
+
risk (see github.com/aeron7/nsepython issue #73).
|
|
149
|
+
"""
|
|
150
|
+
global _nse_warmed
|
|
151
|
+
|
|
152
|
+
session = _get_nse_session()
|
|
153
|
+
api_headers = dict(headers)
|
|
154
|
+
api_headers.update({
|
|
155
|
+
"Accept": "application/json, text/plain, */*",
|
|
156
|
+
"Referer": "https://www.nseindia.com/option-chain",
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if "equity-stockIndices" in payload and "SECURITIES" in payload:
|
|
160
|
+
return _equity_stockindices_fallback(session, api_headers)
|
|
26
161
|
|
|
27
|
-
|
|
28
|
-
|
|
162
|
+
try:
|
|
163
|
+
r = session.get(payload, headers=api_headers, timeout=30)
|
|
164
|
+
if r.status_code in (401, 403, 404, 429, 503):
|
|
165
|
+
# Could just be a stale/expired Akamai cookie jar -- re-warm once
|
|
166
|
+
# and retry before giving up.
|
|
167
|
+
_nse_warmed = False
|
|
168
|
+
session = _get_nse_session()
|
|
169
|
+
r = session.get(payload, headers=api_headers, timeout=30)
|
|
29
170
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
raw = os.popen(cmd).read()
|
|
171
|
+
if r.status_code != 200:
|
|
172
|
+
raise NSEEndpointError(f"nsefetch: HTTP {r.status_code} for {payload}")
|
|
33
173
|
|
|
34
174
|
try:
|
|
35
|
-
return json
|
|
175
|
+
return r.json()
|
|
36
176
|
except ValueError:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
177
|
+
raise NSEEndpointError(
|
|
178
|
+
f"nsefetch: NSE returned a non-JSON body (length={len(r.text)}) for {payload}"
|
|
179
|
+
)
|
|
180
|
+
except NSEEndpointError:
|
|
181
|
+
raise
|
|
182
|
+
except Exception as e:
|
|
183
|
+
raise NSEEndpointError(f"nsefetch: request failed for {payload}: {e}")
|
|
43
184
|
|
|
44
|
-
if(mode=='local'):
|
|
45
|
-
def nsefetch(payload):
|
|
46
185
|
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Optional, lazily-imported Playwright cookie-harvest fallback.
|
|
188
|
+
#
|
|
189
|
+
# For most of the library, curl_cffi's TLS impersonation + the warm-up above
|
|
190
|
+
# is all that's needed -- it is NOT the same as "requests is blocked", and it
|
|
191
|
+
# is NOT, in practice, gated behind a real JS-solved Akamai sensor challenge
|
|
192
|
+
# for the endpoints this library actually calls today (verified live: a
|
|
193
|
+
# fully browser-solved cookie jar makes zero difference to the handful of
|
|
194
|
+
# genuinely-retired routes like /api/quote-equity or /api/equity-stockIndices
|
|
195
|
+
# -- they are simply dead/404, not JS-walled).
|
|
196
|
+
#
|
|
197
|
+
# This helper exists as a best-effort escape hatch for the rarer case where
|
|
198
|
+
# NSE *does* flip an endpoint to require a cookie only a real browser's JS
|
|
199
|
+
# engine can produce -- curl_cffi never executes JavaScript, so it cannot
|
|
200
|
+
# solve that kind of challenge itself. It is intentionally NOT imported at
|
|
201
|
+
# module load time and NOT wired automatically into nsefetch(): it is slow
|
|
202
|
+
# (it launches a real headless browser), and for the specific endpoints this
|
|
203
|
+
# library has found still blocked as of this writing (the historical
|
|
204
|
+
# bulk/block/short-deals and securityArchives routes), the block looks like a
|
|
205
|
+
# server-side 503/retirement rather than a missing-JS-cookie problem, so
|
|
206
|
+
# there's no evidence a browser visit would fix them either. Call
|
|
207
|
+
# nse_harvest_playwright_cookies() yourself, once, near the start of your
|
|
208
|
+
# script if you want to try it against an endpoint you believe is genuinely
|
|
209
|
+
# JS-walled; it injects the solved cookies into the same shared session
|
|
210
|
+
# nsefetch() uses for every call after that.
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def nse_harvest_playwright_cookies(url="https://www.nseindia.com/option-chain", timeout_ms=45000):
|
|
214
|
+
"""Launch a real headless Chromium (via Playwright), let it naturally
|
|
215
|
+
pass NSE's Akamai Bot Manager JS sensor challenge by visiting `url`, then
|
|
216
|
+
copy its solved cookie jar into the shared curl_cffi session nsefetch()
|
|
217
|
+
uses. Optional, best-effort, and NOT required for the vast majority of
|
|
218
|
+
this library's functions.
|
|
219
|
+
|
|
220
|
+
Requires: pip install playwright && playwright install chromium
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
from playwright.sync_api import sync_playwright
|
|
224
|
+
except ImportError as e:
|
|
225
|
+
raise ImportError(
|
|
226
|
+
"nse_harvest_playwright_cookies() needs Playwright to drive a "
|
|
227
|
+
"real browser. Install it with: pip install playwright && "
|
|
228
|
+
"playwright install chromium"
|
|
229
|
+
) from e
|
|
230
|
+
|
|
231
|
+
session = _get_nse_session()
|
|
232
|
+
harvested = {}
|
|
233
|
+
with sync_playwright() as p:
|
|
234
|
+
browser = p.chromium.launch(headless=True)
|
|
47
235
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# 'Sec-Fetch-User': '?1',
|
|
64
|
-
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
|
65
|
-
# 'Sec-Fetch-Site': 'none',
|
|
66
|
-
# 'Sec-Fetch-Mode': 'navigate',
|
|
67
|
-
# 'Accept-Encoding': 'gzip, deflate, br',
|
|
68
|
-
# 'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
|
|
69
|
-
# }
|
|
70
|
-
|
|
71
|
-
#Rahul_Mittal's entry
|
|
72
|
-
headers = {
|
|
73
|
-
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
74
|
-
"accept-language": "en-US,en;q=0.9,en-IN;q=0.8,en-GB;q=0.7",
|
|
75
|
-
"cache-control": "max-age=0",
|
|
76
|
-
"priority": "u=0, i",
|
|
77
|
-
"sec-ch-ua": '"Microsoft Edge";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
|
|
78
|
-
"sec-ch-ua-mobile": "?0",
|
|
79
|
-
"sec-ch-ua-platform": '"Windows"',
|
|
80
|
-
"sec-fetch-dest": "document",
|
|
81
|
-
"sec-fetch-mode": "navigate",
|
|
82
|
-
"sec-fetch-site": "none",
|
|
83
|
-
"sec-fetch-user": "?1",
|
|
84
|
-
"upgrade-insecure-requests": "1",
|
|
85
|
-
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0"
|
|
86
|
-
}
|
|
236
|
+
page = browser.new_page(user_agent=headers["User-Agent"])
|
|
237
|
+
page.goto("https://www.nseindia.com", timeout=timeout_ms)
|
|
238
|
+
page.wait_for_timeout(2000)
|
|
239
|
+
page.goto(url, timeout=timeout_ms)
|
|
240
|
+
page.wait_for_timeout(2000)
|
|
241
|
+
for c in page.context.cookies():
|
|
242
|
+
harvested[c["name"]] = c["value"]
|
|
243
|
+
finally:
|
|
244
|
+
browser.close()
|
|
245
|
+
|
|
246
|
+
for name, value in harvested.items():
|
|
247
|
+
try:
|
|
248
|
+
session.cookies.set(name, value, domain=".nseindia.com")
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
87
251
|
|
|
252
|
+
global _nse_warmed
|
|
253
|
+
_nse_warmed = True # don't let the next nsefetch() stomp these with a plain re-warm
|
|
254
|
+
return harvested
|
|
88
255
|
|
|
89
256
|
|
|
257
|
+
def _nse_fetch_csv_text(url: str) -> str:
|
|
258
|
+
"""Fetch a plain-text/CSV archive file through the shared curl_cffi
|
|
259
|
+
session (so these also benefit from the TLS-impersonation fix and don't
|
|
260
|
+
rely on plain `requests`/`pd.read_csv`'s bare urllib fetch, which
|
|
261
|
+
confirmed-live testing shows just hangs/times out against
|
|
262
|
+
nsearchives.nseindia.com, and is the less reliable of the two archive
|
|
263
|
+
hosts generally as NSE tightens Akamai enforcement over time)."""
|
|
264
|
+
session = _get_nse_session()
|
|
265
|
+
r = session.get(url, headers=headers, timeout=30)
|
|
266
|
+
if r.status_code != 200:
|
|
267
|
+
raise NSEEndpointError(f"nsefetch (csv): HTTP {r.status_code} for {url}")
|
|
268
|
+
return r.text
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
headers = {
|
|
272
|
+
'Connection': 'keep-alive',
|
|
273
|
+
'Cache-Control': 'max-age=0',
|
|
274
|
+
'DNT': '1',
|
|
275
|
+
'Upgrade-Insecure-Requests': '1',
|
|
276
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36',
|
|
277
|
+
'Sec-Fetch-User': '?1',
|
|
278
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
|
279
|
+
'Sec-Fetch-Site': 'none',
|
|
280
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
281
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
282
|
+
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
|
|
283
|
+
}
|
|
284
|
+
|
|
90
285
|
#Curl headers
|
|
91
286
|
curl_headers = ''' -H "authority: beta.nseindia.com" -H "cache-control: max-age=0" -H "dnt: 1" -H "upgrade-insecure-requests: 1" -H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36" -H "sec-fetch-user: ?1" -H "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" -H "sec-fetch-site: none" -H "sec-fetch-mode: navigate" -H "accept-encoding: gzip, deflate, br" -H "accept-language: en-US,en;q=0.9,hi;q=0.8" --compressed'''
|
|
92
287
|
|
|
@@ -102,17 +297,10 @@ def running_status():
|
|
|
102
297
|
|
|
103
298
|
#Getting FNO Symboles
|
|
104
299
|
def fnolist():
|
|
105
|
-
# df = pd.read_csv("https://www1.nseindia.com/content/fo/fo_mktlots.csv")
|
|
106
|
-
# return [x.strip(' ') for x in df.drop(df.index[3]).iloc[:,1].to_list()]
|
|
107
|
-
|
|
108
300
|
positions = nsefetch('https://www.nseindia.com/api/equity-stockIndices?index=SECURITIES%20IN%20F%26O')
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
i=0
|
|
113
|
-
for x in range(i, len(positions['data'])):
|
|
114
|
-
nselist=nselist+[positions['data'][x]['symbol']]
|
|
115
|
-
|
|
301
|
+
nselist = indices.copy()
|
|
302
|
+
for x in range(len(positions['data'])):
|
|
303
|
+
nselist.append(positions['data'][x]['symbol'])
|
|
116
304
|
return nselist
|
|
117
305
|
|
|
118
306
|
def nsesymbolpurify(symbol):
|
|
@@ -121,15 +309,46 @@ def nsesymbolpurify(symbol):
|
|
|
121
309
|
|
|
122
310
|
def nse_optionchain_scrapper(symbol):
|
|
123
311
|
symbol = nsesymbolpurify(symbol)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
312
|
+
# Using getSymbolDerivativesData as it provides all expiries and strikes in one go
|
|
313
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolDerivativesData&symbol={symbol}'
|
|
314
|
+
payload = nsefetch(url)
|
|
315
|
+
|
|
316
|
+
# Transformation to match the "data" structure expected by pcr and other functions
|
|
317
|
+
if payload and 'data' in payload:
|
|
318
|
+
new_data = []
|
|
319
|
+
# Group by strikePrice and expiryDate to create a combined CE/PE structure if possible,
|
|
320
|
+
# or just provide the raw list if the consumers can handle it.
|
|
321
|
+
# The current pcr() handles a list of entries where each has CE/PE keys OR is the entry itself.
|
|
322
|
+
|
|
323
|
+
# Actually, let's restructure it to be more compatible with the expected 'data' format:
|
|
324
|
+
# a list of dictionaries, each having 'strikePrice', 'expiryDate', 'CE', 'PE'.
|
|
325
|
+
combined = {}
|
|
326
|
+
for entry in payload['data']:
|
|
327
|
+
sp = entry.get('strikePrice')
|
|
328
|
+
ed = entry.get('expiryDate')
|
|
329
|
+
ot = entry.get('optionType')
|
|
330
|
+
if not sp or not ed or ot == 'XX': continue
|
|
331
|
+
|
|
332
|
+
key = (sp, ed)
|
|
333
|
+
if key not in combined:
|
|
334
|
+
combined[key] = {'strikePrice': sp, 'expiryDate': ed, 'CE': None, 'PE': None}
|
|
335
|
+
|
|
336
|
+
combined[key][ot] = entry
|
|
337
|
+
|
|
338
|
+
payload['data'] = list(combined.values())
|
|
339
|
+
|
|
128
340
|
return payload
|
|
129
341
|
|
|
130
342
|
|
|
131
343
|
def oi_chain_builder(symbol,expiry="latest",oi_mode="full"):
|
|
132
344
|
|
|
345
|
+
if expiry == "latest":
|
|
346
|
+
dates = expiry_list(symbol, type="list")
|
|
347
|
+
if dates:
|
|
348
|
+
expiry = dates[0]
|
|
349
|
+
else:
|
|
350
|
+
return pd.DataFrame(), 0.0, ""
|
|
351
|
+
|
|
133
352
|
payload = nse_optionchain_scrapper(symbol)
|
|
134
353
|
|
|
135
354
|
if(oi_mode=='compact'):
|
|
@@ -138,109 +357,329 @@ def oi_chain_builder(symbol,expiry="latest",oi_mode="full"):
|
|
|
138
357
|
col_names = ['CALLS_Chart','CALLS_OI','CALLS_Chng in OI','CALLS_Volume','CALLS_IV','CALLS_LTP','CALLS_Net Chng','CALLS_Bid Qty','CALLS_Bid Price','CALLS_Ask Price','CALLS_Ask Qty','Strike Price','PUTS_Bid Qty','PUTS_Bid Price','PUTS_Ask Price','PUTS_Ask Qty','PUTS_Net Chng','PUTS_LTP','PUTS_IV','PUTS_Volume','PUTS_Chng in OI','PUTS_OI','PUTS_Chart']
|
|
139
358
|
oi_data = pd.DataFrame(columns = col_names)
|
|
140
359
|
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
oi_row['CALLS_LTP']=payload['records']['data'][m]['CE']['lastPrice']
|
|
155
|
-
oi_row['CALLS_Net Chng']=payload['records']['data'][m]['CE']['change']
|
|
156
|
-
if(oi_mode=='full'):
|
|
157
|
-
oi_row['CALLS_Bid Qty']=payload['records']['data'][m]['CE']['bidQty']
|
|
158
|
-
oi_row['CALLS_Bid Price']=payload['records']['data'][m]['CE']['bidprice']
|
|
159
|
-
oi_row['CALLS_Ask Price']=payload['records']['data'][m]['CE']['askPrice']
|
|
160
|
-
oi_row['CALLS_Ask Qty']=payload['records']['data'][m]['CE']['askQty']
|
|
161
|
-
except KeyError:
|
|
162
|
-
oi_row['CALLS_OI'], oi_row['CALLS_Chng in OI'], oi_row['CALLS_Volume'], oi_row['CALLS_IV'], oi_row['CALLS_LTP'],oi_row['CALLS_Net Chng']=0,0,0,0,0,0
|
|
163
|
-
if(oi_mode=='full'):
|
|
164
|
-
oi_row['CALLS_Bid Qty'],oi_row['CALLS_Bid Price'],oi_row['CALLS_Ask Price'],oi_row['CALLS_Ask Qty']=0,0,0,0
|
|
165
|
-
pass
|
|
166
|
-
|
|
167
|
-
oi_row['Strike Price']=payload['records']['data'][m]['strikePrice']
|
|
168
|
-
|
|
169
|
-
try:
|
|
170
|
-
oi_row['PUTS_OI']=payload['records']['data'][m]['PE']['openInterest']
|
|
171
|
-
oi_row['PUTS_Chng in OI']=payload['records']['data'][m]['PE']['changeinOpenInterest']
|
|
172
|
-
oi_row['PUTS_Volume']=payload['records']['data'][m]['PE']['totalTradedVolume']
|
|
173
|
-
oi_row['PUTS_IV']=payload['records']['data'][m]['PE']['impliedVolatility']
|
|
174
|
-
oi_row['PUTS_LTP']=payload['records']['data'][m]['PE']['lastPrice']
|
|
175
|
-
oi_row['PUTS_Net Chng']=payload['records']['data'][m]['PE']['change']
|
|
176
|
-
if(oi_mode=='full'):
|
|
177
|
-
oi_row['PUTS_Bid Qty']=payload['records']['data'][m]['PE']['bidQty']
|
|
178
|
-
oi_row['PUTS_Bid Price']=payload['records']['data'][m]['PE']['bidprice']
|
|
179
|
-
oi_row['PUTS_Ask Price']=payload['records']['data'][m]['PE']['askPrice']
|
|
180
|
-
oi_row['PUTS_Ask Qty']=payload['records']['data'][m]['PE']['askQty']
|
|
181
|
-
except KeyError:
|
|
182
|
-
oi_row['PUTS_OI'], oi_row['PUTS_Chng in OI'], oi_row['PUTS_Volume'], oi_row['PUTS_IV'], oi_row['PUTS_LTP'],oi_row['PUTS_Net Chng']=0,0,0,0,0,0
|
|
183
|
-
if(oi_mode=='full'):
|
|
184
|
-
oi_row['PUTS_Bid Qty'],oi_row['PUTS_Bid Price'],oi_row['PUTS_Ask Price'],oi_row['PUTS_Ask Qty']=0,0,0,0
|
|
185
|
-
else:
|
|
186
|
-
logging.info(m)
|
|
187
|
-
|
|
188
|
-
if(oi_mode=='full'):
|
|
189
|
-
oi_row['CALLS_Chart'],oi_row['PUTS_Chart']=0,0
|
|
190
|
-
#oi_data = oi_data.append(oi_row, ignore_index=True)
|
|
191
|
-
#oi_data = pd.concat([oi_data, oi_row], ignore_index=True)
|
|
192
|
-
oi_data = pd.concat([oi_data, pd.DataFrame([oi_row])], ignore_index=True)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
oi_data['time_stamp']=payload['records']['timestamp']
|
|
197
|
-
return oi_data,float(payload['records']['underlyingValue']),payload['records']['timestamp']
|
|
360
|
+
# We will populate these dynamically
|
|
361
|
+
rows_list = []
|
|
362
|
+
|
|
363
|
+
if 'expiryDates' not in payload:
|
|
364
|
+
# Fallback for new API structure
|
|
365
|
+
if(expiry=="latest"):
|
|
366
|
+
expiry = expiry_list(symbol, type="list")[0]
|
|
367
|
+
data_list = payload['data']
|
|
368
|
+
else:
|
|
369
|
+
# Legacy structure support
|
|
370
|
+
if(expiry=="latest"):
|
|
371
|
+
expiry = payload['records']['expiryDates'][0]
|
|
372
|
+
data_list = payload['records']['data']
|
|
198
373
|
|
|
374
|
+
for m in range(len(data_list)):
|
|
375
|
+
current_expiry_str = data_list[m].get('expiryDates') or data_list[m].get('expiryDate')
|
|
376
|
+
try:
|
|
377
|
+
# Convert both to date objects for robust comparison
|
|
378
|
+
if "-" in current_expiry_str:
|
|
379
|
+
parts = current_expiry_str.split("-")
|
|
380
|
+
if parts[1].isdigit(): fmt = "%d-%m-%Y"
|
|
381
|
+
else: fmt = "%d-%b-%Y"
|
|
382
|
+
curr_date = datetime.datetime.strptime(current_expiry_str, fmt).date()
|
|
383
|
+
|
|
384
|
+
parts_exp = expiry.split("-")
|
|
385
|
+
if parts_exp[1].isdigit(): fmt_exp = "%d-%m-%Y"
|
|
386
|
+
else: fmt_exp = "%d-%b-%Y"
|
|
387
|
+
exp_date = datetime.datetime.strptime(expiry, fmt_exp).date()
|
|
388
|
+
match = (curr_date == exp_date)
|
|
389
|
+
else:
|
|
390
|
+
match = (current_expiry_str == expiry)
|
|
391
|
+
except:
|
|
392
|
+
match = (current_expiry_str == expiry)
|
|
393
|
+
|
|
394
|
+
if match:
|
|
395
|
+
oi_row = {col: 0 for col in col_names}
|
|
396
|
+
oi_row['Strike Price'] = data_list[m]['strikePrice']
|
|
397
|
+
|
|
398
|
+
for side in ['CE', 'PE']:
|
|
399
|
+
prefix = f"{'CALLS' if side == 'CE' else 'PUTS'}_"
|
|
400
|
+
if side in data_list[m] and data_list[m][side] is not None:
|
|
401
|
+
d = data_list[m][side]
|
|
402
|
+
oi_row[prefix + 'OI'] = d.get('openInterest', 0)
|
|
403
|
+
oi_row[prefix + 'Chng in OI'] = d.get('changeinOpenInterest', 0)
|
|
404
|
+
oi_row[prefix + 'Volume'] = d.get('totalTradedVolume', 0)
|
|
405
|
+
oi_row[prefix + 'IV'] = d.get('impliedVolatility', 0)
|
|
406
|
+
oi_row[prefix + 'LTP'] = d.get('lastPrice', 0)
|
|
407
|
+
oi_row[prefix + 'Net Chng'] = d.get('change', 0)
|
|
408
|
+
|
|
409
|
+
if oi_mode == 'full':
|
|
410
|
+
# New API key mapping
|
|
411
|
+
oi_row[prefix + 'Bid Qty'] = d.get('buyQuantity1', d.get('bidQty', 0))
|
|
412
|
+
oi_row[prefix + 'Bid Price'] = d.get('buyPrice1', d.get('bidprice', 0))
|
|
413
|
+
oi_row[prefix + 'Ask Price'] = d.get('sellPrice1', d.get('askPrice', 0))
|
|
414
|
+
oi_row[prefix + 'Ask Qty'] = d.get('sellQuantity1', d.get('askQty', 0))
|
|
415
|
+
oi_row[prefix + 'Chart'] = 0
|
|
416
|
+
|
|
417
|
+
rows_list.append(oi_row)
|
|
418
|
+
|
|
419
|
+
oi_data = pd.DataFrame(rows_list)
|
|
420
|
+
timestamp = payload.get('timestamp', payload.get('records', {}).get('timestamp', ''))
|
|
421
|
+
underlyingValue = payload.get('underlyingValue', payload.get('records', {}).get('underlyingValue', 0))
|
|
422
|
+
|
|
423
|
+
# github.com/aeron7/nsepython issue #80: the current getSymbolDerivativesData
|
|
424
|
+
# payload carries no top-level (or 'records') underlyingValue at all -- it
|
|
425
|
+
# only lives inside each individual CE/PE leaf record. Dig it out of there
|
|
426
|
+
# if the top-level lookup above came back empty.
|
|
427
|
+
if not underlyingValue and data_list:
|
|
428
|
+
for entry in data_list:
|
|
429
|
+
for side in ('CE', 'PE'):
|
|
430
|
+
leaf = entry.get(side)
|
|
431
|
+
if leaf and leaf.get('underlyingValue'):
|
|
432
|
+
underlyingValue = leaf['underlyingValue']
|
|
433
|
+
break
|
|
434
|
+
if underlyingValue:
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
oi_data['time_stamp'] = timestamp
|
|
438
|
+
return oi_data, float(underlyingValue or 0), timestamp
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def nse_quote_derivatives(symbol):
|
|
442
|
+
symbol = nsesymbolpurify(symbol)
|
|
443
|
+
if symbol.upper() in fnolist():
|
|
444
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolDerivativesData&symbol='+symbol)
|
|
445
|
+
return payload
|
|
446
|
+
else:
|
|
447
|
+
return {"error": f"{symbol} is not in derivatives list."}
|
|
199
448
|
|
|
200
449
|
def nse_quote(symbol,section=""):
|
|
201
|
-
#https://forum.unofficed.com/t/nsetools-get-quote-is-not-fetching-delivery-data-and-delivery-can-you-include-this-as-part-of-feature-request/1115/4
|
|
450
|
+
#https://forum.unofficed.com/t/nsetools-get-quote-is-not-fetching-delivery-data-and-delivery-can-you-include-this-as-part-of-feature-request/1115/4
|
|
202
451
|
symbol = nsesymbolpurify(symbol)
|
|
203
452
|
|
|
204
453
|
if(section==""):
|
|
205
|
-
if any(x in symbol for x in
|
|
206
|
-
payload = nsefetch('https://www.nseindia.com/api/
|
|
454
|
+
if any(x in symbol for x in indices):
|
|
455
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolDerivativesData&symbol='+symbol)
|
|
207
456
|
else:
|
|
208
|
-
payload = nsefetch('https://www.nseindia.com/api/
|
|
457
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol)
|
|
209
458
|
return payload
|
|
210
459
|
|
|
460
|
+
if(section=="trade_info"):
|
|
461
|
+
# The old /api/quote-equity?section=trade_info route is dead on the
|
|
462
|
+
# live site (confirmed HTTP 403, even through the fully-warmed
|
|
463
|
+
# curl_cffi session round 1 built). But every category of data the
|
|
464
|
+
# old endpoint used to return is already present, just reshuffled,
|
|
465
|
+
# inside the NEW working GetQuoteApi?functionName=getSymbolData
|
|
466
|
+
# response this function's section=="" branch already fetches --
|
|
467
|
+
# confirmed field-by-field against the real, documented old
|
|
468
|
+
# response shape (EquityTradeInfo: marketDeptOrderBook.{bid,ask,
|
|
469
|
+
# tradeInfo,valueAtRisk} + securityWiseDP), so this is a pure
|
|
470
|
+
# remap/slice of data already being fetched, not a new network call.
|
|
471
|
+
#
|
|
472
|
+
# Two small fidelity gaps versus the old route, both because the
|
|
473
|
+
# source data for them no longer exists anywhere in the new
|
|
474
|
+
# response (not a mapping oversight):
|
|
475
|
+
# - noBlockDeals/bulkBlockDeals: the new endpoint carries no
|
|
476
|
+
# block-deal info at all -> defaulted to True/[] (i.e. "no
|
|
477
|
+
# block deals known"), not derived from a live block-deal
|
|
478
|
+
# check. Use nse_blockdeal()/get_blockdeals() directly if you
|
|
479
|
+
# need real block-deal data.
|
|
480
|
+
# - securityWiseDP.seriesRemarks: no equivalent field exists in
|
|
481
|
+
# the new response -> always None, same as it is for most
|
|
482
|
+
# symbols on the old route anyway.
|
|
483
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol)
|
|
484
|
+
eq = payload['equityResponse'][0]
|
|
485
|
+
ob = eq.get('orderBook', {})
|
|
486
|
+
md = eq.get('metaData', {})
|
|
487
|
+
ti = eq.get('tradeInfo', {})
|
|
488
|
+
pi = eq.get('priceInfo', {})
|
|
489
|
+
si = eq.get('secInfo', {})
|
|
490
|
+
|
|
491
|
+
bid = [{"price": ob.get(f"buyPrice{i}"), "quantity": ob.get(f"buyQuantity{i}")} for i in range(1, 6)]
|
|
492
|
+
ask = [{"price": ob.get(f"sellPrice{i}"), "quantity": ob.get(f"sellQuantity{i}")} for i in range(1, 6)]
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
"noBlockDeals": True,
|
|
496
|
+
"bulkBlockDeals": [],
|
|
497
|
+
"marketDeptOrderBook": {
|
|
498
|
+
"totalBuyQuantity": ob.get("totalBuyQuantity"),
|
|
499
|
+
"totalSellQuantity": ob.get("totalSellQuantity"),
|
|
500
|
+
"open": md.get("open"),
|
|
501
|
+
"bid": bid,
|
|
502
|
+
"ask": ask,
|
|
503
|
+
"tradeInfo": {
|
|
504
|
+
"totalTradedVolume": ti.get("totalTradedVolume"),
|
|
505
|
+
"totalTradedValue": ti.get("totalTradedValue"),
|
|
506
|
+
"totalMarketCap": ti.get("totalMarketCap"),
|
|
507
|
+
"ffmc": ti.get("ffmc"),
|
|
508
|
+
"impactCost": ti.get("impactCost"),
|
|
509
|
+
"cmDailyVolatility": pi.get("cmDailyVolatility"),
|
|
510
|
+
"cmAnnualVolatility": pi.get("cmAnnualVolatility"),
|
|
511
|
+
"marketLot": ti.get("marketLot"),
|
|
512
|
+
"activeSeries": ti.get("series"),
|
|
513
|
+
},
|
|
514
|
+
"valueAtRisk": {
|
|
515
|
+
"securityVar": si.get("securityvar"),
|
|
516
|
+
"indexVar": si.get("indexvar"),
|
|
517
|
+
"varMargin": si.get("varMargin"),
|
|
518
|
+
"extremeLossMargin": si.get("extremelossMargin"),
|
|
519
|
+
"adhocMargin": si.get("adhocMargin"),
|
|
520
|
+
"applicableMargin": si.get("applicableMargin"),
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
"securityWiseDP": {
|
|
524
|
+
"quantityTraded": ti.get("quantitytraded"),
|
|
525
|
+
"deliveryQuantity": ti.get("deliveryquantity"),
|
|
526
|
+
"deliveryToTradedQuantity": ti.get("deliveryToTradedQuantity"),
|
|
527
|
+
"seriesRemarks": None,
|
|
528
|
+
"secWiseDelPosDate": ti.get("secwisedelposdate"),
|
|
529
|
+
},
|
|
530
|
+
}
|
|
531
|
+
|
|
211
532
|
if(section!=""):
|
|
533
|
+
# Any other section value (e.g. the old "preOpenMarket") still hits
|
|
534
|
+
# the dead /api/quote-equity§ion= route -- not yet remapped to
|
|
535
|
+
# a working source. Only trade_info was confirmed+fixed this round.
|
|
212
536
|
payload = nsefetch('https://www.nseindia.com/api/quote-equity?symbol='+symbol+'§ion='+section)
|
|
213
537
|
return payload
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
538
|
+
def nse_expirydetails(payload, i=0, symbol=None):
|
|
539
|
+
expiry_dates = []
|
|
540
|
+
if 'records' in payload:
|
|
541
|
+
expiry_dates = payload['records']['expiryDates']
|
|
542
|
+
elif 'expiryDates' in payload:
|
|
543
|
+
expiry_dates = payload['expiryDates']
|
|
544
|
+
elif 'data' in payload:
|
|
545
|
+
unique_dates = set()
|
|
546
|
+
for entry in payload['data']:
|
|
547
|
+
if 'expiryDate' in entry:
|
|
548
|
+
unique_dates.add(entry['expiryDate'])
|
|
549
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
550
|
+
|
|
551
|
+
# Filter future dates
|
|
552
|
+
future_expiry_dates = []
|
|
553
|
+
if expiry_dates:
|
|
554
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
555
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
556
|
+
|
|
557
|
+
# Fallback to expiry_list if i is out of range and we can determine the symbol
|
|
558
|
+
if i >= len(future_expiry_dates):
|
|
559
|
+
if not symbol and 'data' in payload and len(payload['data']) > 0:
|
|
560
|
+
# Try to extract symbol from payload data
|
|
561
|
+
first_entry = payload['data'][0]
|
|
562
|
+
symbol = first_entry.get('symbol')
|
|
563
|
+
if not symbol:
|
|
564
|
+
if 'CE' in first_entry and first_entry['CE']:
|
|
565
|
+
symbol = first_entry['CE'].get('underlying')
|
|
566
|
+
elif 'PE' in first_entry and first_entry['PE']:
|
|
567
|
+
symbol = first_entry['PE'].get('underlying')
|
|
568
|
+
|
|
569
|
+
if symbol:
|
|
570
|
+
dates = expiry_list(symbol, type="list")
|
|
571
|
+
if dates:
|
|
572
|
+
# Filter future dates from expiry_list as well
|
|
573
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
574
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
575
|
+
|
|
576
|
+
if i >= len(future_expiry_dates):
|
|
577
|
+
return None, None
|
|
578
|
+
|
|
579
|
+
currentExpiry = future_expiry_dates[i]
|
|
580
|
+
currentExpiry_dt = datetime.datetime.strptime(currentExpiry, '%d-%b-%Y').date()
|
|
581
|
+
date_today = run_time.date()
|
|
582
|
+
dte = (currentExpiry_dt - date_today).days
|
|
583
|
+
return currentExpiry_dt, dte
|
|
584
|
+
def pcr(payload, inp=0):
|
|
229
585
|
ce_oi = 0
|
|
230
586
|
pe_oi = 0
|
|
231
|
-
|
|
232
|
-
|
|
587
|
+
|
|
588
|
+
# Identify the data and expiry dates based on structure
|
|
589
|
+
if 'records' in payload:
|
|
590
|
+
# Legacy structure
|
|
591
|
+
data_list = payload['records']['data']
|
|
592
|
+
expiry_dates = payload['records']['expiryDates']
|
|
593
|
+
elif 'data' in payload:
|
|
594
|
+
# New structure
|
|
595
|
+
data_list = payload['data']
|
|
596
|
+
# Extract unique sorted expiry dates from data
|
|
597
|
+
unique_dates = set()
|
|
598
|
+
for entry in data_list:
|
|
599
|
+
ed = entry.get('expiryDate') or entry.get('expiryDates')
|
|
600
|
+
if ed:
|
|
601
|
+
unique_dates.add(ed)
|
|
602
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%m-%Y") if "-" in x and x.split("-")[1].isdigit() else datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
603
|
+
else:
|
|
604
|
+
# If payload is empty or unknown, we can't proceed without fetching
|
|
605
|
+
# But we need a symbol. Try to get it from payload if possible.
|
|
606
|
+
return 0.0
|
|
607
|
+
|
|
608
|
+
if not expiry_dates or inp >= len(expiry_dates):
|
|
609
|
+
# Requested index is outside the current payload's scope.
|
|
610
|
+
# Check if we can fetch more data for this specific symbol.
|
|
611
|
+
symbol = payload.get('symbol') or payload.get('records', {}).get('symbol')
|
|
612
|
+
if not symbol and 'data' in payload and len(payload['data']) > 0:
|
|
613
|
+
first = payload['data'][0]
|
|
614
|
+
symbol = first.get('symbol') or (first.get('CE') and first['CE'].get('underlying'))
|
|
615
|
+
|
|
616
|
+
if symbol and inp > 0:
|
|
617
|
+
# Fetch all expiries to find the target one
|
|
618
|
+
all_expiries = expiry_list(symbol, type="list")
|
|
619
|
+
if inp < len(all_expiries):
|
|
620
|
+
target = all_expiries[inp]
|
|
621
|
+
# Fetch specific expiry data using getOptionChainData
|
|
622
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getOptionChainData&symbol={nsesymbolpurify(symbol)}¶ms=expiryDate={target}'
|
|
623
|
+
new_payload = nsefetch(url)
|
|
624
|
+
if new_payload and 'data' in new_payload:
|
|
625
|
+
for entry in new_payload['data']:
|
|
626
|
+
ce_oi += entry.get('CE', {}).get('openInterest', 0) if entry.get('CE') else 0
|
|
627
|
+
pe_oi += entry.get('PE', {}).get('openInterest', 0) if entry.get('PE') else 0
|
|
628
|
+
if ce_oi > 0: return pe_oi / ce_oi
|
|
629
|
+
return 0.0
|
|
630
|
+
|
|
631
|
+
target_expiry = expiry_dates[inp]
|
|
632
|
+
|
|
633
|
+
found_data = False
|
|
634
|
+
for i in data_list:
|
|
635
|
+
curr_exp = i.get('expiryDate') or i.get('expiryDates')
|
|
636
|
+
if curr_exp == target_expiry:
|
|
637
|
+
found_data = True
|
|
233
638
|
try:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
639
|
+
if 'CE' in i and i['CE']:
|
|
640
|
+
ce_oi += i['CE'].get('openInterest', 0)
|
|
641
|
+
if 'PE' in i and i['PE']:
|
|
642
|
+
pe_oi += i['PE'].get('openInterest', 0)
|
|
643
|
+
except (KeyError, TypeError):
|
|
237
644
|
pass
|
|
645
|
+
|
|
646
|
+
# If we didn't find any data for the target expiry in the payload,
|
|
647
|
+
# it means the payload was filtered (e.g. by the scrapper). Fetch it now.
|
|
648
|
+
if not found_data:
|
|
649
|
+
symbol = payload.get('symbol') or payload.get('records', {}).get('symbol')
|
|
650
|
+
if symbol:
|
|
651
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getOptionChainData&symbol={nsesymbolpurify(symbol)}¶ms=expiryDate={target_expiry}'
|
|
652
|
+
new_payload = nsefetch(url)
|
|
653
|
+
if new_payload and 'data' in new_payload:
|
|
654
|
+
for entry in new_payload['data']:
|
|
655
|
+
ce_oi += entry.get('CE', {}).get('openInterest', 0) if entry.get('CE') else 0
|
|
656
|
+
pe_oi += entry.get('PE', {}).get('openInterest', 0) if entry.get('PE') else 0
|
|
657
|
+
|
|
658
|
+
if ce_oi == 0:
|
|
659
|
+
return 0.0
|
|
660
|
+
|
|
238
661
|
return pe_oi / ce_oi
|
|
239
662
|
|
|
240
663
|
#forum.unofficed.com/t/unable-to-find-nse-quote-meta-api/702/4
|
|
241
664
|
#Refer https://forum.unofficed.com/t/changed-the-nse-quote-ltp-function/1276
|
|
242
665
|
def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
243
|
-
|
|
666
|
+
if(optionType!="-"):
|
|
667
|
+
payload = nse_quote_derivatives(symbol)
|
|
668
|
+
else:
|
|
669
|
+
if any(x in symbol for x in indices):
|
|
670
|
+
payload = nse_quote_derivatives(symbol)
|
|
671
|
+
else:
|
|
672
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol)
|
|
673
|
+
|
|
674
|
+
lastPrice = 0
|
|
675
|
+
|
|
676
|
+
if(optionType=="-"):
|
|
677
|
+
if 'equityResponse' in payload and len(payload['equityResponse']) > 0:
|
|
678
|
+
lastPrice = payload['equityResponse'][0]['orderBook']['lastPrice']
|
|
679
|
+
elif 'data' in payload and len(payload['data']) > 0:
|
|
680
|
+
# For indices, underlyingValue in derivative payload is the current index LTP
|
|
681
|
+
lastPrice = payload['data'][0].get('underlyingValue')
|
|
682
|
+
return lastPrice
|
|
244
683
|
|
|
245
684
|
meta = "Options"
|
|
246
685
|
if(optionType=="Fut"): meta = "Futures"
|
|
@@ -248,36 +687,68 @@ def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
|
248
687
|
if(optionType=="CE"):optionType="Call"
|
|
249
688
|
|
|
250
689
|
if(expiryDate=="latest") or (expiryDate=="next"):
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
690
|
+
i = 0 if expiryDate=="latest" else 1
|
|
691
|
+
expiry_dates = []
|
|
692
|
+
|
|
693
|
+
# Extract from new FNO payload structure
|
|
694
|
+
if 'data' in payload:
|
|
695
|
+
unique_dates = set()
|
|
696
|
+
for entry in payload['data']:
|
|
697
|
+
if 'expiryDate' in entry:
|
|
698
|
+
it = entry.get('instrumentType', '')
|
|
699
|
+
if (meta == "Futures" and "FUT" in it) or (meta == "Options" and "OPT" in it):
|
|
700
|
+
unique_dates.add(entry['expiryDate'])
|
|
701
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
702
|
+
|
|
703
|
+
# Filter future dates
|
|
704
|
+
future_expiry_dates = []
|
|
705
|
+
if expiry_dates:
|
|
706
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
707
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
708
|
+
|
|
709
|
+
# Fallback to expiry_list
|
|
710
|
+
if i >= len(future_expiry_dates):
|
|
711
|
+
dates = expiry_list(symbol, type="list")
|
|
712
|
+
if dates:
|
|
713
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
714
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
715
|
+
|
|
716
|
+
if i < len(future_expiry_dates):
|
|
717
|
+
expiryDate = future_expiry_dates[i]
|
|
262
718
|
|
|
263
719
|
|
|
264
720
|
if(optionType!="-"):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
721
|
+
data_list = payload.get('data', [])
|
|
722
|
+
for i in data_list:
|
|
723
|
+
# Check instrument type in identifier or metadata if present
|
|
724
|
+
if meta == "Futures":
|
|
725
|
+
is_match = "FUT" in i.get('instrumentType', '')
|
|
726
|
+
else:
|
|
727
|
+
is_match = "OPT" in i.get('instrumentType', '')
|
|
728
|
+
|
|
729
|
+
if is_match:
|
|
268
730
|
if(optionType=="Fut"):
|
|
269
|
-
if(i
|
|
270
|
-
lastPrice = i
|
|
731
|
+
if(i.get('expiryDate')==expiryDate):
|
|
732
|
+
lastPrice = i.get('lastPrice')
|
|
733
|
+
break
|
|
271
734
|
|
|
272
735
|
if((optionType=="Put")or(optionType=="Call")):
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
736
|
+
# Some APIs have optionType as 'PE'/'CE' or 'Put'/'Call'
|
|
737
|
+
p_opt_type = i.get('optionType')
|
|
738
|
+
if p_opt_type == "PE": p_opt_type = "Put"
|
|
739
|
+
if p_opt_type == "CE": p_opt_type = "Call"
|
|
740
|
+
|
|
741
|
+
if (i.get("expiryDate")==expiryDate):
|
|
742
|
+
if (p_opt_type==optionType):
|
|
743
|
+
# strikePrice in payload is often string with padding
|
|
744
|
+
try:
|
|
745
|
+
p_strike = float(str(i.get("strikePrice")).strip())
|
|
746
|
+
except:
|
|
747
|
+
p_strike = 0
|
|
748
|
+
|
|
749
|
+
if (p_strike==float(strikePrice)):
|
|
750
|
+
lastPrice = i.get('lastPrice')
|
|
751
|
+
break
|
|
281
752
|
|
|
282
753
|
return lastPrice
|
|
283
754
|
|
|
@@ -292,45 +763,84 @@ def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
|
292
763
|
# print(nse_quote_ltp("RELIANCE","next","PE",2300))
|
|
293
764
|
|
|
294
765
|
def nse_quote_meta(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
766
|
+
if(optionType!="-"):
|
|
767
|
+
payload = nse_quote_derivatives(symbol)
|
|
768
|
+
else:
|
|
769
|
+
if any(x in symbol for x in indices):
|
|
770
|
+
payload = nse_quote_derivatives(symbol)
|
|
771
|
+
else:
|
|
772
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol)
|
|
298
773
|
|
|
299
|
-
|
|
300
|
-
#forum.unofficed.com/t/unable-to-find-nse-quote-meta-api/702/4
|
|
301
|
-
if((symbol in indices) and (optionType=="Fut")):
|
|
302
|
-
dates = expiry_list("RELIANCE","list")
|
|
303
|
-
if(expiryDate=="latest"): expiryDate=dates[0]
|
|
304
|
-
if(expiryDate=="next"): expiryDate=dates[1]
|
|
774
|
+
metadata = {}
|
|
305
775
|
|
|
306
|
-
if(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if(expiryDate=="next"): expiryDate=dates[1]
|
|
776
|
+
if(optionType=="-"):
|
|
777
|
+
if 'equityResponse' in payload and len(payload['equityResponse']) > 0:
|
|
778
|
+
metadata = payload['equityResponse'][0].get('metaData', {})
|
|
779
|
+
return metadata
|
|
311
780
|
|
|
312
781
|
meta = "Options"
|
|
313
782
|
if(optionType=="Fut"): meta = "Futures"
|
|
314
783
|
if(optionType=="PE"):optionType="Put"
|
|
315
784
|
if(optionType=="CE"):optionType="Call"
|
|
316
785
|
|
|
786
|
+
if(expiryDate=="latest") or (expiryDate=="next"):
|
|
787
|
+
i = 0 if expiryDate=="latest" else 1
|
|
788
|
+
expiry_dates = []
|
|
789
|
+
if 'data' in payload:
|
|
790
|
+
unique_dates = set()
|
|
791
|
+
for entry in payload['data']:
|
|
792
|
+
if 'expiryDate' in entry:
|
|
793
|
+
it = entry.get('instrumentType', '')
|
|
794
|
+
if (meta == "Futures" and "FUT" in it) or (meta == "Options" and "OPT" in it):
|
|
795
|
+
unique_dates.add(entry['expiryDate'])
|
|
796
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
797
|
+
|
|
798
|
+
future_expiry_dates = []
|
|
799
|
+
if expiry_dates:
|
|
800
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
801
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
802
|
+
|
|
803
|
+
if i >= len(future_expiry_dates):
|
|
804
|
+
dates = expiry_list(symbol, type="list")
|
|
805
|
+
if dates:
|
|
806
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
807
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
808
|
+
|
|
809
|
+
if i < len(future_expiry_dates):
|
|
810
|
+
expiryDate = future_expiry_dates[i]
|
|
811
|
+
|
|
812
|
+
# print(f"DEBUG: Calculated expiryDate={expiryDate}, meta={meta}, optionType={optionType}")
|
|
813
|
+
|
|
317
814
|
if(optionType!="-"):
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
815
|
+
data_list = payload.get('data', [])
|
|
816
|
+
# print(f"DEBUG: Searching in {len(data_list)} items")
|
|
817
|
+
for i in data_list:
|
|
818
|
+
if meta == "Futures":
|
|
819
|
+
is_match = "FUT" in i.get('instrumentType', '')
|
|
820
|
+
else:
|
|
821
|
+
is_match = "OPT" in i.get('instrumentType', '')
|
|
822
|
+
|
|
823
|
+
if is_match:
|
|
321
824
|
if(optionType=="Fut"):
|
|
322
|
-
if(i
|
|
323
|
-
metadata = i
|
|
825
|
+
if(i.get('expiryDate')==expiryDate):
|
|
826
|
+
metadata = i
|
|
827
|
+
break
|
|
324
828
|
|
|
325
829
|
if((optionType=="Put")or(optionType=="Call")):
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
830
|
+
p_opt_type = i.get('optionType')
|
|
831
|
+
if p_opt_type == "PE": p_opt_type = "Put"
|
|
832
|
+
if p_opt_type == "CE": p_opt_type = "Call"
|
|
833
|
+
|
|
834
|
+
if (i.get("expiryDate")==expiryDate):
|
|
835
|
+
if (p_opt_type==optionType):
|
|
836
|
+
try:
|
|
837
|
+
p_strike = float(str(i.get("strikePrice")).strip())
|
|
838
|
+
except:
|
|
839
|
+
p_strike = 0
|
|
840
|
+
|
|
841
|
+
if (p_strike==float(strikePrice)):
|
|
842
|
+
metadata = i
|
|
843
|
+
break
|
|
334
844
|
|
|
335
845
|
return metadata
|
|
336
846
|
|
|
@@ -355,8 +865,20 @@ def nse_eq(symbol):
|
|
|
355
865
|
payload = nsefetch('https://www.nseindia.com/api/quote-derivative?symbol='+symbol)
|
|
356
866
|
except:
|
|
357
867
|
pass
|
|
358
|
-
except KeyError:
|
|
359
|
-
|
|
868
|
+
except (KeyError, NSEEndpointError):
|
|
869
|
+
# /api/quote-equity is retired on the live site (confirmed HTTP 403,
|
|
870
|
+
# Akamai/WAF "Access Denied" page, as of 2026) with no indication it
|
|
871
|
+
# is coming back. The newer NextApi GetQuoteApi endpoint carries the
|
|
872
|
+
# same underlying data (just in a different JSON shape - data lives
|
|
873
|
+
# under payload['equityResponse'][0] instead of payload['priceInfo']/
|
|
874
|
+
# payload['info']) so we fall back to that instead of returning {}.
|
|
875
|
+
logging.warning(
|
|
876
|
+
"nse_eq(%s): /api/quote-equity is retired; returning data from "
|
|
877
|
+
"the newer NextApi quote endpoint instead (see nse_quote() - the "
|
|
878
|
+
"JSON shape differs from the old quote-equity response).",
|
|
879
|
+
symbol,
|
|
880
|
+
)
|
|
881
|
+
payload = nse_quote(symbol)
|
|
360
882
|
return payload
|
|
361
883
|
|
|
362
884
|
|
|
@@ -370,8 +892,18 @@ def nse_fno(symbol):
|
|
|
370
892
|
payload = nsefetch('https://www.nseindia.com/api/quote-equity?symbol='+symbol)
|
|
371
893
|
except KeyError:
|
|
372
894
|
pass
|
|
373
|
-
except KeyError:
|
|
374
|
-
|
|
895
|
+
except (KeyError, NSEEndpointError):
|
|
896
|
+
# /api/quote-derivative is likewise retired (confirmed HTTP 404 on
|
|
897
|
+
# the live site). getSymbolDerivativesData via nse_quote_derivatives()
|
|
898
|
+
# is the working replacement (different JSON shape: a flat 'data'
|
|
899
|
+
# list of per-strike CE/PE records instead of records/underlyingValue).
|
|
900
|
+
logging.warning(
|
|
901
|
+
"nse_fno(%s): /api/quote-derivative is retired; returning data "
|
|
902
|
+
"from the newer NextApi derivatives endpoint instead (see "
|
|
903
|
+
"nse_quote_derivatives() - the JSON shape differs).",
|
|
904
|
+
symbol,
|
|
905
|
+
)
|
|
906
|
+
payload = nse_quote_derivatives(symbol)
|
|
375
907
|
return payload
|
|
376
908
|
|
|
377
909
|
def quote_equity(symbol):
|
|
@@ -411,19 +943,31 @@ def nse_past_results(symbol):
|
|
|
411
943
|
symbol = nsesymbolpurify(symbol)
|
|
412
944
|
return nsefetch('https://www.nseindia.com/api/results-comparision?symbol='+symbol)
|
|
413
945
|
|
|
414
|
-
def expiry_list(symbol,type="
|
|
415
|
-
logging.info("Getting Expiry List of: "+ symbol)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
946
|
+
def expiry_list(symbol, type=""):
|
|
947
|
+
logging.info("Getting Expiry List of: " + symbol)
|
|
948
|
+
symbol = nsesymbolpurify(symbol)
|
|
949
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getOptionChainDropdown&symbol={symbol}'
|
|
950
|
+
payload = nsefetch(url)
|
|
951
|
+
|
|
952
|
+
if not payload or 'expiryDates' not in payload:
|
|
953
|
+
return [] if type == "list" else pd.DataFrame()
|
|
421
954
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
955
|
+
expiry_dates = payload['expiryDates']
|
|
956
|
+
|
|
957
|
+
# Format dates from DD-MM-YYYY to DD-Mon-YYYY
|
|
958
|
+
formatted_dates = []
|
|
959
|
+
for d in expiry_dates:
|
|
960
|
+
try:
|
|
961
|
+
dt = datetime.datetime.strptime(d, "%d-%m-%Y")
|
|
962
|
+
formatted_dates.append(dt.strftime("%d-%b-%Y"))
|
|
963
|
+
except:
|
|
964
|
+
formatted_dates.append(d)
|
|
965
|
+
|
|
966
|
+
if type == "list":
|
|
967
|
+
return formatted_dates
|
|
968
|
+
else:
|
|
969
|
+
# If anything other than "list" is provided (like "df", "pandas", or default), return DataFrame
|
|
970
|
+
return pd.DataFrame({'Date': formatted_dates})
|
|
427
971
|
|
|
428
972
|
|
|
429
973
|
def nse_custom_function_secfno(symbol,attribute="lastPrice"):
|
|
@@ -442,8 +986,22 @@ def nse_marketStatus():
|
|
|
442
986
|
return payload
|
|
443
987
|
|
|
444
988
|
def nse_circular(mode="latest"):
|
|
989
|
+
# The old mode="latest" path (https://nseindia.com/api/latest-circular,
|
|
990
|
+
# no `www.`) is dead on the live site: it returns HTTP 200 but a bare
|
|
991
|
+
# {'error': True, 'status': 500} JSON body -- confirmed this is NOT an
|
|
992
|
+
# Akamai bot-challenge (no injected script, no 403/503), just NSE's own
|
|
993
|
+
# "this route doesn't exist" response. NSE renamed the circulars page
|
|
994
|
+
# itself from /resources/circulars to
|
|
995
|
+
# /resources/exchange-communication-circulars, and a Playwright network
|
|
996
|
+
# capture on that live page shows it calling
|
|
997
|
+
# https://www.nseindia.com/api/circulars?fromDate=DD-MM-YYYY&toDate=DD-MM-YYYY
|
|
998
|
+
# (with `www.`) -- the SAME URL this function's own mode!="latest"
|
|
999
|
+
# branch already used and which was independently confirmed live
|
|
1000
|
+
# (zero params defaults to NSE's own last-7-days/150-record window).
|
|
1001
|
+
# Fix: route "latest" to that same working endpoint too, instead of the
|
|
1002
|
+
# dead no-www path.
|
|
445
1003
|
if(mode=="latest"):
|
|
446
|
-
payload = nsefetch('https://nseindia.com/api/
|
|
1004
|
+
payload = nsefetch('https://www.nseindia.com/api/circulars')
|
|
447
1005
|
else:
|
|
448
1006
|
payload = nsefetch('https://www.nseindia.com/api/circulars')
|
|
449
1007
|
return payload
|
|
@@ -465,21 +1023,30 @@ def nsetools_get_quote(symbol):
|
|
|
465
1023
|
return payload['data'][m]
|
|
466
1024
|
|
|
467
1025
|
|
|
1026
|
+
def _nse_index_data():
|
|
1027
|
+
# iislliveblob.niftyindices.com is a dead host (confirmed live: NXDOMAIN,
|
|
1028
|
+
# twice). /api/allIndices on the main site carries the same live index
|
|
1029
|
+
# quotes (139 indices as of this writing, including pe/pb/dy per index).
|
|
1030
|
+
# Its per-row key is 'index' (e.g. "NIFTY 50"), not the old 'indexName' --
|
|
1031
|
+
# alias it so nse_get_index_list()/nse_get_index_quote() below (and any
|
|
1032
|
+
# external code doing the same lookup) keep working unchanged.
|
|
1033
|
+
payload = nsefetch("https://www.nseindia.com/api/allIndices")
|
|
1034
|
+
rows = payload.get("data", [])
|
|
1035
|
+
for row in rows:
|
|
1036
|
+
row.setdefault("indexName", row.get("index"))
|
|
1037
|
+
return rows
|
|
1038
|
+
|
|
1039
|
+
|
|
468
1040
|
def nse_index():
|
|
469
|
-
|
|
470
|
-
payload = pd.DataFrame(payload["data"])
|
|
471
|
-
return payload
|
|
1041
|
+
return pd.DataFrame(_nse_index_data())
|
|
472
1042
|
|
|
473
1043
|
def nse_get_index_list():
|
|
474
|
-
|
|
475
|
-
payload = pd.DataFrame(payload["data"])
|
|
476
|
-
return payload["indexName"].tolist()
|
|
1044
|
+
return pd.DataFrame(_nse_index_data())["indexName"].tolist()
|
|
477
1045
|
|
|
478
1046
|
def nse_get_index_quote(index):
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return payload['data'][m]
|
|
1047
|
+
for row in _nse_index_data():
|
|
1048
|
+
if row["indexName"] == index.upper():
|
|
1049
|
+
return row
|
|
483
1050
|
|
|
484
1051
|
def nse_get_advances_declines(mode="pandas"):
|
|
485
1052
|
try:
|
|
@@ -505,10 +1072,19 @@ def nse_get_top_gainers():
|
|
|
505
1072
|
return df.head(5)
|
|
506
1073
|
|
|
507
1074
|
def nse_get_fno_lot_sizes(symbol="all",mode="list"):
|
|
508
|
-
|
|
1075
|
+
# github.com/aeron7/nsepythonserver issue #4 ("lot sizes not working"):
|
|
1076
|
+
# two stacked bugs, confirmed live. (1) archives.nseindia.com silently
|
|
1077
|
+
# redirects this specific file to an unrelated PDF circular these days
|
|
1078
|
+
# (NSE's archives -> nsearchives host migration left a stale redirect on
|
|
1079
|
+
# just this path) -- nsearchives.nseindia.com/content/fo/fo_mktlots.csv
|
|
1080
|
+
# is the real, current location, confirmed live with the exact same CSV
|
|
1081
|
+
# shape. (2) plain `requests.get()` against nsearchives.nseindia.com
|
|
1082
|
+
# hangs to a read-timeout (confirmed live) -- it needs the same
|
|
1083
|
+
# curl_cffi TLS impersonation as the rest of the site now.
|
|
1084
|
+
url="https://nsearchives.nseindia.com/content/fo/fo_mktlots.csv"
|
|
509
1085
|
|
|
510
1086
|
if(mode=="list"):
|
|
511
|
-
s=
|
|
1087
|
+
s = _nse_fetch_csv_text(url)
|
|
512
1088
|
res_dict = {}
|
|
513
1089
|
for line in s.split('\n'):
|
|
514
1090
|
if line != '' and re.search(',', line) and (line.casefold().find('symbol') == -1):
|
|
@@ -520,7 +1096,7 @@ def nse_get_fno_lot_sizes(symbol="all",mode="list"):
|
|
|
520
1096
|
return res_dict[symbol.upper()]
|
|
521
1097
|
|
|
522
1098
|
if(mode=="pandas"):
|
|
523
|
-
payload = pd.read_csv(url)
|
|
1099
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(url)))
|
|
524
1100
|
if(symbol=="all"):
|
|
525
1101
|
return payload
|
|
526
1102
|
else:
|
|
@@ -574,7 +1150,13 @@ def black_scholes_dexter(S0,X,t,σ="",r=10,q=0.0,td=365):
|
|
|
574
1150
|
|
|
575
1151
|
def equity_history_virgin(symbol,series,start_date,end_date):
|
|
576
1152
|
#url="https://www.nseindia.com/api/historical/cm/equity?symbol="+symbol+"&series=[%22"+series+"%22]&from="+str(start_date)+"&to="+str(end_date)+""
|
|
577
|
-
|
|
1153
|
+
# NOTE: the original /api/historical/cm/equity route is retired on the
|
|
1154
|
+
# live site (confirmed HTTP 503 as of 2026, even via curl_cffi). NSE's
|
|
1155
|
+
# replacement is /api/historicalOR/cm/equity -- same query params, same
|
|
1156
|
+
# response shape (payload['data'] records with CH_TIMESTAMP/
|
|
1157
|
+
# CH_CLOSING_PRICE/etc), confirmed live, so this is a plain host-path
|
|
1158
|
+
# swap with no downstream parsing changes needed.
|
|
1159
|
+
url = 'https://www.nseindia.com/api/historicalOR/cm/equity?symbol=' + symbol + '&series=["' + series + '"]&from=' + start_date + '&to=' + end_date
|
|
578
1160
|
|
|
579
1161
|
payload = nsefetch(url)
|
|
580
1162
|
return pd.DataFrame.from_records(payload["data"])
|
|
@@ -654,7 +1236,10 @@ def derivative_history_virgin(symbol,start_date,end_date,instrumentType,expiry_d
|
|
|
654
1236
|
strikePrice = "%.2f" % strikePrice
|
|
655
1237
|
strikePrice = str(strikePrice)
|
|
656
1238
|
|
|
657
|
-
|
|
1239
|
+
# /api/historical/fo/derivatives is retired (HTTP 503 live); the
|
|
1240
|
+
# confirmed-working replacement is /api/historicalOR/fo/derivatives with
|
|
1241
|
+
# the same query params and response shape.
|
|
1242
|
+
nsefetch_url = "https://www.nseindia.com/api/historicalOR/fo/derivatives?&from="+str(start_date)+"&to="+str(end_date)+"&optionType="+optionType+"&strikePrice="+strikePrice+"&expiryDate="+expiry_date+"&instrumentType="+instrumentType+"&symbol="+symbol+""
|
|
658
1243
|
payload = nsefetch(nsefetch_url)
|
|
659
1244
|
logging.info(nsefetch_url)
|
|
660
1245
|
logging.info(payload)
|
|
@@ -720,7 +1305,10 @@ def derivative_history(symbol,start_date,end_date,instrumentType,expiry_date,str
|
|
|
720
1305
|
|
|
721
1306
|
def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
722
1307
|
if(end_date==""):end_date=end_date
|
|
723
|
-
|
|
1308
|
+
# Same retirement as derivative_history_virgin()/equity_history_virgin()
|
|
1309
|
+
# above -- /api/historical/* is gone, /api/historicalOR/* is the working
|
|
1310
|
+
# replacement with an identical response shape.
|
|
1311
|
+
nsefetch_url = "https://www.nseindia.com/api/historicalOR/fo/derivatives/meta?&from="+start_date+"&to="+end_date+"&symbol="+symbol+""
|
|
724
1312
|
payload = nsefetch(nsefetch_url)
|
|
725
1313
|
|
|
726
1314
|
#print(payload)
|
|
@@ -755,58 +1343,159 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
755
1343
|
return filtered_date_payload
|
|
756
1344
|
|
|
757
1345
|
# # Nifty Indicies Site
|
|
1346
|
+
#
|
|
1347
|
+
# niftyindices.com is a completely separate host/site from nseindia.com (no
|
|
1348
|
+
# Akamai Bot Manager symptoms observed here) -- but it was fully redesigned
|
|
1349
|
+
# onto a different CMS at some point: the old ASP.NET WebMethods under
|
|
1350
|
+
# `niftyindices.com/Backpage.aspx/*` (returning `{"d": "<json string>"}`) are
|
|
1351
|
+
# gone, and POSTing to them now just returns the site's homepage HTML, which
|
|
1352
|
+
# is exactly github.com/aeron7/nsepython issue #78's
|
|
1353
|
+
# `JSONDecodeError: Expecting value: line 1 column 2 (char 1)`.
|
|
1354
|
+
#
|
|
1355
|
+
# The working replacement (confirmed live) is `www.niftyindices.com/BackPage/*`
|
|
1356
|
+
# (note: `www.` + `BackPage` not `Backpage.aspx`), which wants a short session
|
|
1357
|
+
# warm-up first (visiting the historical-data report page) and returns a
|
|
1358
|
+
# direct JSON array rather than the old `{"d": "..."}` wrapper.
|
|
758
1359
|
|
|
759
1360
|
niftyindices_headers = {
|
|
760
|
-
'Connection': 'keep-alive',
|
|
761
|
-
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
|
|
762
1361
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
|
763
|
-
'
|
|
1362
|
+
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
|
|
1363
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
1364
|
+
'Origin': 'https://www.niftyindices.com',
|
|
1365
|
+
'Referer': 'https://www.niftyindices.com/reports/historical-data',
|
|
1366
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
|
764
1367
|
'X-Requested-With': 'XMLHttpRequest',
|
|
1368
|
+
'sec-ch-ua': '"Not;A=Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
|
|
765
1369
|
'sec-ch-ua-mobile': '?0',
|
|
766
|
-
'
|
|
767
|
-
'Content-Type': 'application/json; charset=UTF-8',
|
|
768
|
-
'Origin': 'https://niftyindices.com',
|
|
769
|
-
'Sec-Fetch-Site': 'same-origin',
|
|
770
|
-
'Sec-Fetch-Mode': 'cors',
|
|
771
|
-
'Sec-Fetch-Dest': 'empty',
|
|
772
|
-
'Referer': 'https://niftyindices.com/reports/historical-data',
|
|
773
|
-
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
|
|
1370
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
774
1371
|
}
|
|
775
1372
|
|
|
776
|
-
|
|
1373
|
+
_niftyindices_session = None
|
|
1374
|
+
_niftyindices_warmed = False
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
def _get_niftyindices_session():
|
|
1378
|
+
global _niftyindices_session, _niftyindices_warmed
|
|
1379
|
+
if _niftyindices_session is None:
|
|
1380
|
+
_niftyindices_session = requests.Session()
|
|
1381
|
+
if not _niftyindices_warmed:
|
|
1382
|
+
try:
|
|
1383
|
+
_niftyindices_session.get(
|
|
1384
|
+
"https://www.niftyindices.com/reports/historical-data",
|
|
1385
|
+
headers=niftyindices_headers, timeout=15,
|
|
1386
|
+
)
|
|
1387
|
+
_niftyindices_warmed = True
|
|
1388
|
+
except Exception as e:
|
|
1389
|
+
logging.warning("niftyindices.com session warm-up failed/partial: %s", e)
|
|
1390
|
+
return _niftyindices_session
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
def _niftyindices_fetch(endpoint, symbol, start_date, end_date):
|
|
1394
|
+
session = _get_niftyindices_session()
|
|
777
1395
|
data = {'cinfo': "{'name':'" + symbol + "','startDate':'" + start_date + "','endDate':'" + end_date + "','indexName':'" + symbol + "'}"}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1396
|
+
response = session.post(
|
|
1397
|
+
f"https://www.niftyindices.com/BackPage/{endpoint}",
|
|
1398
|
+
headers=niftyindices_headers, json=data, timeout=20,
|
|
1399
|
+
)
|
|
1400
|
+
text = response.text.strip()
|
|
1401
|
+
if text.startswith('<!DOCTYPE') or text.startswith('<html') or text == "":
|
|
1402
|
+
raise NSEEndpointError(
|
|
1403
|
+
f"niftyindices.com/BackPage/{endpoint} returned HTML/empty instead of JSON "
|
|
1404
|
+
f"(HTTP {response.status_code}) -- the site may be down or have changed again."
|
|
1405
|
+
)
|
|
1406
|
+
try:
|
|
1407
|
+
payload = response.json()
|
|
1408
|
+
except ValueError:
|
|
1409
|
+
raise NSEEndpointError(
|
|
1410
|
+
f"niftyindices.com/BackPage/{endpoint}: non-JSON body (HTTP {response.status_code})"
|
|
1411
|
+
)
|
|
1412
|
+
# Old API wrapped the payload as {"d": "<json string>"}; the new one
|
|
1413
|
+
# returns the array directly. Support both so this keeps working if
|
|
1414
|
+
# niftyindices.com ever reverts/mixes the two shapes.
|
|
1415
|
+
if isinstance(payload, dict) and "d" in payload:
|
|
1416
|
+
payload = json.loads(payload["d"])
|
|
1417
|
+
return pd.DataFrame.from_records(payload)
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def index_history(symbol,start_date,end_date):
|
|
1421
|
+
return _niftyindices_fetch("getHistoricaldatatabletoString", symbol, start_date, end_date)
|
|
782
1422
|
|
|
783
1423
|
def index_pe_pb_div(symbol,start_date,end_date):
|
|
784
|
-
|
|
785
|
-
payload = requests.post('https://niftyindices.com/Backpage.aspx/getpepbHistoricaldataDBtoString', headers=niftyindices_headers, json=data).json()
|
|
786
|
-
payload = json.loads(payload["d"])
|
|
787
|
-
payload=pd.DataFrame.from_records(payload)
|
|
788
|
-
return payload
|
|
1424
|
+
return _niftyindices_fetch("getpepbHistoricaldataDBtoString", symbol, start_date, end_date)
|
|
789
1425
|
|
|
790
1426
|
def index_total_returns(symbol,start_date,end_date):
|
|
791
|
-
|
|
792
|
-
payload = requests.post('https://niftyindices.com/Backpage.aspx/getTotalReturnIndexString', headers=niftyindices_headers, json=data).json()
|
|
793
|
-
payload = json.loads(payload["d"])
|
|
794
|
-
payload=pd.DataFrame.from_records(payload)
|
|
795
|
-
return payload
|
|
1427
|
+
return _niftyindices_fetch("getTotalReturnIndexString", symbol, start_date, end_date)
|
|
796
1428
|
|
|
797
1429
|
def get_bhavcopy(date):
|
|
798
1430
|
date = date.replace("-","")
|
|
799
|
-
payload=pd.read_csv(
|
|
1431
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1432
|
+
"https://archives.nseindia.com/products/content/sec_bhavdata_full_"+date+".csv")))
|
|
800
1433
|
return payload
|
|
801
1434
|
|
|
802
1435
|
def get_bulkdeals():
|
|
803
|
-
payload=pd.read_csv(
|
|
1436
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1437
|
+
"https://archives.nseindia.com/content/equities/bulk.csv")))
|
|
804
1438
|
return payload
|
|
805
1439
|
|
|
806
1440
|
def get_blockdeals():
|
|
807
|
-
payload=pd.read_csv(
|
|
1441
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1442
|
+
"https://archives.nseindia.com/content/equities/block.csv")))
|
|
808
1443
|
return payload
|
|
809
1444
|
|
|
1445
|
+
def _nse_top_corp_info(symbol):
|
|
1446
|
+
"""`/api/top-corp-info?symbol=X&market=equities` bundles a company's
|
|
1447
|
+
latest announcements, corporate actions (bonus/dividend/split/demerger),
|
|
1448
|
+
shareholding pattern history, financial results, and board meetings in
|
|
1449
|
+
one call -- confirmed live and working through curl_cffi+warm-up. This
|
|
1450
|
+
backs both dividend_timeline() and share_holding() below."""
|
|
1451
|
+
symbol = nsesymbolpurify(symbol)
|
|
1452
|
+
return nsefetch(f"https://www.nseindia.com/api/top-corp-info?symbol={symbol}&market=equities")
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
def dividend_timeline(symbol):
|
|
1456
|
+
"""github.com/aeron7/nsepython issue #75: documented on
|
|
1457
|
+
unofficed.com/nse-python/ but never actually implemented in the code
|
|
1458
|
+
(calling it raised `AttributeError: module 'nsepython' has no attribute
|
|
1459
|
+
'dividend_timeline'`). Implemented here from `/api/top-corp-info`'s
|
|
1460
|
+
`corporate_actions` list, filtered down to the dividend-purpose entries
|
|
1461
|
+
(that list also contains bonuses/splits/demergers/etc, which this
|
|
1462
|
+
function intentionally excludes to match its name)."""
|
|
1463
|
+
data = _nse_top_corp_info(symbol)
|
|
1464
|
+
actions = (data.get("corporate_actions") or {}).get("data") or []
|
|
1465
|
+
dividends = [a for a in actions if "dividend" in (a.get("purpose") or "").lower()]
|
|
1466
|
+
return pd.DataFrame.from_records(dividends)
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def share_holding(symbol):
|
|
1470
|
+
"""github.com/aeron7/nsepython issue #75: same situation as
|
|
1471
|
+
dividend_timeline() above -- documented but not implemented. Built from
|
|
1472
|
+
`/api/top-corp-info`'s `shareholdings_patterns` data, which is a dict
|
|
1473
|
+
keyed by filing date (e.g. "31-Mar-2026") whose value is a list of
|
|
1474
|
+
{"<category>": "<percent>"} rows (Promoter & Promoter Group / Public /
|
|
1475
|
+
Shares held by Employee Trusts / Total). Flattened here into one row per
|
|
1476
|
+
filing date with a column per category, newest filing first."""
|
|
1477
|
+
data = _nse_top_corp_info(symbol)
|
|
1478
|
+
by_date = (data.get("shareholdings_patterns") or {}).get("data") or {}
|
|
1479
|
+
rows = []
|
|
1480
|
+
for filing_date, categories in by_date.items():
|
|
1481
|
+
row = {"date": filing_date}
|
|
1482
|
+
for entry in categories:
|
|
1483
|
+
for k, v in entry.items():
|
|
1484
|
+
row[k.strip()] = v.strip() if isinstance(v, str) else v
|
|
1485
|
+
rows.append(row)
|
|
1486
|
+
df = pd.DataFrame.from_records(rows)
|
|
1487
|
+
if not df.empty and "date" in df.columns:
|
|
1488
|
+
try:
|
|
1489
|
+
df = df.sort_values(
|
|
1490
|
+
by="date",
|
|
1491
|
+
key=lambda s: pd.to_datetime(s, format="%d-%b-%Y"),
|
|
1492
|
+
ascending=False,
|
|
1493
|
+
).reset_index(drop=True)
|
|
1494
|
+
except Exception:
|
|
1495
|
+
pass
|
|
1496
|
+
return df
|
|
1497
|
+
|
|
1498
|
+
|
|
810
1499
|
#Request from subhash
|
|
811
1500
|
## https://unofficed.com/how-to-find-the-beta-of-indian-stocks-using-python/
|
|
812
1501
|
def get_beta_df_maker(symbol,days):
|
|
@@ -841,11 +1530,32 @@ def getbeta(symbol,days=365,symbol2="NIFTY 50"):
|
|
|
841
1530
|
|
|
842
1531
|
def get_beta(symbol,days=365,symbol2="NIFTY 50"):
|
|
843
1532
|
#Default is 248 days. (Input of Subhash)
|
|
844
|
-
|
|
845
|
-
|
|
1533
|
+
# github.com/aeron7/nsepython issue #75: this used to raise a raw
|
|
1534
|
+
# KeyError('data') because equity_history() silently returned {} on a
|
|
1535
|
+
# blocked/retired endpoint. nsefetch() now raises a descriptive
|
|
1536
|
+
# NSEEndpointError instead of swallowing the failure -- surface that
|
|
1537
|
+
# (plus any other unexpected shape problem) as a clear, named error
|
|
1538
|
+
# instead of a bare KeyError, per the issue reporter's own suggestion.
|
|
1539
|
+
try:
|
|
1540
|
+
df = get_beta_df_maker(symbol,days)
|
|
1541
|
+
df2 = get_beta_df_maker(symbol2,days)
|
|
1542
|
+
except NSEEndpointError:
|
|
1543
|
+
raise
|
|
1544
|
+
except Exception as e:
|
|
1545
|
+
raise NSEEndpointError(
|
|
1546
|
+
f"get_beta({symbol!r}, symbol2={symbol2!r}): could not build the "
|
|
1547
|
+
f"daily-change series needed for beta -- {e}"
|
|
1548
|
+
) from e
|
|
846
1549
|
|
|
847
1550
|
x=df["daily_change"].tolist()
|
|
848
1551
|
y=df2["daily_change"].tolist()
|
|
1552
|
+
|
|
1553
|
+
if not x or not y:
|
|
1554
|
+
raise NSEEndpointError(
|
|
1555
|
+
f"get_beta({symbol!r}, symbol2={symbol2!r}): got no historical "
|
|
1556
|
+
f"price data back for the requested {days}-day window."
|
|
1557
|
+
)
|
|
1558
|
+
|
|
849
1559
|
#stackoverflow.com/questions/42670055/is-there-any-better-way-to-calculate-the-covariance-of-two-lists-than-this
|
|
850
1560
|
mean_x = sum(x) / len(x)
|
|
851
1561
|
mean_y = sum(y) / len(y)
|
|
@@ -854,12 +1564,26 @@ def get_beta(symbol,days=365,symbol2="NIFTY 50"):
|
|
|
854
1564
|
mean = sum(y) / len(y)
|
|
855
1565
|
variance = sum((i - mean) ** 2 for i in y) / len(y)
|
|
856
1566
|
|
|
1567
|
+
if variance == 0:
|
|
1568
|
+
raise NSEEndpointError(
|
|
1569
|
+
f"get_beta({symbol!r}, symbol2={symbol2!r}): symbol2 had zero "
|
|
1570
|
+
f"price variance over this window, beta is undefined."
|
|
1571
|
+
)
|
|
1572
|
+
|
|
857
1573
|
beta = covariance/variance
|
|
858
1574
|
return round(beta,3)
|
|
859
1575
|
|
|
860
1576
|
def nse_preopen(key="NIFTY",type="pandas"):
|
|
861
1577
|
payload = nsefetch("https://www.nseindia.com/api/market-data-pre-open?key="+key+"")
|
|
862
1578
|
if(type=="pandas"):
|
|
1579
|
+
# NSE's pre-open-market window for most `key` values (e.g. "NIFTY")
|
|
1580
|
+
# is only populated for a few minutes each morning; outside that
|
|
1581
|
+
# window `data` is a legitimate empty list ({"data": [], "msg": "No
|
|
1582
|
+
# Data Found"}), which used to raise a confusing KeyError('metadata')
|
|
1583
|
+
# trying to pull a column out of an empty DataFrame. Return an empty
|
|
1584
|
+
# DataFrame instead.
|
|
1585
|
+
if not payload.get('data'):
|
|
1586
|
+
return pd.DataFrame()
|
|
863
1587
|
payload = pd.DataFrame(payload['data'])
|
|
864
1588
|
payload = pd.json_normalize(payload['metadata'])
|
|
865
1589
|
return payload
|
|
@@ -886,7 +1610,8 @@ def nse_most_active(type="securities",sort="value"):
|
|
|
886
1610
|
|
|
887
1611
|
def nse_eq_symbols():
|
|
888
1612
|
#https://forum.unofficed.com/t/feature-request-stocklist-api/1073/11
|
|
889
|
-
eq_list_pd = pd.read_csv(
|
|
1613
|
+
eq_list_pd = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1614
|
+
'https://archives.nseindia.com/content/equities/EQUITY_L.csv')))
|
|
890
1615
|
return eq_list_pd['SYMBOL'].tolist()
|
|
891
1616
|
|
|
892
1617
|
def nse_price_band_hitters(bandtype="both",view="AllSec"):
|
|
@@ -904,16 +1629,42 @@ def nse_largedeals(mode="bulk_deals"):
|
|
|
904
1629
|
return pd.DataFrame(payload["SHORT_DEALS_DATA"])
|
|
905
1630
|
if(mode=="block_deals"):
|
|
906
1631
|
return pd.DataFrame(payload["BLOCK_DEALS_DATA"])
|
|
907
|
-
|
|
1632
|
+
|
|
908
1633
|
def nse_largedeals_historical(from_date, to_date, mode="bulk_deals"):
|
|
1634
|
+
# The old /api/historical/{bulk-deals,short-selling,block-deals} family is
|
|
1635
|
+
# retired on the live site (confirmed HTTP 503 straight from NSE's origin
|
|
1636
|
+
# -- not an Akamai bot-challenge: the 503 body is a tiny generic Apache
|
|
1637
|
+
# ErrorDocument page returned with a consistent ~20-30ms *origin* timing
|
|
1638
|
+
# on every single attempt, with or without warm-up/referer variations,
|
|
1639
|
+
# which is the signature of a dead backend route rather than a solvable
|
|
1640
|
+
# JS sensor wall).
|
|
1641
|
+
#
|
|
1642
|
+
# Found the real, current replacement by driving NSE's own "Bulk Deals/
|
|
1643
|
+
# Block Deals/ Short Selling Archives" report page
|
|
1644
|
+
# (https://www.nseindia.com/report-detail/display-bulk-and-block-deals)
|
|
1645
|
+
# with Playwright and capturing what it actually calls when you click
|
|
1646
|
+
# Go: `/api/historicalOR/bulk-block-short-deals?optionType=<mode>&from=
|
|
1647
|
+
# ..&to=..` -- same host-prefix swap pattern as equity/derivatives above,
|
|
1648
|
+
# just a different path and param name (`optionType=`, not a path
|
|
1649
|
+
# segment), confirmed live for all three modes. Response shape is the
|
|
1650
|
+
# same `{"data": [...]}` the old endpoint returned, just with a different
|
|
1651
|
+
# (current) NSE column-name scheme:
|
|
1652
|
+
# bulk_deals/block_deals -> BD_DT_DATE, BD_DT_ORDER, BD_SYMBOL,
|
|
1653
|
+
# BD_SCRIP_NAME, BD_CLIENT_NAME, BD_BUY_SELL,
|
|
1654
|
+
# BD_QTY_TRD, BD_TP_WATP, BD_REMARKS
|
|
1655
|
+
# short_deals -> SS_DATE, SS_DATE_ORDER, SS_SYMBOL, SS_NAME,
|
|
1656
|
+
# SS_QTY
|
|
909
1657
|
if mode == "bulk_deals":
|
|
910
|
-
|
|
1658
|
+
option_type = "bulk_deals"
|
|
911
1659
|
elif mode == "short_deals":
|
|
912
|
-
|
|
1660
|
+
option_type = "short_selling"
|
|
913
1661
|
elif mode == "block_deals":
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1662
|
+
option_type = "block_deals"
|
|
1663
|
+
else:
|
|
1664
|
+
option_type = mode
|
|
1665
|
+
|
|
1666
|
+
url = ('https://www.nseindia.com/api/historicalOR/bulk-block-short-deals'
|
|
1667
|
+
'?optionType=' + option_type + '&from=' + from_date + '&to=' + to_date)
|
|
917
1668
|
logging.info("Fetching " + str(url))
|
|
918
1669
|
payload = nsefetch(url)
|
|
919
1670
|
return pd.DataFrame(payload["data"])
|
|
@@ -922,45 +1673,86 @@ def nse_largedeals_historical(from_date, to_date, mode="bulk_deals"):
|
|
|
922
1673
|
#print(get_fao_participant_oi("04-06-2021"))
|
|
923
1674
|
def get_fao_participant_oi(date):
|
|
924
1675
|
date = date.replace("-","")
|
|
925
|
-
payload=pd.read_csv(
|
|
1676
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1677
|
+
"https://archives.nseindia.com/content/nsccl/fao_participant_oi_"+date+".csv")))
|
|
926
1678
|
return payload
|
|
927
1679
|
|
|
928
1680
|
#https://forum.unofficed.com/t/how-to-check-if-the-market-is-open-today-or-not/1268/1
|
|
929
1681
|
def is_market_open(segment = "FO"): #COM,CD,CB,CMOT,COM,FO,IRD,MF,NDM,NTRP,SLBS
|
|
930
|
-
|
|
1682
|
+
# Bug fix: the previous version returned True/False based only on
|
|
1683
|
+
# holiday_json's *first* entry, so it almost always reported "open"
|
|
1684
|
+
# regardless of today's actual date (today is essentially never the
|
|
1685
|
+
# first holiday in the list). Scan the whole list for a match instead.
|
|
931
1686
|
holiday_json = nse_holidays()[segment]
|
|
932
1687
|
|
|
933
1688
|
# Get today's date in the format 'dd-Mon-yyyy'
|
|
934
1689
|
today_date = datetime.date.today().strftime('%d-%b-%Y')
|
|
935
1690
|
|
|
936
|
-
# Check if today's date is in the holiday_json
|
|
937
1691
|
for holiday in holiday_json:
|
|
938
|
-
if holiday
|
|
939
|
-
print("
|
|
940
|
-
return True
|
|
941
|
-
if holiday['tradingDate'] == today_date:
|
|
942
|
-
print(f"Market is closed today because of {holiday['description']}")
|
|
1692
|
+
if holiday.get('tradingDate') == today_date:
|
|
1693
|
+
print(f"Market is closed today because of {holiday.get('description')}")
|
|
943
1694
|
return False
|
|
944
1695
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if(meta=="Futures"):
|
|
949
|
-
selected_key = next((key for key in payload["expiryDatesByInstrument"] if "futures" in key.lower()), None)
|
|
950
|
-
if(meta=="Options"):
|
|
951
|
-
selected_key = next((key for key in payload["expiryDatesByInstrument"] if "options" in key.lower()), None)
|
|
1696
|
+
print("FNO Market is open today. Have a Nice Trade!")
|
|
1697
|
+
return True
|
|
952
1698
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
expiry_dates = [
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1699
|
+
def nse_expirydetails_by_symbol(symbol,meta ="Futures",i=0):
|
|
1700
|
+
payload = nse_quote_derivatives(symbol)
|
|
1701
|
+
expiry_dates = []
|
|
1702
|
+
|
|
1703
|
+
# Extract from new FNO payload structure
|
|
1704
|
+
if 'data' in payload:
|
|
1705
|
+
unique_dates = set()
|
|
1706
|
+
for entry in payload['data']:
|
|
1707
|
+
if 'expiryDate' in entry:
|
|
1708
|
+
# Filter by meta type if possible, though 'data' usually contains all
|
|
1709
|
+
# To be precise, we can check instrumentType
|
|
1710
|
+
it = entry.get('instrumentType', '')
|
|
1711
|
+
if (meta == "Futures" and "FUT" in it) or (meta == "Options" and "OPT" in it):
|
|
1712
|
+
unique_dates.add(entry['expiryDate'])
|
|
1713
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
1714
|
+
|
|
1715
|
+
# Filter future dates
|
|
1716
|
+
future_expiry_dates = []
|
|
1717
|
+
if expiry_dates:
|
|
1718
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
1719
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
1720
|
+
|
|
1721
|
+
# Fallback to expiry_list if i is out of range
|
|
1722
|
+
if i >= len(future_expiry_dates):
|
|
1723
|
+
dates = expiry_list(symbol, type="list")
|
|
1724
|
+
if dates:
|
|
1725
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
1726
|
+
future_expiry_dates = sorted([date.strftime("%d-%b-%Y") for date in temp_dates if date >= datetime.datetime.now().date()], key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
1727
|
+
|
|
1728
|
+
if i >= len(future_expiry_dates):
|
|
1729
|
+
return None, None
|
|
1730
|
+
|
|
1731
|
+
currentExpiry = future_expiry_dates[i]
|
|
1732
|
+
currentExpiry_dt = datetime.datetime.strptime(currentExpiry, '%d-%b-%Y').date()
|
|
1733
|
+
date_today = run_time.date()
|
|
1734
|
+
dte = (currentExpiry_dt - date_today).days
|
|
1735
|
+
return currentExpiry_dt, dte
|
|
1736
|
+
|
|
1737
|
+
def security_wise_archive(from_date, to_date, symbol, series="ALL"):
|
|
1738
|
+
# The old /api/historical/securityArchives route is retired on the live
|
|
1739
|
+
# site (confirmed HTTP 503 straight from NSE's origin -- same dead-route
|
|
1740
|
+
# signature as nse_largedeals_historical() above, not a solvable Akamai
|
|
1741
|
+
# challenge: tiny generic Apache ErrorDocument body, consistent fast
|
|
1742
|
+
# origin timing on every attempt regardless of warm-up/referer).
|
|
1743
|
+
#
|
|
1744
|
+
# Found the real, current replacement by driving NSE's own "Security-wise
|
|
1745
|
+
# Archives (Equities)" report page
|
|
1746
|
+
# (https://www.nseindia.com/report-detail/eq_security) with Playwright
|
|
1747
|
+
# and capturing what it actually calls when you click Go:
|
|
1748
|
+
# `/api/historicalOR/generateSecurityWiseHistoricalData?from=..&to=..&
|
|
1749
|
+
# symbol=..&type=..&series=..` -- same host-prefix-swap family as
|
|
1750
|
+
# equity_history()/derivative_history() above, just a different path and
|
|
1751
|
+
# `type=` instead of `dataType=`. Confirmed live: response shape is the
|
|
1752
|
+
# same `{"data": [...]}` with the same CH_*/COP_DELIV_* column names the
|
|
1753
|
+
# old endpoint used (cross-checked against equity_history()'s numbers for
|
|
1754
|
+
# the same symbol/dates -- exact match).
|
|
1755
|
+
base_url = "https://www.nseindia.com/api/historicalOR/generateSecurityWiseHistoricalData"
|
|
1756
|
+
url = f"{base_url}?from={from_date}&to={to_date}&symbol={symbol.upper()}&type=priceVolumeDeliverable&series={series.upper()}"
|
|
965
1757
|
payload = nsefetch(url)
|
|
966
|
-
return pd.DataFrame(payload['data'])
|
|
1758
|
+
return pd.DataFrame(payload['data'])
|