nsepython 2.97__py3-none-any.whl → 2.99__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 +2063 -327
- {nsepython-2.97.dist-info → nsepython-2.99.dist-info}/METADATA +4 -3
- nsepython-2.99.dist-info/RECORD +7 -0
- {nsepython-2.97.dist-info → nsepython-2.99.dist-info}/WHEEL +1 -1
- nsepython-2.97.dist-info/RECORD +0 -7
- {nsepython-2.97.dist-info → nsepython-2.99.dist-info}/licenses/LICENSE +0 -0
- {nsepython-2.97.dist-info → nsepython-2.99.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,91 +9,297 @@ 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
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
255
|
+
|
|
87
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
|
|
88
269
|
|
|
89
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
|
|
|
93
288
|
run_time=datetime.datetime.now()
|
|
94
289
|
|
|
95
290
|
#Constants
|
|
96
|
-
|
|
291
|
+
#
|
|
292
|
+
# Round 3: NSE has since added live F&O index-derivative products beyond
|
|
293
|
+
# the original 3 (confirmed live -- getSymbolDerivativesData&symbol=
|
|
294
|
+
# MIDCPNIFTY and &symbol=NIFTYNXT50 both return real, actively-traded
|
|
295
|
+
# CE/PE records right now, 2026-10). nse_quote()'s own `any(x in symbol
|
|
296
|
+
# for x in indices)` substring check already happened to work for these by
|
|
297
|
+
# accident (both names contain the substring "NIFTY"), but fnolist()'s
|
|
298
|
+
# exact-membership check (used by nse_quote_derivatives()) did not, which
|
|
299
|
+
# silently made nse_quote_ltp()/nse_quote_meta() return 0/{} for these
|
|
300
|
+
# symbols instead of real data. Listed explicitly here (not just relying on
|
|
301
|
+
# substring luck) so fnolist() membership works for them too.
|
|
302
|
+
indices = ['NIFTY','FINNIFTY','BANKNIFTY','MIDCPNIFTY','NIFTYNXT50']
|
|
97
303
|
|
|
98
304
|
def running_status():
|
|
99
305
|
start_now=datetime.datetime.now().replace(hour=9, minute=15, second=0, microsecond=0)
|
|
@@ -102,17 +308,10 @@ def running_status():
|
|
|
102
308
|
|
|
103
309
|
#Getting FNO Symboles
|
|
104
310
|
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
311
|
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
|
-
|
|
312
|
+
nselist = indices.copy()
|
|
313
|
+
for x in range(len(positions['data'])):
|
|
314
|
+
nselist.append(positions['data'][x]['symbol'])
|
|
116
315
|
return nselist
|
|
117
316
|
|
|
118
317
|
def nsesymbolpurify(symbol):
|
|
@@ -121,15 +320,46 @@ def nsesymbolpurify(symbol):
|
|
|
121
320
|
|
|
122
321
|
def nse_optionchain_scrapper(symbol):
|
|
123
322
|
symbol = nsesymbolpurify(symbol)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
323
|
+
# Using getSymbolDerivativesData as it provides all expiries and strikes in one go
|
|
324
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolDerivativesData&symbol={symbol}'
|
|
325
|
+
payload = nsefetch(url)
|
|
326
|
+
|
|
327
|
+
# Transformation to match the "data" structure expected by pcr and other functions
|
|
328
|
+
if payload and 'data' in payload:
|
|
329
|
+
new_data = []
|
|
330
|
+
# Group by strikePrice and expiryDate to create a combined CE/PE structure if possible,
|
|
331
|
+
# or just provide the raw list if the consumers can handle it.
|
|
332
|
+
# The current pcr() handles a list of entries where each has CE/PE keys OR is the entry itself.
|
|
333
|
+
|
|
334
|
+
# Actually, let's restructure it to be more compatible with the expected 'data' format:
|
|
335
|
+
# a list of dictionaries, each having 'strikePrice', 'expiryDate', 'CE', 'PE'.
|
|
336
|
+
combined = {}
|
|
337
|
+
for entry in payload['data']:
|
|
338
|
+
sp = entry.get('strikePrice')
|
|
339
|
+
ed = entry.get('expiryDate')
|
|
340
|
+
ot = entry.get('optionType')
|
|
341
|
+
if not sp or not ed or ot == 'XX': continue
|
|
342
|
+
|
|
343
|
+
key = (sp, ed)
|
|
344
|
+
if key not in combined:
|
|
345
|
+
combined[key] = {'strikePrice': sp, 'expiryDate': ed, 'CE': None, 'PE': None}
|
|
346
|
+
|
|
347
|
+
combined[key][ot] = entry
|
|
348
|
+
|
|
349
|
+
payload['data'] = list(combined.values())
|
|
350
|
+
|
|
128
351
|
return payload
|
|
129
352
|
|
|
130
353
|
|
|
131
354
|
def oi_chain_builder(symbol,expiry="latest",oi_mode="full"):
|
|
132
355
|
|
|
356
|
+
if expiry == "latest":
|
|
357
|
+
dates = expiry_list(symbol, type="list")
|
|
358
|
+
if dates:
|
|
359
|
+
expiry = dates[0]
|
|
360
|
+
else:
|
|
361
|
+
return pd.DataFrame(), 0.0, ""
|
|
362
|
+
|
|
133
363
|
payload = nse_optionchain_scrapper(symbol)
|
|
134
364
|
|
|
135
365
|
if(oi_mode=='compact'):
|
|
@@ -138,109 +368,439 @@ def oi_chain_builder(symbol,expiry="latest",oi_mode="full"):
|
|
|
138
368
|
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
369
|
oi_data = pd.DataFrame(columns = col_names)
|
|
140
370
|
|
|
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']
|
|
371
|
+
# We will populate these dynamically
|
|
372
|
+
rows_list = []
|
|
373
|
+
|
|
374
|
+
if 'expiryDates' not in payload:
|
|
375
|
+
# Fallback for new API structure
|
|
376
|
+
if(expiry=="latest"):
|
|
377
|
+
expiry = expiry_list(symbol, type="list")[0]
|
|
378
|
+
data_list = payload['data']
|
|
379
|
+
else:
|
|
380
|
+
# Legacy structure support
|
|
381
|
+
if(expiry=="latest"):
|
|
382
|
+
expiry = payload['records']['expiryDates'][0]
|
|
383
|
+
data_list = payload['records']['data']
|
|
198
384
|
|
|
385
|
+
for m in range(len(data_list)):
|
|
386
|
+
current_expiry_str = data_list[m].get('expiryDates') or data_list[m].get('expiryDate')
|
|
387
|
+
try:
|
|
388
|
+
# Convert both to date objects for robust comparison
|
|
389
|
+
if "-" in current_expiry_str:
|
|
390
|
+
parts = current_expiry_str.split("-")
|
|
391
|
+
if parts[1].isdigit(): fmt = "%d-%m-%Y"
|
|
392
|
+
else: fmt = "%d-%b-%Y"
|
|
393
|
+
curr_date = datetime.datetime.strptime(current_expiry_str, fmt).date()
|
|
394
|
+
|
|
395
|
+
parts_exp = expiry.split("-")
|
|
396
|
+
if parts_exp[1].isdigit(): fmt_exp = "%d-%m-%Y"
|
|
397
|
+
else: fmt_exp = "%d-%b-%Y"
|
|
398
|
+
exp_date = datetime.datetime.strptime(expiry, fmt_exp).date()
|
|
399
|
+
match = (curr_date == exp_date)
|
|
400
|
+
else:
|
|
401
|
+
match = (current_expiry_str == expiry)
|
|
402
|
+
except:
|
|
403
|
+
match = (current_expiry_str == expiry)
|
|
404
|
+
|
|
405
|
+
if match:
|
|
406
|
+
oi_row = {col: 0 for col in col_names}
|
|
407
|
+
oi_row['Strike Price'] = data_list[m]['strikePrice']
|
|
408
|
+
|
|
409
|
+
for side in ['CE', 'PE']:
|
|
410
|
+
prefix = f"{'CALLS' if side == 'CE' else 'PUTS'}_"
|
|
411
|
+
if side in data_list[m] and data_list[m][side] is not None:
|
|
412
|
+
d = data_list[m][side]
|
|
413
|
+
oi_row[prefix + 'OI'] = d.get('openInterest', 0)
|
|
414
|
+
oi_row[prefix + 'Chng in OI'] = d.get('changeinOpenInterest', 0)
|
|
415
|
+
oi_row[prefix + 'Volume'] = d.get('totalTradedVolume', 0)
|
|
416
|
+
oi_row[prefix + 'IV'] = d.get('impliedVolatility', 0)
|
|
417
|
+
oi_row[prefix + 'LTP'] = d.get('lastPrice', 0)
|
|
418
|
+
oi_row[prefix + 'Net Chng'] = d.get('change', 0)
|
|
419
|
+
|
|
420
|
+
if oi_mode == 'full':
|
|
421
|
+
# New API key mapping
|
|
422
|
+
oi_row[prefix + 'Bid Qty'] = d.get('buyQuantity1', d.get('bidQty', 0))
|
|
423
|
+
oi_row[prefix + 'Bid Price'] = d.get('buyPrice1', d.get('bidprice', 0))
|
|
424
|
+
oi_row[prefix + 'Ask Price'] = d.get('sellPrice1', d.get('askPrice', 0))
|
|
425
|
+
oi_row[prefix + 'Ask Qty'] = d.get('sellQuantity1', d.get('askQty', 0))
|
|
426
|
+
oi_row[prefix + 'Chart'] = 0
|
|
427
|
+
|
|
428
|
+
rows_list.append(oi_row)
|
|
429
|
+
|
|
430
|
+
oi_data = pd.DataFrame(rows_list)
|
|
431
|
+
timestamp = payload.get('timestamp', payload.get('records', {}).get('timestamp', ''))
|
|
432
|
+
underlyingValue = payload.get('underlyingValue', payload.get('records', {}).get('underlyingValue', 0))
|
|
433
|
+
|
|
434
|
+
# github.com/aeron7/nsepython issue #80: the current getSymbolDerivativesData
|
|
435
|
+
# payload carries no top-level (or 'records') underlyingValue at all -- it
|
|
436
|
+
# only lives inside each individual CE/PE leaf record. Dig it out of there
|
|
437
|
+
# if the top-level lookup above came back empty.
|
|
438
|
+
if not underlyingValue and data_list:
|
|
439
|
+
for entry in data_list:
|
|
440
|
+
for side in ('CE', 'PE'):
|
|
441
|
+
leaf = entry.get(side)
|
|
442
|
+
if leaf and leaf.get('underlyingValue'):
|
|
443
|
+
underlyingValue = leaf['underlyingValue']
|
|
444
|
+
break
|
|
445
|
+
if underlyingValue:
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
oi_data['time_stamp'] = timestamp
|
|
449
|
+
return oi_data, float(underlyingValue or 0), timestamp
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def nse_quote_derivatives(symbol):
|
|
453
|
+
symbol = nsesymbolpurify(symbol)
|
|
454
|
+
# Round 3 bug fix: the membership check below was correctly
|
|
455
|
+
# case-insensitive (symbol.upper() in fnolist()) but the URL built right
|
|
456
|
+
# after it used the original, un-uppercased `symbol` -- the live
|
|
457
|
+
# getSymbolDerivativesData endpoint is itself case-sensitive, so a
|
|
458
|
+
# lowercase/mixed-case symbol (e.g. "sbin", "banknifty") silently came
|
|
459
|
+
# back as {'data': [], 'timestamp': ''} (a plausible-looking empty
|
|
460
|
+
# response, not an error) instead of real data. Uppercase once and reuse
|
|
461
|
+
# it for both the check and the fetch.
|
|
462
|
+
symbol_u = symbol.upper()
|
|
463
|
+
if symbol_u in fnolist():
|
|
464
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolDerivativesData&symbol='+symbol_u)
|
|
465
|
+
return payload
|
|
466
|
+
else:
|
|
467
|
+
return {"error": f"{symbol} is not in derivatives list."}
|
|
199
468
|
|
|
200
469
|
def nse_quote(symbol,section=""):
|
|
201
470
|
#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
|
|
471
|
+
#
|
|
472
|
+
# section='' (default) already returns the FULL detail in one call --
|
|
473
|
+
# metaData/secInfo/priceInfo/orderBook/tradeInfo are all present together
|
|
474
|
+
# in that single payload. Only pass section= for a genuine sub-slice:
|
|
475
|
+
# 'trade_info' -- order-book depth / VaR margin slice
|
|
476
|
+
# 'preOpenMarket' -- the day's 09:00-09:08 IST pre-open auction ladder
|
|
477
|
+
# Round 3 research (checked against unofficed.com's own docs, the
|
|
478
|
+
# hi-imcodeman/stock-nse-india reference TS implementation, and this
|
|
479
|
+
# project's entire GitHub issue history) found no evidence NSE's old API
|
|
480
|
+
# ever accepted any OTHER section= value -- 'preOpenMarket'/'metadata'/
|
|
481
|
+
# 'industryInfo'/'info'/'priceInfo'/'securityInfo' were never alternate
|
|
482
|
+
# query values, just top-level keys inside the un-sectioned response
|
|
483
|
+
# section='' already returns.
|
|
202
484
|
symbol = nsesymbolpurify(symbol)
|
|
485
|
+
# Round 3 bug fix: this substring check used to be case-sensitive, so a
|
|
486
|
+
# lowercase/mixed-case index name (e.g. "banknifty") fell through to the
|
|
487
|
+
# equity branch below and 404'd (banknifty isn't an equity symbol).
|
|
488
|
+
symbol_u = symbol.upper()
|
|
203
489
|
|
|
204
490
|
if(section==""):
|
|
205
|
-
if any(x in
|
|
206
|
-
payload = nsefetch('https://www.nseindia.com/api/
|
|
491
|
+
if any(x in symbol_u for x in indices):
|
|
492
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolDerivativesData&symbol='+symbol_u)
|
|
207
493
|
else:
|
|
208
|
-
payload = nsefetch('https://www.nseindia.com/api/
|
|
209
|
-
return payload
|
|
210
|
-
|
|
211
|
-
if(section!=""):
|
|
212
|
-
payload = nsefetch('https://www.nseindia.com/api/quote-equity?symbol='+symbol+'§ion='+section)
|
|
494
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol_u)
|
|
213
495
|
return payload
|
|
214
496
|
|
|
497
|
+
if(section=="trade_info"):
|
|
498
|
+
# The old /api/quote-equity?section=trade_info route is dead on the
|
|
499
|
+
# live site (confirmed HTTP 403, even through the fully-warmed
|
|
500
|
+
# curl_cffi session round 1 built). But every category of data the
|
|
501
|
+
# old endpoint used to return is already present, just reshuffled,
|
|
502
|
+
# inside the NEW working GetQuoteApi?functionName=getSymbolData
|
|
503
|
+
# response this function's section=="" branch already fetches --
|
|
504
|
+
# confirmed field-by-field against the real, documented old
|
|
505
|
+
# response shape (EquityTradeInfo: marketDeptOrderBook.{bid,ask,
|
|
506
|
+
# tradeInfo,valueAtRisk} + securityWiseDP), so this is a pure
|
|
507
|
+
# remap/slice of data already being fetched, not a new network call.
|
|
508
|
+
#
|
|
509
|
+
# Two small fidelity gaps versus the old route, both because the
|
|
510
|
+
# source data for them no longer exists anywhere in the new
|
|
511
|
+
# response (not a mapping oversight):
|
|
512
|
+
# - noBlockDeals/bulkBlockDeals: the new endpoint carries no
|
|
513
|
+
# block-deal info at all -> defaulted to True/[] (i.e. "no
|
|
514
|
+
# block deals known"), not derived from a live block-deal
|
|
515
|
+
# check. Use nse_blockdeal()/get_blockdeals() directly if you
|
|
516
|
+
# need real block-deal data.
|
|
517
|
+
# - securityWiseDP.seriesRemarks: no equivalent field exists in
|
|
518
|
+
# the new response -> always None, same as it is for most
|
|
519
|
+
# symbols on the old route anyway.
|
|
520
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol_u)
|
|
521
|
+
if 'equityResponse' not in payload or not payload['equityResponse']:
|
|
522
|
+
raise NSEEndpointError(
|
|
523
|
+
f"nse_quote({symbol!r}, section='trade_info'): no "
|
|
524
|
+
f"equityResponse in payload -- this section is only "
|
|
525
|
+
f"meaningful for an equity symbol (not an index/derivative "
|
|
526
|
+
f"underlying)."
|
|
527
|
+
)
|
|
528
|
+
eq = payload['equityResponse'][0]
|
|
529
|
+
ob = eq.get('orderBook', {})
|
|
530
|
+
md = eq.get('metaData', {})
|
|
531
|
+
ti = eq.get('tradeInfo', {})
|
|
532
|
+
pi = eq.get('priceInfo', {})
|
|
533
|
+
si = eq.get('secInfo', {})
|
|
534
|
+
|
|
535
|
+
bid = [{"price": ob.get(f"buyPrice{i}"), "quantity": ob.get(f"buyQuantity{i}")} for i in range(1, 6)]
|
|
536
|
+
ask = [{"price": ob.get(f"sellPrice{i}"), "quantity": ob.get(f"sellQuantity{i}")} for i in range(1, 6)]
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
"noBlockDeals": True,
|
|
540
|
+
"bulkBlockDeals": [],
|
|
541
|
+
"marketDeptOrderBook": {
|
|
542
|
+
"totalBuyQuantity": ob.get("totalBuyQuantity"),
|
|
543
|
+
"totalSellQuantity": ob.get("totalSellQuantity"),
|
|
544
|
+
"open": md.get("open"),
|
|
545
|
+
"bid": bid,
|
|
546
|
+
"ask": ask,
|
|
547
|
+
"tradeInfo": {
|
|
548
|
+
"totalTradedVolume": ti.get("totalTradedVolume"),
|
|
549
|
+
"totalTradedValue": ti.get("totalTradedValue"),
|
|
550
|
+
"totalMarketCap": ti.get("totalMarketCap"),
|
|
551
|
+
"ffmc": ti.get("ffmc"),
|
|
552
|
+
"impactCost": ti.get("impactCost"),
|
|
553
|
+
"cmDailyVolatility": pi.get("cmDailyVolatility"),
|
|
554
|
+
"cmAnnualVolatility": pi.get("cmAnnualVolatility"),
|
|
555
|
+
"marketLot": ti.get("marketLot"),
|
|
556
|
+
"activeSeries": ti.get("series"),
|
|
557
|
+
},
|
|
558
|
+
"valueAtRisk": {
|
|
559
|
+
"securityVar": si.get("securityvar"),
|
|
560
|
+
"indexVar": si.get("indexvar"),
|
|
561
|
+
"varMargin": si.get("varMargin"),
|
|
562
|
+
"extremeLossMargin": si.get("extremelossMargin"),
|
|
563
|
+
"adhocMargin": si.get("adhocMargin"),
|
|
564
|
+
"applicableMargin": si.get("applicableMargin"),
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
"securityWiseDP": {
|
|
568
|
+
"quantityTraded": ti.get("quantitytraded"),
|
|
569
|
+
"deliveryQuantity": ti.get("deliveryquantity"),
|
|
570
|
+
"deliveryToTradedQuantity": ti.get("deliveryToTradedQuantity"),
|
|
571
|
+
"seriesRemarks": None,
|
|
572
|
+
"secWiseDelPosDate": ti.get("secwisedelposdate"),
|
|
573
|
+
},
|
|
574
|
+
}
|
|
215
575
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
576
|
+
if(section=="preOpenMarket"):
|
|
577
|
+
# Round 3: NSE's old /api/quote-equity§ion=preOpenMarket route
|
|
578
|
+
# is dead (confirmed HTTP 403, same wall as every other section
|
|
579
|
+
# value below), but a genuinely live, working replacement exists:
|
|
580
|
+
# /api/market-data-pre-open?key=ALL returns ALL ~2200 symbols' real
|
|
581
|
+
# pre-open order-book ladders in one shot. Filter it down to the
|
|
582
|
+
# requested symbol instead of fabricating anything.
|
|
583
|
+
#
|
|
584
|
+
# Caveat (documented, not disguised): this is literally the
|
|
585
|
+
# 09:00-09:08 IST pre-open auction snapshot, not continuous/live
|
|
586
|
+
# intraday data -- checked well after market open it will look
|
|
587
|
+
# "stale" because it reflects that morning's last pre-open auction.
|
|
588
|
+
# That is the real, live content of this feed, not a bug.
|
|
589
|
+
payload = nsefetch('https://www.nseindia.com/api/market-data-pre-open?key=ALL')
|
|
590
|
+
for entry in payload.get('data', []):
|
|
591
|
+
if entry.get('metadata', {}).get('symbol') == symbol_u:
|
|
592
|
+
return entry['detail']['preOpenMarket']
|
|
593
|
+
raise NSEEndpointError(
|
|
594
|
+
f"nse_quote({symbol!r}, section='preOpenMarket'): {symbol_u} was "
|
|
595
|
+
f"not found in today's pre-open-market list -- either it isn't a "
|
|
596
|
+
f"pre-open-eligible series, or today's pre-open session hasn't "
|
|
597
|
+
f"run/populated yet."
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Round 3: every other section value (e.g. the old 'metadata'/
|
|
601
|
+
# 'industryInfo'/'info'/'priceInfo'/'securityInfo') used to fall through
|
|
602
|
+
# here and hit the dead /api/quote-equity§ion=X route -- a ~5s
|
|
603
|
+
# double-retry ending in a misleading HTTP 403, for a route that was
|
|
604
|
+
# never real in the first place. Checked against unofficed.com's own
|
|
605
|
+
# docs, the hi-imcodeman/stock-nse-india reference implementation, and
|
|
606
|
+
# this project's full GitHub issue history: NSE's API never accepted
|
|
607
|
+
# any section value beyond 'trade_info' -- those other names are just
|
|
608
|
+
# top-level keys inside the un-sectioned response, already returned in
|
|
609
|
+
# full by nse_quote(symbol) (section=""). Raise immediately and clearly
|
|
610
|
+
# instead of a slow, confusing network round-trip to a route that was
|
|
611
|
+
# never real.
|
|
612
|
+
raise ValueError(
|
|
613
|
+
f"nse_quote: unsupported section={section!r}; only '' (full quote), "
|
|
614
|
+
f"'trade_info', and 'preOpenMarket' are supported -- NSE's old "
|
|
615
|
+
f"quote-equity API never had other section values. section='' "
|
|
616
|
+
f"already returns the full detail (metaData/secInfo/priceInfo/"
|
|
617
|
+
f"orderBook/tradeInfo all together)."
|
|
618
|
+
)
|
|
619
|
+
def nse_expirydetails(payload, i=0, symbol=None):
|
|
620
|
+
expiry_dates = []
|
|
621
|
+
if 'records' in payload:
|
|
622
|
+
expiry_dates = payload['records']['expiryDates']
|
|
623
|
+
elif 'expiryDates' in payload:
|
|
624
|
+
expiry_dates = payload['expiryDates']
|
|
625
|
+
elif 'data' in payload:
|
|
626
|
+
unique_dates = set()
|
|
627
|
+
for entry in payload['data']:
|
|
628
|
+
if 'expiryDate' in entry:
|
|
629
|
+
unique_dates.add(entry['expiryDate'])
|
|
630
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
631
|
+
|
|
632
|
+
# Filter future dates
|
|
633
|
+
future_expiry_dates = []
|
|
634
|
+
if expiry_dates:
|
|
635
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
636
|
+
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"))
|
|
637
|
+
|
|
638
|
+
# Fallback to expiry_list if i is out of range and we can determine the symbol
|
|
639
|
+
if i >= len(future_expiry_dates):
|
|
640
|
+
if not symbol and 'data' in payload and len(payload['data']) > 0:
|
|
641
|
+
# Try to extract symbol from payload data
|
|
642
|
+
first_entry = payload['data'][0]
|
|
643
|
+
symbol = first_entry.get('symbol')
|
|
644
|
+
if not symbol:
|
|
645
|
+
if 'CE' in first_entry and first_entry['CE']:
|
|
646
|
+
symbol = first_entry['CE'].get('underlying')
|
|
647
|
+
elif 'PE' in first_entry and first_entry['PE']:
|
|
648
|
+
symbol = first_entry['PE'].get('underlying')
|
|
649
|
+
|
|
650
|
+
if symbol:
|
|
651
|
+
dates = expiry_list(symbol, type="list")
|
|
652
|
+
if dates:
|
|
653
|
+
# Filter future dates from expiry_list as well
|
|
654
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
655
|
+
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"))
|
|
656
|
+
|
|
657
|
+
if i >= len(future_expiry_dates):
|
|
658
|
+
return None, None
|
|
659
|
+
|
|
660
|
+
currentExpiry = future_expiry_dates[i]
|
|
661
|
+
currentExpiry_dt = datetime.datetime.strptime(currentExpiry, '%d-%b-%Y').date()
|
|
662
|
+
date_today = run_time.date()
|
|
663
|
+
dte = (currentExpiry_dt - date_today).days
|
|
664
|
+
return currentExpiry_dt, dte
|
|
665
|
+
|
|
666
|
+
def _pcr_entry_oi(entry):
|
|
667
|
+
"""Round 3 fix: pcr() must accept BOTH option-chain shapes this library
|
|
668
|
+
can hand it --
|
|
669
|
+
- the NESTED per-strike shape nse_optionchain_scrapper()/option_chain()
|
|
670
|
+
return: {'strikePrice','expiryDate','CE':{...},'PE':{...}}
|
|
671
|
+
- the FLAT per-contract-leg shape nse_quote_derivatives()/nse_quote()
|
|
672
|
+
(for derivatives) actually return now: each entry IS one leg
|
|
673
|
+
directly, with optionType=='CE'/'PE' and openInterest at the TOP
|
|
674
|
+
LEVEL -- there is no nested entry['CE']/entry['PE'] in this shape at
|
|
675
|
+
all.
|
|
676
|
+
Before this fix, pcr()'s aggregation loop only ever read entry['CE']/
|
|
677
|
+
entry['PE'], so feeding it the flat shape matched the target expiry
|
|
678
|
+
(found_data=True) but silently added 0 to both ce_oi/pe_oi every time,
|
|
679
|
+
returning a plausible-looking-but-wrong pcr of 0.0 instead of raising.
|
|
680
|
+
Returns (ce_oi_contribution, pe_oi_contribution) for one entry.
|
|
681
|
+
"""
|
|
682
|
+
if ('CE' in entry) or ('PE' in entry):
|
|
683
|
+
ce = entry['CE'].get('openInterest', 0) or 0 if entry.get('CE') else 0
|
|
684
|
+
pe = entry['PE'].get('openInterest', 0) or 0 if entry.get('PE') else 0
|
|
685
|
+
return ce, pe
|
|
686
|
+
if entry.get('optionType') == 'CE':
|
|
687
|
+
return entry.get('openInterest', 0) or 0, 0
|
|
688
|
+
if entry.get('optionType') == 'PE':
|
|
689
|
+
return 0, entry.get('openInterest', 0) or 0
|
|
690
|
+
return 0, 0
|
|
691
|
+
|
|
692
|
+
def pcr(payload, inp=0):
|
|
229
693
|
ce_oi = 0
|
|
230
694
|
pe_oi = 0
|
|
231
|
-
|
|
232
|
-
|
|
695
|
+
|
|
696
|
+
# Identify the data and expiry dates based on structure
|
|
697
|
+
if 'records' in payload:
|
|
698
|
+
# Legacy structure
|
|
699
|
+
data_list = payload['records']['data']
|
|
700
|
+
expiry_dates = payload['records']['expiryDates']
|
|
701
|
+
elif 'data' in payload:
|
|
702
|
+
# New structure (covers BOTH the nested per-strike shape and the
|
|
703
|
+
# flat per-contract-leg shape -- see _pcr_entry_oi() above)
|
|
704
|
+
data_list = payload['data']
|
|
705
|
+
# Extract unique sorted expiry dates from data
|
|
706
|
+
unique_dates = set()
|
|
707
|
+
for entry in data_list:
|
|
708
|
+
ed = entry.get('expiryDate') or entry.get('expiryDates')
|
|
709
|
+
if ed:
|
|
710
|
+
unique_dates.add(ed)
|
|
711
|
+
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"))
|
|
712
|
+
else:
|
|
713
|
+
# Round 3: a payload with neither 'records' nor 'data' isn't a
|
|
714
|
+
# recognizable option-chain/derivatives shape at all -- returning
|
|
715
|
+
# 0.0 here used to silently look like "zero put/call OI" instead of
|
|
716
|
+
# "this isn't option-chain data". Raise clearly instead.
|
|
717
|
+
raise NSEEndpointError(
|
|
718
|
+
"pcr(): payload has neither 'records' nor 'data' -- pass the "
|
|
719
|
+
"output of option_chain()/nse_optionchain_scrapper(), "
|
|
720
|
+
"nse_quote_derivatives(), or nse_quote() for a derivatives "
|
|
721
|
+
"symbol."
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
if not expiry_dates or inp >= len(expiry_dates):
|
|
725
|
+
# Requested index is outside the current payload's scope.
|
|
726
|
+
# Check if we can fetch more data for this specific symbol.
|
|
727
|
+
symbol = payload.get('symbol') or payload.get('records', {}).get('symbol')
|
|
728
|
+
if not symbol and 'data' in payload and len(payload['data']) > 0:
|
|
729
|
+
first = payload['data'][0]
|
|
730
|
+
symbol = (first.get('symbol') or first.get('underlying')
|
|
731
|
+
or (first.get('CE') and first['CE'].get('underlying'))
|
|
732
|
+
or (first.get('PE') and first['PE'].get('underlying')))
|
|
733
|
+
|
|
734
|
+
if symbol and inp > 0:
|
|
735
|
+
# Fetch all expiries to find the target one
|
|
736
|
+
all_expiries = expiry_list(symbol, type="list")
|
|
737
|
+
if inp < len(all_expiries):
|
|
738
|
+
target = all_expiries[inp]
|
|
739
|
+
# Fetch specific expiry data using getOptionChainData
|
|
740
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getOptionChainData&symbol={nsesymbolpurify(symbol)}¶ms=expiryDate={target}'
|
|
741
|
+
new_payload = nsefetch(url)
|
|
742
|
+
if new_payload and 'data' in new_payload:
|
|
743
|
+
for entry in new_payload['data']:
|
|
744
|
+
ce_oi += entry.get('CE', {}).get('openInterest', 0) if entry.get('CE') else 0
|
|
745
|
+
pe_oi += entry.get('PE', {}).get('openInterest', 0) if entry.get('PE') else 0
|
|
746
|
+
if ce_oi > 0: return pe_oi / ce_oi
|
|
747
|
+
return 0.0
|
|
748
|
+
|
|
749
|
+
target_expiry = expiry_dates[inp]
|
|
750
|
+
|
|
751
|
+
found_data = False
|
|
752
|
+
for i in data_list:
|
|
753
|
+
curr_exp = i.get('expiryDate') or i.get('expiryDates')
|
|
754
|
+
if curr_exp == target_expiry:
|
|
755
|
+
found_data = True
|
|
233
756
|
try:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
757
|
+
c, p = _pcr_entry_oi(i)
|
|
758
|
+
ce_oi += c
|
|
759
|
+
pe_oi += p
|
|
760
|
+
except (KeyError, TypeError):
|
|
237
761
|
pass
|
|
762
|
+
|
|
763
|
+
# If we didn't find any data for the target expiry in the payload,
|
|
764
|
+
# it means the payload was filtered (e.g. by the scrapper). Fetch it now.
|
|
765
|
+
if not found_data:
|
|
766
|
+
symbol = payload.get('symbol') or payload.get('records', {}).get('symbol')
|
|
767
|
+
if symbol:
|
|
768
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getOptionChainData&symbol={nsesymbolpurify(symbol)}¶ms=expiryDate={target_expiry}'
|
|
769
|
+
new_payload = nsefetch(url)
|
|
770
|
+
if new_payload and 'data' in new_payload:
|
|
771
|
+
for entry in new_payload['data']:
|
|
772
|
+
ce_oi += entry.get('CE', {}).get('openInterest', 0) if entry.get('CE') else 0
|
|
773
|
+
pe_oi += entry.get('PE', {}).get('openInterest', 0) if entry.get('PE') else 0
|
|
774
|
+
|
|
775
|
+
if ce_oi == 0:
|
|
776
|
+
return 0.0
|
|
777
|
+
|
|
238
778
|
return pe_oi / ce_oi
|
|
239
779
|
|
|
240
780
|
#forum.unofficed.com/t/unable-to-find-nse-quote-meta-api/702/4
|
|
241
781
|
#Refer https://forum.unofficed.com/t/changed-the-nse-quote-ltp-function/1276
|
|
242
782
|
def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
243
|
-
|
|
783
|
+
# Round 3 bug fix: this index-routing check was case-sensitive, so e.g.
|
|
784
|
+
# nse_quote_ltp("banknifty") (no optionType) missed the indices branch,
|
|
785
|
+
# fell through to the equity getSymbolData endpoint, and 404'd. Checking
|
|
786
|
+
# against symbol.upper() routes it correctly regardless of case.
|
|
787
|
+
if(optionType!="-"):
|
|
788
|
+
payload = nse_quote_derivatives(symbol)
|
|
789
|
+
else:
|
|
790
|
+
if any(x in symbol.upper() for x in indices):
|
|
791
|
+
payload = nse_quote_derivatives(symbol)
|
|
792
|
+
else:
|
|
793
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol.upper())
|
|
794
|
+
|
|
795
|
+
lastPrice = 0
|
|
796
|
+
|
|
797
|
+
if(optionType=="-"):
|
|
798
|
+
if 'equityResponse' in payload and len(payload['equityResponse']) > 0:
|
|
799
|
+
lastPrice = payload['equityResponse'][0]['orderBook']['lastPrice']
|
|
800
|
+
elif 'data' in payload and len(payload['data']) > 0:
|
|
801
|
+
# For indices, underlyingValue in derivative payload is the current index LTP
|
|
802
|
+
lastPrice = payload['data'][0].get('underlyingValue')
|
|
803
|
+
return lastPrice
|
|
244
804
|
|
|
245
805
|
meta = "Options"
|
|
246
806
|
if(optionType=="Fut"): meta = "Futures"
|
|
@@ -248,36 +808,68 @@ def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
|
248
808
|
if(optionType=="CE"):optionType="Call"
|
|
249
809
|
|
|
250
810
|
if(expiryDate=="latest") or (expiryDate=="next"):
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
811
|
+
i = 0 if expiryDate=="latest" else 1
|
|
812
|
+
expiry_dates = []
|
|
813
|
+
|
|
814
|
+
# Extract from new FNO payload structure
|
|
815
|
+
if 'data' in payload:
|
|
816
|
+
unique_dates = set()
|
|
817
|
+
for entry in payload['data']:
|
|
818
|
+
if 'expiryDate' in entry:
|
|
819
|
+
it = entry.get('instrumentType', '')
|
|
820
|
+
if (meta == "Futures" and "FUT" in it) or (meta == "Options" and "OPT" in it):
|
|
821
|
+
unique_dates.add(entry['expiryDate'])
|
|
822
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
823
|
+
|
|
824
|
+
# Filter future dates
|
|
825
|
+
future_expiry_dates = []
|
|
826
|
+
if expiry_dates:
|
|
827
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
828
|
+
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"))
|
|
829
|
+
|
|
830
|
+
# Fallback to expiry_list
|
|
831
|
+
if i >= len(future_expiry_dates):
|
|
832
|
+
dates = expiry_list(symbol, type="list")
|
|
833
|
+
if dates:
|
|
834
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
835
|
+
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"))
|
|
836
|
+
|
|
837
|
+
if i < len(future_expiry_dates):
|
|
838
|
+
expiryDate = future_expiry_dates[i]
|
|
262
839
|
|
|
263
840
|
|
|
264
841
|
if(optionType!="-"):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
842
|
+
data_list = payload.get('data', [])
|
|
843
|
+
for i in data_list:
|
|
844
|
+
# Check instrument type in identifier or metadata if present
|
|
845
|
+
if meta == "Futures":
|
|
846
|
+
is_match = "FUT" in i.get('instrumentType', '')
|
|
847
|
+
else:
|
|
848
|
+
is_match = "OPT" in i.get('instrumentType', '')
|
|
849
|
+
|
|
850
|
+
if is_match:
|
|
268
851
|
if(optionType=="Fut"):
|
|
269
|
-
if(i
|
|
270
|
-
lastPrice = i
|
|
852
|
+
if(i.get('expiryDate')==expiryDate):
|
|
853
|
+
lastPrice = i.get('lastPrice')
|
|
854
|
+
break
|
|
271
855
|
|
|
272
856
|
if((optionType=="Put")or(optionType=="Call")):
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
857
|
+
# Some APIs have optionType as 'PE'/'CE' or 'Put'/'Call'
|
|
858
|
+
p_opt_type = i.get('optionType')
|
|
859
|
+
if p_opt_type == "PE": p_opt_type = "Put"
|
|
860
|
+
if p_opt_type == "CE": p_opt_type = "Call"
|
|
861
|
+
|
|
862
|
+
if (i.get("expiryDate")==expiryDate):
|
|
863
|
+
if (p_opt_type==optionType):
|
|
864
|
+
# strikePrice in payload is often string with padding
|
|
865
|
+
try:
|
|
866
|
+
p_strike = float(str(i.get("strikePrice")).strip())
|
|
867
|
+
except:
|
|
868
|
+
p_strike = 0
|
|
869
|
+
|
|
870
|
+
if (p_strike==float(strikePrice)):
|
|
871
|
+
lastPrice = i.get('lastPrice')
|
|
872
|
+
break
|
|
281
873
|
|
|
282
874
|
return lastPrice
|
|
283
875
|
|
|
@@ -292,58 +884,212 @@ def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
|
292
884
|
# print(nse_quote_ltp("RELIANCE","next","PE",2300))
|
|
293
885
|
|
|
294
886
|
def nse_quote_meta(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
887
|
+
# Round 3 bug fix: case-sensitive index routing (see nse_quote_ltp()).
|
|
888
|
+
if(optionType!="-"):
|
|
889
|
+
payload = nse_quote_derivatives(symbol)
|
|
890
|
+
else:
|
|
891
|
+
if any(x in symbol.upper() for x in indices):
|
|
892
|
+
payload = nse_quote_derivatives(symbol)
|
|
893
|
+
else:
|
|
894
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol.upper())
|
|
298
895
|
|
|
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]
|
|
896
|
+
metadata = {}
|
|
305
897
|
|
|
306
|
-
if(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
898
|
+
if(optionType=="-"):
|
|
899
|
+
if 'equityResponse' in payload and len(payload['equityResponse']) > 0:
|
|
900
|
+
metadata = payload['equityResponse'][0].get('metaData', {})
|
|
901
|
+
elif 'data' in payload and len(payload['data']) > 0:
|
|
902
|
+
# Round 3 fix (known gap #2): index/derivative underlyings go
|
|
903
|
+
# through nse_quote_derivatives()'s flat per-contract-leg shape,
|
|
904
|
+
# which has no 'equityResponse'/'metaData' at all -- this used to
|
|
905
|
+
# silently fall through to the {} default, making every index
|
|
906
|
+
# symbol look like "no data" instead of "wrong shape for this
|
|
907
|
+
# accessor". There is no equity-style open/high/low/close
|
|
908
|
+
# snapshot anywhere in this payload for an index (only
|
|
909
|
+
# underlyingValue/underlying/timestamp per leg), so we return the
|
|
910
|
+
# real fields that DO exist instead of fabricating the rest.
|
|
911
|
+
first = payload['data'][0]
|
|
912
|
+
metadata = {
|
|
913
|
+
"symbol": first.get('underlying') or symbol.upper(),
|
|
914
|
+
"underlyingValue": first.get('underlyingValue'),
|
|
915
|
+
"timestamp": payload.get('timestamp'),
|
|
916
|
+
}
|
|
917
|
+
return metadata
|
|
311
918
|
|
|
312
919
|
meta = "Options"
|
|
313
920
|
if(optionType=="Fut"): meta = "Futures"
|
|
314
921
|
if(optionType=="PE"):optionType="Put"
|
|
315
922
|
if(optionType=="CE"):optionType="Call"
|
|
316
923
|
|
|
924
|
+
if(expiryDate=="latest") or (expiryDate=="next"):
|
|
925
|
+
i = 0 if expiryDate=="latest" else 1
|
|
926
|
+
expiry_dates = []
|
|
927
|
+
if 'data' in payload:
|
|
928
|
+
unique_dates = set()
|
|
929
|
+
for entry in payload['data']:
|
|
930
|
+
if 'expiryDate' in entry:
|
|
931
|
+
it = entry.get('instrumentType', '')
|
|
932
|
+
if (meta == "Futures" and "FUT" in it) or (meta == "Options" and "OPT" in it):
|
|
933
|
+
unique_dates.add(entry['expiryDate'])
|
|
934
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
935
|
+
|
|
936
|
+
future_expiry_dates = []
|
|
937
|
+
if expiry_dates:
|
|
938
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
939
|
+
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"))
|
|
940
|
+
|
|
941
|
+
if i >= len(future_expiry_dates):
|
|
942
|
+
dates = expiry_list(symbol, type="list")
|
|
943
|
+
if dates:
|
|
944
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
945
|
+
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"))
|
|
946
|
+
|
|
947
|
+
if i < len(future_expiry_dates):
|
|
948
|
+
expiryDate = future_expiry_dates[i]
|
|
949
|
+
|
|
950
|
+
# print(f"DEBUG: Calculated expiryDate={expiryDate}, meta={meta}, optionType={optionType}")
|
|
951
|
+
|
|
317
952
|
if(optionType!="-"):
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
953
|
+
data_list = payload.get('data', [])
|
|
954
|
+
# print(f"DEBUG: Searching in {len(data_list)} items")
|
|
955
|
+
for i in data_list:
|
|
956
|
+
if meta == "Futures":
|
|
957
|
+
is_match = "FUT" in i.get('instrumentType', '')
|
|
958
|
+
else:
|
|
959
|
+
is_match = "OPT" in i.get('instrumentType', '')
|
|
960
|
+
|
|
961
|
+
if is_match:
|
|
321
962
|
if(optionType=="Fut"):
|
|
322
|
-
if(i
|
|
323
|
-
metadata = i
|
|
963
|
+
if(i.get('expiryDate')==expiryDate):
|
|
964
|
+
metadata = i
|
|
965
|
+
break
|
|
324
966
|
|
|
325
967
|
if((optionType=="Put")or(optionType=="Call")):
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
968
|
+
p_opt_type = i.get('optionType')
|
|
969
|
+
if p_opt_type == "PE": p_opt_type = "Put"
|
|
970
|
+
if p_opt_type == "CE": p_opt_type = "Call"
|
|
971
|
+
|
|
972
|
+
if (i.get("expiryDate")==expiryDate):
|
|
973
|
+
if (p_opt_type==optionType):
|
|
974
|
+
try:
|
|
975
|
+
p_strike = float(str(i.get("strikePrice")).strip())
|
|
976
|
+
except:
|
|
977
|
+
p_strike = 0
|
|
978
|
+
|
|
979
|
+
if (p_strike==float(strikePrice)):
|
|
980
|
+
metadata = i
|
|
981
|
+
break
|
|
334
982
|
|
|
335
983
|
return metadata
|
|
336
984
|
|
|
337
985
|
def nse_optionchain_ltp(payload,strikePrice,optionType,inp=0,intent=""):
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
986
|
+
# Round 3 bug fix (new finding, highest severity found this round): this
|
|
987
|
+
# function unconditionally indexed payload['records'] -- the pre-rewrite
|
|
988
|
+
# legacy NSE shape. option_chain()/nse_optionchain_scrapper() (this
|
|
989
|
+
# library's OWN current option-chain source, since round 1) return
|
|
990
|
+
# {'data': [...], 'timestamp': ...} instead -- 'records' doesn't exist
|
|
991
|
+
# anywhere in the live code path any more, so this function could never
|
|
992
|
+
# succeed with real data produced by this library: every call crashed
|
|
993
|
+
# with KeyError('records'), unconditionally.
|
|
994
|
+
if 'records' in payload:
|
|
995
|
+
# Legacy shape, kept for any caller handing in an old-style cached
|
|
996
|
+
# payload captured before this library's rewrite.
|
|
997
|
+
expiry_dates = payload['records']['expiryDates']
|
|
998
|
+
expiry_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
999
|
+
expiry_dates = [date.strftime("%d-%b-%Y") for date in expiry_dates if date >= datetime.datetime.now().date()]
|
|
1000
|
+
if inp >= len(expiry_dates):
|
|
1001
|
+
raise NSEEndpointError(
|
|
1002
|
+
f"nse_optionchain_ltp(): requested expiry index {inp} is out "
|
|
1003
|
+
f"of range -- only {len(expiry_dates)} future expiries found."
|
|
1004
|
+
)
|
|
1005
|
+
expiryDate = expiry_dates[inp]
|
|
1006
|
+
for x in range(len(payload['records']['data'])):
|
|
1007
|
+
row = payload['records']['data'][x]
|
|
1008
|
+
if (row['strikePrice'] == strikePrice) and (row['expiryDate'] == expiryDate):
|
|
1009
|
+
leg = row[optionType]
|
|
1010
|
+
if(intent==""): return leg['lastPrice']
|
|
1011
|
+
if(intent=="sell"): return leg['bidprice']
|
|
1012
|
+
if(intent=="buy"): return leg['askPrice']
|
|
1013
|
+
return None
|
|
1014
|
+
|
|
1015
|
+
if 'data' in payload:
|
|
1016
|
+
# Current shape: option_chain()/nse_optionchain_scrapper()'s flat
|
|
1017
|
+
# 'data' list, each entry already grouped per-strike with 'CE'/'PE'
|
|
1018
|
+
# sub-dicts (see nse_optionchain_scrapper()'s combine step) --
|
|
1019
|
+
# strikePrice/expiryDate live on the outer entry, the price fields
|
|
1020
|
+
# live inside entry[optionType].
|
|
1021
|
+
data_list = payload['data']
|
|
1022
|
+
|
|
1023
|
+
def _parse_exp(d):
|
|
1024
|
+
try:
|
|
1025
|
+
if "-" in d and d.split("-")[1].isdigit():
|
|
1026
|
+
return datetime.datetime.strptime(d, "%d-%m-%Y").date()
|
|
1027
|
+
return datetime.datetime.strptime(d, "%d-%b-%Y").date()
|
|
1028
|
+
except Exception:
|
|
1029
|
+
return None
|
|
1030
|
+
|
|
1031
|
+
unique_dates = sorted(
|
|
1032
|
+
{e.get('expiryDate') for e in data_list if e.get('expiryDate')},
|
|
1033
|
+
key=lambda d: _parse_exp(d) or datetime.date.max,
|
|
1034
|
+
)
|
|
1035
|
+
today = datetime.datetime.now().date()
|
|
1036
|
+
future_dates = [d for d in unique_dates if (_parse_exp(d) or today) >= today]
|
|
1037
|
+
if inp >= len(future_dates):
|
|
1038
|
+
raise NSEEndpointError(
|
|
1039
|
+
f"nse_optionchain_ltp(): requested expiry index {inp} is out "
|
|
1040
|
+
f"of range -- only {len(future_dates)} future expiries found "
|
|
1041
|
+
f"in this payload."
|
|
1042
|
+
)
|
|
1043
|
+
expiryDate = future_dates[inp]
|
|
1044
|
+
|
|
1045
|
+
try:
|
|
1046
|
+
target_strike = float(str(strikePrice).strip())
|
|
1047
|
+
except Exception:
|
|
1048
|
+
target_strike = None
|
|
1049
|
+
|
|
1050
|
+
for entry in data_list:
|
|
1051
|
+
if entry.get('expiryDate') != expiryDate:
|
|
1052
|
+
continue
|
|
1053
|
+
try:
|
|
1054
|
+
entry_strike = float(str(entry.get('strikePrice')).strip())
|
|
1055
|
+
except Exception:
|
|
1056
|
+
continue
|
|
1057
|
+
if target_strike is not None and entry_strike != target_strike:
|
|
1058
|
+
continue
|
|
1059
|
+
if 'optionType' in entry and 'CE' not in entry and 'PE' not in entry:
|
|
1060
|
+
# Flat per-leg shape (nse_quote_derivatives()/nse_quote()'s
|
|
1061
|
+
# getSymbolDerivativesData output): each list entry IS one
|
|
1062
|
+
# CE or PE leg directly (entry['optionType'] == 'CE'/'PE',
|
|
1063
|
+
# price fields on the entry itself) rather than one entry
|
|
1064
|
+
# per strike holding both legs nested under entry['CE']/
|
|
1065
|
+
# entry['PE']. Round-3 bug (confirmed live, fixed here):
|
|
1066
|
+
# entry.get(optionType) always returned None for this shape
|
|
1067
|
+
# since there's no such nested key on a flat leg.
|
|
1068
|
+
if entry.get('optionType') != optionType:
|
|
1069
|
+
continue
|
|
1070
|
+
leg = entry
|
|
1071
|
+
else:
|
|
1072
|
+
leg = entry.get(optionType)
|
|
1073
|
+
if not leg:
|
|
1074
|
+
continue
|
|
1075
|
+
if intent == "":
|
|
1076
|
+
return leg.get('lastPrice')
|
|
1077
|
+
if intent == "sell":
|
|
1078
|
+
# The live getSymbolDerivativesData-backed payload carries
|
|
1079
|
+
# no bid/ask order-book fields at all (confirmed live) --
|
|
1080
|
+
# only the legacy 'records' shape had bidprice/askPrice.
|
|
1081
|
+
# This is a genuine data-availability gap, not a lookup
|
|
1082
|
+
# bug: returns None rather than guessing a price.
|
|
1083
|
+
return leg.get('buyPrice1', leg.get('bidprice'))
|
|
1084
|
+
if intent == "buy":
|
|
1085
|
+
return leg.get('sellPrice1', leg.get('askPrice'))
|
|
1086
|
+
return None
|
|
1087
|
+
|
|
1088
|
+
raise NSEEndpointError(
|
|
1089
|
+
"nse_optionchain_ltp(): payload has neither 'records' nor 'data' -- "
|
|
1090
|
+
"pass the output of option_chain()/nse_optionchain_scrapper() "
|
|
1091
|
+
"directly."
|
|
1092
|
+
)
|
|
347
1093
|
|
|
348
1094
|
def nse_eq(symbol):
|
|
349
1095
|
symbol = nsesymbolpurify(symbol)
|
|
@@ -355,8 +1101,20 @@ def nse_eq(symbol):
|
|
|
355
1101
|
payload = nsefetch('https://www.nseindia.com/api/quote-derivative?symbol='+symbol)
|
|
356
1102
|
except:
|
|
357
1103
|
pass
|
|
358
|
-
except KeyError:
|
|
359
|
-
|
|
1104
|
+
except (KeyError, NSEEndpointError):
|
|
1105
|
+
# /api/quote-equity is retired on the live site (confirmed HTTP 403,
|
|
1106
|
+
# Akamai/WAF "Access Denied" page, as of 2026) with no indication it
|
|
1107
|
+
# is coming back. The newer NextApi GetQuoteApi endpoint carries the
|
|
1108
|
+
# same underlying data (just in a different JSON shape - data lives
|
|
1109
|
+
# under payload['equityResponse'][0] instead of payload['priceInfo']/
|
|
1110
|
+
# payload['info']) so we fall back to that instead of returning {}.
|
|
1111
|
+
logging.warning(
|
|
1112
|
+
"nse_eq(%s): /api/quote-equity is retired; returning data from "
|
|
1113
|
+
"the newer NextApi quote endpoint instead (see nse_quote() - the "
|
|
1114
|
+
"JSON shape differs from the old quote-equity response).",
|
|
1115
|
+
symbol,
|
|
1116
|
+
)
|
|
1117
|
+
payload = nse_quote(symbol)
|
|
360
1118
|
return payload
|
|
361
1119
|
|
|
362
1120
|
|
|
@@ -370,8 +1128,18 @@ def nse_fno(symbol):
|
|
|
370
1128
|
payload = nsefetch('https://www.nseindia.com/api/quote-equity?symbol='+symbol)
|
|
371
1129
|
except KeyError:
|
|
372
1130
|
pass
|
|
373
|
-
except KeyError:
|
|
374
|
-
|
|
1131
|
+
except (KeyError, NSEEndpointError):
|
|
1132
|
+
# /api/quote-derivative is likewise retired (confirmed HTTP 404 on
|
|
1133
|
+
# the live site). getSymbolDerivativesData via nse_quote_derivatives()
|
|
1134
|
+
# is the working replacement (different JSON shape: a flat 'data'
|
|
1135
|
+
# list of per-strike CE/PE records instead of records/underlyingValue).
|
|
1136
|
+
logging.warning(
|
|
1137
|
+
"nse_fno(%s): /api/quote-derivative is retired; returning data "
|
|
1138
|
+
"from the newer NextApi derivatives endpoint instead (see "
|
|
1139
|
+
"nse_quote_derivatives() - the JSON shape differs).",
|
|
1140
|
+
symbol,
|
|
1141
|
+
)
|
|
1142
|
+
payload = nse_quote_derivatives(symbol)
|
|
375
1143
|
return payload
|
|
376
1144
|
|
|
377
1145
|
def quote_equity(symbol):
|
|
@@ -384,10 +1152,22 @@ def option_chain(symbol):
|
|
|
384
1152
|
return nse_optionchain_scrapper(symbol)
|
|
385
1153
|
|
|
386
1154
|
def nse_holidays(type="trading"):
|
|
1155
|
+
# Round 3 bug fix: these were two independent `if`s with no `else`, so
|
|
1156
|
+
# any type other than exactly "trading"/"clearing" left `payload` never
|
|
1157
|
+
# assigned, and `return payload` blew up with an unrelated-looking
|
|
1158
|
+
# UnboundLocalError instead of a clear "invalid type" message. Confirmed
|
|
1159
|
+
# live that NSE's own /api/holiday-master endpoint only accepts these
|
|
1160
|
+
# two type values (anything else comes back HTTP 200 with a zero-length
|
|
1161
|
+
# body) -- so raise a clear, descriptive error for anything else.
|
|
387
1162
|
if(type=="clearing"):
|
|
388
1163
|
payload = nsefetch('https://www.nseindia.com/api/holiday-master?type=clearing')
|
|
389
|
-
|
|
1164
|
+
elif(type=="trading"):
|
|
390
1165
|
payload = nsefetch('https://www.nseindia.com/api/holiday-master?type=trading')
|
|
1166
|
+
else:
|
|
1167
|
+
raise ValueError(
|
|
1168
|
+
f"nse_holidays: invalid type={type!r} -- NSE's holiday-master "
|
|
1169
|
+
f"API only supports type='trading' or type='clearing'."
|
|
1170
|
+
)
|
|
391
1171
|
return payload
|
|
392
1172
|
|
|
393
1173
|
def holiday_master(type="trading"):
|
|
@@ -411,19 +1191,31 @@ def nse_past_results(symbol):
|
|
|
411
1191
|
symbol = nsesymbolpurify(symbol)
|
|
412
1192
|
return nsefetch('https://www.nseindia.com/api/results-comparision?symbol='+symbol)
|
|
413
1193
|
|
|
414
|
-
def expiry_list(symbol,type="
|
|
415
|
-
logging.info("Getting Expiry List of: "+ symbol)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
1194
|
+
def expiry_list(symbol, type=""):
|
|
1195
|
+
logging.info("Getting Expiry List of: " + symbol)
|
|
1196
|
+
symbol = nsesymbolpurify(symbol)
|
|
1197
|
+
url = f'https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getOptionChainDropdown&symbol={symbol}'
|
|
1198
|
+
payload = nsefetch(url)
|
|
1199
|
+
|
|
1200
|
+
if not payload or 'expiryDates' not in payload:
|
|
1201
|
+
return [] if type == "list" else pd.DataFrame()
|
|
421
1202
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1203
|
+
expiry_dates = payload['expiryDates']
|
|
1204
|
+
|
|
1205
|
+
# Format dates from DD-MM-YYYY to DD-Mon-YYYY
|
|
1206
|
+
formatted_dates = []
|
|
1207
|
+
for d in expiry_dates:
|
|
1208
|
+
try:
|
|
1209
|
+
dt = datetime.datetime.strptime(d, "%d-%m-%Y")
|
|
1210
|
+
formatted_dates.append(dt.strftime("%d-%b-%Y"))
|
|
1211
|
+
except:
|
|
1212
|
+
formatted_dates.append(d)
|
|
1213
|
+
|
|
1214
|
+
if type == "list":
|
|
1215
|
+
return formatted_dates
|
|
1216
|
+
else:
|
|
1217
|
+
# If anything other than "list" is provided (like "df", "pandas", or default), return DataFrame
|
|
1218
|
+
return pd.DataFrame({'Date': formatted_dates})
|
|
427
1219
|
|
|
428
1220
|
|
|
429
1221
|
def nse_custom_function_secfno(symbol,attribute="lastPrice"):
|
|
@@ -442,8 +1234,22 @@ def nse_marketStatus():
|
|
|
442
1234
|
return payload
|
|
443
1235
|
|
|
444
1236
|
def nse_circular(mode="latest"):
|
|
1237
|
+
# The old mode="latest" path (https://nseindia.com/api/latest-circular,
|
|
1238
|
+
# no `www.`) is dead on the live site: it returns HTTP 200 but a bare
|
|
1239
|
+
# {'error': True, 'status': 500} JSON body -- confirmed this is NOT an
|
|
1240
|
+
# Akamai bot-challenge (no injected script, no 403/503), just NSE's own
|
|
1241
|
+
# "this route doesn't exist" response. NSE renamed the circulars page
|
|
1242
|
+
# itself from /resources/circulars to
|
|
1243
|
+
# /resources/exchange-communication-circulars, and a Playwright network
|
|
1244
|
+
# capture on that live page shows it calling
|
|
1245
|
+
# https://www.nseindia.com/api/circulars?fromDate=DD-MM-YYYY&toDate=DD-MM-YYYY
|
|
1246
|
+
# (with `www.`) -- the SAME URL this function's own mode!="latest"
|
|
1247
|
+
# branch already used and which was independently confirmed live
|
|
1248
|
+
# (zero params defaults to NSE's own last-7-days/150-record window).
|
|
1249
|
+
# Fix: route "latest" to that same working endpoint too, instead of the
|
|
1250
|
+
# dead no-www path.
|
|
445
1251
|
if(mode=="latest"):
|
|
446
|
-
payload = nsefetch('https://nseindia.com/api/
|
|
1252
|
+
payload = nsefetch('https://www.nseindia.com/api/circulars')
|
|
447
1253
|
else:
|
|
448
1254
|
payload = nsefetch('https://www.nseindia.com/api/circulars')
|
|
449
1255
|
return payload
|
|
@@ -465,21 +1271,30 @@ def nsetools_get_quote(symbol):
|
|
|
465
1271
|
return payload['data'][m]
|
|
466
1272
|
|
|
467
1273
|
|
|
1274
|
+
def _nse_index_data():
|
|
1275
|
+
# iislliveblob.niftyindices.com is a dead host (confirmed live: NXDOMAIN,
|
|
1276
|
+
# twice). /api/allIndices on the main site carries the same live index
|
|
1277
|
+
# quotes (139 indices as of this writing, including pe/pb/dy per index).
|
|
1278
|
+
# Its per-row key is 'index' (e.g. "NIFTY 50"), not the old 'indexName' --
|
|
1279
|
+
# alias it so nse_get_index_list()/nse_get_index_quote() below (and any
|
|
1280
|
+
# external code doing the same lookup) keep working unchanged.
|
|
1281
|
+
payload = nsefetch("https://www.nseindia.com/api/allIndices")
|
|
1282
|
+
rows = payload.get("data", [])
|
|
1283
|
+
for row in rows:
|
|
1284
|
+
row.setdefault("indexName", row.get("index"))
|
|
1285
|
+
return rows
|
|
1286
|
+
|
|
1287
|
+
|
|
468
1288
|
def nse_index():
|
|
469
|
-
|
|
470
|
-
payload = pd.DataFrame(payload["data"])
|
|
471
|
-
return payload
|
|
1289
|
+
return pd.DataFrame(_nse_index_data())
|
|
472
1290
|
|
|
473
1291
|
def nse_get_index_list():
|
|
474
|
-
|
|
475
|
-
payload = pd.DataFrame(payload["data"])
|
|
476
|
-
return payload["indexName"].tolist()
|
|
1292
|
+
return pd.DataFrame(_nse_index_data())["indexName"].tolist()
|
|
477
1293
|
|
|
478
1294
|
def nse_get_index_quote(index):
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return payload['data'][m]
|
|
1295
|
+
for row in _nse_index_data():
|
|
1296
|
+
if row["indexName"] == index.upper():
|
|
1297
|
+
return row
|
|
483
1298
|
|
|
484
1299
|
def nse_get_advances_declines(mode="pandas"):
|
|
485
1300
|
try:
|
|
@@ -505,10 +1320,19 @@ def nse_get_top_gainers():
|
|
|
505
1320
|
return df.head(5)
|
|
506
1321
|
|
|
507
1322
|
def nse_get_fno_lot_sizes(symbol="all",mode="list"):
|
|
508
|
-
|
|
1323
|
+
# github.com/aeron7/nsepythonserver issue #4 ("lot sizes not working"):
|
|
1324
|
+
# two stacked bugs, confirmed live. (1) archives.nseindia.com silently
|
|
1325
|
+
# redirects this specific file to an unrelated PDF circular these days
|
|
1326
|
+
# (NSE's archives -> nsearchives host migration left a stale redirect on
|
|
1327
|
+
# just this path) -- nsearchives.nseindia.com/content/fo/fo_mktlots.csv
|
|
1328
|
+
# is the real, current location, confirmed live with the exact same CSV
|
|
1329
|
+
# shape. (2) plain `requests.get()` against nsearchives.nseindia.com
|
|
1330
|
+
# hangs to a read-timeout (confirmed live) -- it needs the same
|
|
1331
|
+
# curl_cffi TLS impersonation as the rest of the site now.
|
|
1332
|
+
url="https://nsearchives.nseindia.com/content/fo/fo_mktlots.csv"
|
|
509
1333
|
|
|
510
1334
|
if(mode=="list"):
|
|
511
|
-
s=
|
|
1335
|
+
s = _nse_fetch_csv_text(url)
|
|
512
1336
|
res_dict = {}
|
|
513
1337
|
for line in s.split('\n'):
|
|
514
1338
|
if line != '' and re.search(',', line) and (line.casefold().find('symbol') == -1):
|
|
@@ -520,7 +1344,7 @@ def nse_get_fno_lot_sizes(symbol="all",mode="list"):
|
|
|
520
1344
|
return res_dict[symbol.upper()]
|
|
521
1345
|
|
|
522
1346
|
if(mode=="pandas"):
|
|
523
|
-
payload = pd.read_csv(url)
|
|
1347
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(url)))
|
|
524
1348
|
if(symbol=="all"):
|
|
525
1349
|
return payload
|
|
526
1350
|
else:
|
|
@@ -552,6 +1376,22 @@ def black_scholes_dexter(S0,X,t,σ="",r=10,q=0.0,td=365):
|
|
|
552
1376
|
S0,X,σ,r,q,t = float(S0),float(X),float(σ/100),float(r/100),float(q/100),float(t/td)
|
|
553
1377
|
#https://unofficed.com/black-scholes-model-options-calculator-google-sheet/
|
|
554
1378
|
|
|
1379
|
+
# Round 3 bug fix: t=0 (an option literally expiring today, a completely
|
|
1380
|
+
# normal real-world input given NSE's weekly expiries) used to raise a
|
|
1381
|
+
# raw, uncaught ZeroDivisionError from sigma*sqrt(t) in d1's denominator.
|
|
1382
|
+
# This is a genuine math-domain limit of the Black-Scholes formula (it's
|
|
1383
|
+
# undefined at t=0), not an NSE-API issue -- so raise a clear, descriptive
|
|
1384
|
+
# error pointing the caller at intrinsic value instead of a bare
|
|
1385
|
+
# ZeroDivisionError.
|
|
1386
|
+
if t <= 0:
|
|
1387
|
+
raise ValueError(
|
|
1388
|
+
f"black_scholes_dexter: t={t*td:g} days to expiry must be > 0 -- "
|
|
1389
|
+
f"Black-Scholes delta/gamma/theta/vega are undefined at t=0 (an "
|
|
1390
|
+
f"option expiring today). Use intrinsic value "
|
|
1391
|
+
f"(max(S0-X,0) for a call / max(X-S0,0) for a put) directly "
|
|
1392
|
+
f"instead for a same-day expiry."
|
|
1393
|
+
)
|
|
1394
|
+
|
|
555
1395
|
d1 = (math.log(S0/X)+(r-q+0.5*σ**2)*t)/(σ*math.sqrt(t))
|
|
556
1396
|
#stackoverflow.com/questions/34258537/python-typeerror-unsupported-operand-types-for-float-and-int
|
|
557
1397
|
|
|
@@ -574,7 +1414,13 @@ def black_scholes_dexter(S0,X,t,σ="",r=10,q=0.0,td=365):
|
|
|
574
1414
|
|
|
575
1415
|
def equity_history_virgin(symbol,series,start_date,end_date):
|
|
576
1416
|
#url="https://www.nseindia.com/api/historical/cm/equity?symbol="+symbol+"&series=[%22"+series+"%22]&from="+str(start_date)+"&to="+str(end_date)+""
|
|
577
|
-
|
|
1417
|
+
# NOTE: the original /api/historical/cm/equity route is retired on the
|
|
1418
|
+
# live site (confirmed HTTP 503 as of 2026, even via curl_cffi). NSE's
|
|
1419
|
+
# replacement is /api/historicalOR/cm/equity -- same query params, same
|
|
1420
|
+
# response shape (payload['data'] records with CH_TIMESTAMP/
|
|
1421
|
+
# CH_CLOSING_PRICE/etc), confirmed live, so this is a plain host-path
|
|
1422
|
+
# swap with no downstream parsing changes needed.
|
|
1423
|
+
url = 'https://www.nseindia.com/api/historicalOR/cm/equity?symbol=' + symbol + '&series=["' + series + '"]&from=' + start_date + '&to=' + end_date
|
|
578
1424
|
|
|
579
1425
|
payload = nsefetch(url)
|
|
580
1426
|
return pd.DataFrame.from_records(payload["data"])
|
|
@@ -654,7 +1500,10 @@ def derivative_history_virgin(symbol,start_date,end_date,instrumentType,expiry_d
|
|
|
654
1500
|
strikePrice = "%.2f" % strikePrice
|
|
655
1501
|
strikePrice = str(strikePrice)
|
|
656
1502
|
|
|
657
|
-
|
|
1503
|
+
# /api/historical/fo/derivatives is retired (HTTP 503 live); the
|
|
1504
|
+
# confirmed-working replacement is /api/historicalOR/fo/derivatives with
|
|
1505
|
+
# the same query params and response shape.
|
|
1506
|
+
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
1507
|
payload = nsefetch(nsefetch_url)
|
|
659
1508
|
logging.info(nsefetch_url)
|
|
660
1509
|
logging.info(payload)
|
|
@@ -719,12 +1568,15 @@ def derivative_history(symbol,start_date,end_date,instrumentType,expiry_date,str
|
|
|
719
1568
|
|
|
720
1569
|
|
|
721
1570
|
def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
722
|
-
|
|
723
|
-
|
|
1571
|
+
# Same retirement as derivative_history_virgin()/equity_history_virgin()
|
|
1572
|
+
# above -- /api/historical/* is gone, /api/historicalOR/* is the working
|
|
1573
|
+
# replacement with an identical response shape.
|
|
1574
|
+
nsefetch_url = "https://www.nseindia.com/api/historicalOR/fo/derivatives/meta?&from="+start_date+"&to="+end_date+"&symbol="+symbol+""
|
|
724
1575
|
payload = nsefetch(nsefetch_url)
|
|
725
1576
|
|
|
726
1577
|
#print(payload)
|
|
727
1578
|
|
|
1579
|
+
payload_data = None
|
|
728
1580
|
for key, value in payload['expiryDatesByInstrument'].items():
|
|
729
1581
|
if type.lower() == "options" and "OPT" in key:
|
|
730
1582
|
payload_data = payload['expiryDatesByInstrument'][key]
|
|
@@ -732,7 +1584,20 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
732
1584
|
elif type.lower() == "futures" and "FUT" in key:
|
|
733
1585
|
payload_data = payload['expiryDatesByInstrument'][key]
|
|
734
1586
|
break
|
|
735
|
-
|
|
1587
|
+
|
|
1588
|
+
if payload_data is None:
|
|
1589
|
+
return []
|
|
1590
|
+
|
|
1591
|
+
# Round 3 bug fix: calling this with its own documented defaults (no
|
|
1592
|
+
# dates -- expiry_history("NIFTY")) used to crash unconditionally with
|
|
1593
|
+
# `ValueError: time data '' does not match format '%d-%m-%Y'`, because
|
|
1594
|
+
# start_date/end_date default to "" but got passed straight into
|
|
1595
|
+
# strptime with no blank-check. Confirmed live that the endpoint itself
|
|
1596
|
+
# already handles blank from/to by returning the full unfiltered expiry
|
|
1597
|
+
# list -- so short-circuit and return that directly instead of crashing.
|
|
1598
|
+
if start_date == "" or end_date == "":
|
|
1599
|
+
return payload_data
|
|
1600
|
+
|
|
736
1601
|
# Convert start_date and end_date to datetime objects
|
|
737
1602
|
start_date = datetime.datetime.strptime(start_date, "%d-%m-%Y")
|
|
738
1603
|
end_date = datetime.datetime.strptime(end_date, "%d-%m-%Y")
|
|
@@ -741,7 +1606,7 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
741
1606
|
filtered_date_payload = []
|
|
742
1607
|
|
|
743
1608
|
# Initialize a flag to check if the first date after end_date has been added
|
|
744
|
-
added_after_end_date = False
|
|
1609
|
+
added_after_end_date = False
|
|
745
1610
|
|
|
746
1611
|
# Iterate through date_payload and filter dates within the range
|
|
747
1612
|
for date_str in payload_data:
|
|
@@ -751,62 +1616,163 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
751
1616
|
elif date_obj > end_date and not added_after_end_date:
|
|
752
1617
|
filtered_date_payload.append(date_str)
|
|
753
1618
|
added_after_end_date = True
|
|
754
|
-
|
|
1619
|
+
|
|
755
1620
|
return filtered_date_payload
|
|
756
1621
|
|
|
757
1622
|
# # Nifty Indicies Site
|
|
1623
|
+
#
|
|
1624
|
+
# niftyindices.com is a completely separate host/site from nseindia.com (no
|
|
1625
|
+
# Akamai Bot Manager symptoms observed here) -- but it was fully redesigned
|
|
1626
|
+
# onto a different CMS at some point: the old ASP.NET WebMethods under
|
|
1627
|
+
# `niftyindices.com/Backpage.aspx/*` (returning `{"d": "<json string>"}`) are
|
|
1628
|
+
# gone, and POSTing to them now just returns the site's homepage HTML, which
|
|
1629
|
+
# is exactly github.com/aeron7/nsepython issue #78's
|
|
1630
|
+
# `JSONDecodeError: Expecting value: line 1 column 2 (char 1)`.
|
|
1631
|
+
#
|
|
1632
|
+
# The working replacement (confirmed live) is `www.niftyindices.com/BackPage/*`
|
|
1633
|
+
# (note: `www.` + `BackPage` not `Backpage.aspx`), which wants a short session
|
|
1634
|
+
# warm-up first (visiting the historical-data report page) and returns a
|
|
1635
|
+
# direct JSON array rather than the old `{"d": "..."}` wrapper.
|
|
758
1636
|
|
|
759
1637
|
niftyindices_headers = {
|
|
760
|
-
'Connection': 'keep-alive',
|
|
761
|
-
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
|
|
762
1638
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
|
763
|
-
'
|
|
1639
|
+
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
|
|
1640
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
1641
|
+
'Origin': 'https://www.niftyindices.com',
|
|
1642
|
+
'Referer': 'https://www.niftyindices.com/reports/historical-data',
|
|
1643
|
+
'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
1644
|
'X-Requested-With': 'XMLHttpRequest',
|
|
1645
|
+
'sec-ch-ua': '"Not;A=Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
|
|
765
1646
|
'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',
|
|
1647
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
774
1648
|
}
|
|
775
1649
|
|
|
776
|
-
|
|
1650
|
+
_niftyindices_session = None
|
|
1651
|
+
_niftyindices_warmed = False
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def _get_niftyindices_session():
|
|
1655
|
+
global _niftyindices_session, _niftyindices_warmed
|
|
1656
|
+
if _niftyindices_session is None:
|
|
1657
|
+
_niftyindices_session = requests.Session()
|
|
1658
|
+
if not _niftyindices_warmed:
|
|
1659
|
+
try:
|
|
1660
|
+
_niftyindices_session.get(
|
|
1661
|
+
"https://www.niftyindices.com/reports/historical-data",
|
|
1662
|
+
headers=niftyindices_headers, timeout=15,
|
|
1663
|
+
)
|
|
1664
|
+
_niftyindices_warmed = True
|
|
1665
|
+
except Exception as e:
|
|
1666
|
+
logging.warning("niftyindices.com session warm-up failed/partial: %s", e)
|
|
1667
|
+
return _niftyindices_session
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def _niftyindices_fetch(endpoint, symbol, start_date, end_date):
|
|
1671
|
+
session = _get_niftyindices_session()
|
|
777
1672
|
data = {'cinfo': "{'name':'" + symbol + "','startDate':'" + start_date + "','endDate':'" + end_date + "','indexName':'" + symbol + "'}"}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1673
|
+
response = session.post(
|
|
1674
|
+
f"https://www.niftyindices.com/BackPage/{endpoint}",
|
|
1675
|
+
headers=niftyindices_headers, json=data, timeout=20,
|
|
1676
|
+
)
|
|
1677
|
+
text = response.text.strip()
|
|
1678
|
+
if text.startswith('<!DOCTYPE') or text.startswith('<html') or text == "":
|
|
1679
|
+
raise NSEEndpointError(
|
|
1680
|
+
f"niftyindices.com/BackPage/{endpoint} returned HTML/empty instead of JSON "
|
|
1681
|
+
f"(HTTP {response.status_code}) -- the site may be down or have changed again."
|
|
1682
|
+
)
|
|
1683
|
+
try:
|
|
1684
|
+
payload = response.json()
|
|
1685
|
+
except ValueError:
|
|
1686
|
+
raise NSEEndpointError(
|
|
1687
|
+
f"niftyindices.com/BackPage/{endpoint}: non-JSON body (HTTP {response.status_code})"
|
|
1688
|
+
)
|
|
1689
|
+
# Old API wrapped the payload as {"d": "<json string>"}; the new one
|
|
1690
|
+
# returns the array directly. Support both so this keeps working if
|
|
1691
|
+
# niftyindices.com ever reverts/mixes the two shapes.
|
|
1692
|
+
if isinstance(payload, dict) and "d" in payload:
|
|
1693
|
+
payload = json.loads(payload["d"])
|
|
1694
|
+
return pd.DataFrame.from_records(payload)
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def index_history(symbol,start_date,end_date):
|
|
1698
|
+
return _niftyindices_fetch("getHistoricaldatatabletoString", symbol, start_date, end_date)
|
|
782
1699
|
|
|
783
1700
|
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
|
|
1701
|
+
return _niftyindices_fetch("getpepbHistoricaldataDBtoString", symbol, start_date, end_date)
|
|
789
1702
|
|
|
790
1703
|
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
|
|
1704
|
+
return _niftyindices_fetch("getTotalReturnIndexString", symbol, start_date, end_date)
|
|
796
1705
|
|
|
797
1706
|
def get_bhavcopy(date):
|
|
798
1707
|
date = date.replace("-","")
|
|
799
|
-
payload=pd.read_csv(
|
|
1708
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1709
|
+
"https://archives.nseindia.com/products/content/sec_bhavdata_full_"+date+".csv")))
|
|
800
1710
|
return payload
|
|
801
1711
|
|
|
802
1712
|
def get_bulkdeals():
|
|
803
|
-
payload=pd.read_csv(
|
|
1713
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1714
|
+
"https://archives.nseindia.com/content/equities/bulk.csv")))
|
|
804
1715
|
return payload
|
|
805
1716
|
|
|
806
1717
|
def get_blockdeals():
|
|
807
|
-
payload=pd.read_csv(
|
|
1718
|
+
payload = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1719
|
+
"https://archives.nseindia.com/content/equities/block.csv")))
|
|
808
1720
|
return payload
|
|
809
1721
|
|
|
1722
|
+
def _nse_top_corp_info(symbol):
|
|
1723
|
+
"""`/api/top-corp-info?symbol=X&market=equities` bundles a company's
|
|
1724
|
+
latest announcements, corporate actions (bonus/dividend/split/demerger),
|
|
1725
|
+
shareholding pattern history, financial results, and board meetings in
|
|
1726
|
+
one call -- confirmed live and working through curl_cffi+warm-up. This
|
|
1727
|
+
backs both dividend_timeline() and share_holding() below."""
|
|
1728
|
+
symbol = nsesymbolpurify(symbol)
|
|
1729
|
+
return nsefetch(f"https://www.nseindia.com/api/top-corp-info?symbol={symbol}&market=equities")
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def dividend_timeline(symbol):
|
|
1733
|
+
"""github.com/aeron7/nsepython issue #75: documented on
|
|
1734
|
+
unofficed.com/nse-python/ but never actually implemented in the code
|
|
1735
|
+
(calling it raised `AttributeError: module 'nsepython' has no attribute
|
|
1736
|
+
'dividend_timeline'`). Implemented here from `/api/top-corp-info`'s
|
|
1737
|
+
`corporate_actions` list, filtered down to the dividend-purpose entries
|
|
1738
|
+
(that list also contains bonuses/splits/demergers/etc, which this
|
|
1739
|
+
function intentionally excludes to match its name)."""
|
|
1740
|
+
data = _nse_top_corp_info(symbol)
|
|
1741
|
+
actions = (data.get("corporate_actions") or {}).get("data") or []
|
|
1742
|
+
dividends = [a for a in actions if "dividend" in (a.get("purpose") or "").lower()]
|
|
1743
|
+
return pd.DataFrame.from_records(dividends)
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
def share_holding(symbol):
|
|
1747
|
+
"""github.com/aeron7/nsepython issue #75: same situation as
|
|
1748
|
+
dividend_timeline() above -- documented but not implemented. Built from
|
|
1749
|
+
`/api/top-corp-info`'s `shareholdings_patterns` data, which is a dict
|
|
1750
|
+
keyed by filing date (e.g. "31-Mar-2026") whose value is a list of
|
|
1751
|
+
{"<category>": "<percent>"} rows (Promoter & Promoter Group / Public /
|
|
1752
|
+
Shares held by Employee Trusts / Total). Flattened here into one row per
|
|
1753
|
+
filing date with a column per category, newest filing first."""
|
|
1754
|
+
data = _nse_top_corp_info(symbol)
|
|
1755
|
+
by_date = (data.get("shareholdings_patterns") or {}).get("data") or {}
|
|
1756
|
+
rows = []
|
|
1757
|
+
for filing_date, categories in by_date.items():
|
|
1758
|
+
row = {"date": filing_date}
|
|
1759
|
+
for entry in categories:
|
|
1760
|
+
for k, v in entry.items():
|
|
1761
|
+
row[k.strip()] = v.strip() if isinstance(v, str) else v
|
|
1762
|
+
rows.append(row)
|
|
1763
|
+
df = pd.DataFrame.from_records(rows)
|
|
1764
|
+
if not df.empty and "date" in df.columns:
|
|
1765
|
+
try:
|
|
1766
|
+
df = df.sort_values(
|
|
1767
|
+
by="date",
|
|
1768
|
+
key=lambda s: pd.to_datetime(s, format="%d-%b-%Y"),
|
|
1769
|
+
ascending=False,
|
|
1770
|
+
).reset_index(drop=True)
|
|
1771
|
+
except Exception:
|
|
1772
|
+
pass
|
|
1773
|
+
return df
|
|
1774
|
+
|
|
1775
|
+
|
|
810
1776
|
#Request from subhash
|
|
811
1777
|
## https://unofficed.com/how-to-find-the-beta-of-indian-stocks-using-python/
|
|
812
1778
|
def get_beta_df_maker(symbol,days):
|
|
@@ -841,11 +1807,32 @@ def getbeta(symbol,days=365,symbol2="NIFTY 50"):
|
|
|
841
1807
|
|
|
842
1808
|
def get_beta(symbol,days=365,symbol2="NIFTY 50"):
|
|
843
1809
|
#Default is 248 days. (Input of Subhash)
|
|
844
|
-
|
|
845
|
-
|
|
1810
|
+
# github.com/aeron7/nsepython issue #75: this used to raise a raw
|
|
1811
|
+
# KeyError('data') because equity_history() silently returned {} on a
|
|
1812
|
+
# blocked/retired endpoint. nsefetch() now raises a descriptive
|
|
1813
|
+
# NSEEndpointError instead of swallowing the failure -- surface that
|
|
1814
|
+
# (plus any other unexpected shape problem) as a clear, named error
|
|
1815
|
+
# instead of a bare KeyError, per the issue reporter's own suggestion.
|
|
1816
|
+
try:
|
|
1817
|
+
df = get_beta_df_maker(symbol,days)
|
|
1818
|
+
df2 = get_beta_df_maker(symbol2,days)
|
|
1819
|
+
except NSEEndpointError:
|
|
1820
|
+
raise
|
|
1821
|
+
except Exception as e:
|
|
1822
|
+
raise NSEEndpointError(
|
|
1823
|
+
f"get_beta({symbol!r}, symbol2={symbol2!r}): could not build the "
|
|
1824
|
+
f"daily-change series needed for beta -- {e}"
|
|
1825
|
+
) from e
|
|
846
1826
|
|
|
847
1827
|
x=df["daily_change"].tolist()
|
|
848
1828
|
y=df2["daily_change"].tolist()
|
|
1829
|
+
|
|
1830
|
+
if not x or not y:
|
|
1831
|
+
raise NSEEndpointError(
|
|
1832
|
+
f"get_beta({symbol!r}, symbol2={symbol2!r}): got no historical "
|
|
1833
|
+
f"price data back for the requested {days}-day window."
|
|
1834
|
+
)
|
|
1835
|
+
|
|
849
1836
|
#stackoverflow.com/questions/42670055/is-there-any-better-way-to-calculate-the-covariance-of-two-lists-than-this
|
|
850
1837
|
mean_x = sum(x) / len(x)
|
|
851
1838
|
mean_y = sum(y) / len(y)
|
|
@@ -854,12 +1841,26 @@ def get_beta(symbol,days=365,symbol2="NIFTY 50"):
|
|
|
854
1841
|
mean = sum(y) / len(y)
|
|
855
1842
|
variance = sum((i - mean) ** 2 for i in y) / len(y)
|
|
856
1843
|
|
|
1844
|
+
if variance == 0:
|
|
1845
|
+
raise NSEEndpointError(
|
|
1846
|
+
f"get_beta({symbol!r}, symbol2={symbol2!r}): symbol2 had zero "
|
|
1847
|
+
f"price variance over this window, beta is undefined."
|
|
1848
|
+
)
|
|
1849
|
+
|
|
857
1850
|
beta = covariance/variance
|
|
858
1851
|
return round(beta,3)
|
|
859
1852
|
|
|
860
1853
|
def nse_preopen(key="NIFTY",type="pandas"):
|
|
861
1854
|
payload = nsefetch("https://www.nseindia.com/api/market-data-pre-open?key="+key+"")
|
|
862
1855
|
if(type=="pandas"):
|
|
1856
|
+
# NSE's pre-open-market window for most `key` values (e.g. "NIFTY")
|
|
1857
|
+
# is only populated for a few minutes each morning; outside that
|
|
1858
|
+
# window `data` is a legitimate empty list ({"data": [], "msg": "No
|
|
1859
|
+
# Data Found"}), which used to raise a confusing KeyError('metadata')
|
|
1860
|
+
# trying to pull a column out of an empty DataFrame. Return an empty
|
|
1861
|
+
# DataFrame instead.
|
|
1862
|
+
if not payload.get('data'):
|
|
1863
|
+
return pd.DataFrame()
|
|
863
1864
|
payload = pd.DataFrame(payload['data'])
|
|
864
1865
|
payload = pd.json_normalize(payload['metadata'])
|
|
865
1866
|
return payload
|
|
@@ -868,8 +1869,13 @@ def nse_preopen(key="NIFTY",type="pandas"):
|
|
|
868
1869
|
|
|
869
1870
|
#By Avinash https://forum.unofficed.com/t/nsepython-documentation/376/102?u=dexter
|
|
870
1871
|
def nse_preopen_movers(key="FO",filter=1.5):
|
|
1872
|
+
# Round 3 bug fix: the body hardcoded the literal 1.5/-1.5 thresholds
|
|
1873
|
+
# instead of using the `filter` parameter at all -- any caller passing
|
|
1874
|
+
# a custom threshold (nse_preopen_movers(key="FO", filter=50)) got
|
|
1875
|
+
# silently ignored and always got the same 1.5% cutoff back, with no
|
|
1876
|
+
# error or warning.
|
|
871
1877
|
preOpen_gainer=nse_preopen(key)
|
|
872
|
-
return preOpen_gainer[preOpen_gainer['pChange'] >
|
|
1878
|
+
return preOpen_gainer[preOpen_gainer['pChange'] >filter],preOpen_gainer[preOpen_gainer['pChange'] <-filter]
|
|
873
1879
|
|
|
874
1880
|
# type = "securities"
|
|
875
1881
|
# type = "etf"
|
|
@@ -886,7 +1892,8 @@ def nse_most_active(type="securities",sort="value"):
|
|
|
886
1892
|
|
|
887
1893
|
def nse_eq_symbols():
|
|
888
1894
|
#https://forum.unofficed.com/t/feature-request-stocklist-api/1073/11
|
|
889
|
-
eq_list_pd = pd.read_csv(
|
|
1895
|
+
eq_list_pd = pd.read_csv(io.StringIO(_nse_fetch_csv_text(
|
|
1896
|
+
'https://archives.nseindia.com/content/equities/EQUITY_L.csv')))
|
|
890
1897
|
return eq_list_pd['SYMBOL'].tolist()
|
|
891
1898
|
|
|
892
1899
|
def nse_price_band_hitters(bandtype="both",view="AllSec"):
|
|
@@ -904,16 +1911,42 @@ def nse_largedeals(mode="bulk_deals"):
|
|
|
904
1911
|
return pd.DataFrame(payload["SHORT_DEALS_DATA"])
|
|
905
1912
|
if(mode=="block_deals"):
|
|
906
1913
|
return pd.DataFrame(payload["BLOCK_DEALS_DATA"])
|
|
907
|
-
|
|
1914
|
+
|
|
908
1915
|
def nse_largedeals_historical(from_date, to_date, mode="bulk_deals"):
|
|
1916
|
+
# The old /api/historical/{bulk-deals,short-selling,block-deals} family is
|
|
1917
|
+
# retired on the live site (confirmed HTTP 503 straight from NSE's origin
|
|
1918
|
+
# -- not an Akamai bot-challenge: the 503 body is a tiny generic Apache
|
|
1919
|
+
# ErrorDocument page returned with a consistent ~20-30ms *origin* timing
|
|
1920
|
+
# on every single attempt, with or without warm-up/referer variations,
|
|
1921
|
+
# which is the signature of a dead backend route rather than a solvable
|
|
1922
|
+
# JS sensor wall).
|
|
1923
|
+
#
|
|
1924
|
+
# Found the real, current replacement by driving NSE's own "Bulk Deals/
|
|
1925
|
+
# Block Deals/ Short Selling Archives" report page
|
|
1926
|
+
# (https://www.nseindia.com/report-detail/display-bulk-and-block-deals)
|
|
1927
|
+
# with Playwright and capturing what it actually calls when you click
|
|
1928
|
+
# Go: `/api/historicalOR/bulk-block-short-deals?optionType=<mode>&from=
|
|
1929
|
+
# ..&to=..` -- same host-prefix swap pattern as equity/derivatives above,
|
|
1930
|
+
# just a different path and param name (`optionType=`, not a path
|
|
1931
|
+
# segment), confirmed live for all three modes. Response shape is the
|
|
1932
|
+
# same `{"data": [...]}` the old endpoint returned, just with a different
|
|
1933
|
+
# (current) NSE column-name scheme:
|
|
1934
|
+
# bulk_deals/block_deals -> BD_DT_DATE, BD_DT_ORDER, BD_SYMBOL,
|
|
1935
|
+
# BD_SCRIP_NAME, BD_CLIENT_NAME, BD_BUY_SELL,
|
|
1936
|
+
# BD_QTY_TRD, BD_TP_WATP, BD_REMARKS
|
|
1937
|
+
# short_deals -> SS_DATE, SS_DATE_ORDER, SS_SYMBOL, SS_NAME,
|
|
1938
|
+
# SS_QTY
|
|
909
1939
|
if mode == "bulk_deals":
|
|
910
|
-
|
|
1940
|
+
option_type = "bulk_deals"
|
|
911
1941
|
elif mode == "short_deals":
|
|
912
|
-
|
|
1942
|
+
option_type = "short_selling"
|
|
913
1943
|
elif mode == "block_deals":
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1944
|
+
option_type = "block_deals"
|
|
1945
|
+
else:
|
|
1946
|
+
option_type = mode
|
|
1947
|
+
|
|
1948
|
+
url = ('https://www.nseindia.com/api/historicalOR/bulk-block-short-deals'
|
|
1949
|
+
'?optionType=' + option_type + '&from=' + from_date + '&to=' + to_date)
|
|
917
1950
|
logging.info("Fetching " + str(url))
|
|
918
1951
|
payload = nsefetch(url)
|
|
919
1952
|
return pd.DataFrame(payload["data"])
|
|
@@ -922,45 +1955,748 @@ def nse_largedeals_historical(from_date, to_date, mode="bulk_deals"):
|
|
|
922
1955
|
#print(get_fao_participant_oi("04-06-2021"))
|
|
923
1956
|
def get_fao_participant_oi(date):
|
|
924
1957
|
date = date.replace("-","")
|
|
925
|
-
|
|
1958
|
+
# Round 3 bug fix: this CSV has a title/caption row as line 1
|
|
1959
|
+
# ('""Participant wise Open Interest...""') with the REAL header on
|
|
1960
|
+
# line 2 -- reading it with no skiprows made pandas parse the caption
|
|
1961
|
+
# as the header and shift the real header row down into the data,
|
|
1962
|
+
# mislabeling every single column (confirmed live on every trading date
|
|
1963
|
+
# tested: columns came out as 'Unnamed: 2', 'Unnamed: 3', etc instead of
|
|
1964
|
+
# 'Future Index Long', 'Total Short Contracts', ...).
|
|
1965
|
+
text = _nse_fetch_csv_text(
|
|
1966
|
+
"https://archives.nseindia.com/content/nsccl/fao_participant_oi_"+date+".csv")
|
|
1967
|
+
payload = pd.read_csv(io.StringIO(text), skiprows=1)
|
|
1968
|
+
# NSE's own header row carries stray trailing whitespace on a couple of
|
|
1969
|
+
# columns (e.g. "Future Stock Short ") -- strip it so column
|
|
1970
|
+
# lookups by name work as documented.
|
|
1971
|
+
payload.columns = [c.strip() for c in payload.columns]
|
|
926
1972
|
return payload
|
|
927
1973
|
|
|
928
1974
|
#https://forum.unofficed.com/t/how-to-check-if-the-market-is-open-today-or-not/1268/1
|
|
929
1975
|
def is_market_open(segment = "FO"): #COM,CD,CB,CMOT,COM,FO,IRD,MF,NDM,NTRP,SLBS
|
|
930
|
-
|
|
1976
|
+
# Bug fix: the previous version returned True/False based only on
|
|
1977
|
+
# holiday_json's *first* entry, so it almost always reported "open"
|
|
1978
|
+
# regardless of today's actual date (today is essentially never the
|
|
1979
|
+
# first holiday in the list). Scan the whole list for a match instead.
|
|
931
1980
|
holiday_json = nse_holidays()[segment]
|
|
932
1981
|
|
|
933
1982
|
# Get today's date in the format 'dd-Mon-yyyy'
|
|
934
1983
|
today_date = datetime.date.today().strftime('%d-%b-%Y')
|
|
935
1984
|
|
|
936
|
-
# Check if today's date is in the holiday_json
|
|
937
1985
|
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']}")
|
|
1986
|
+
if holiday.get('tradingDate') == today_date:
|
|
1987
|
+
print(f"Market is closed today because of {holiday.get('description')}")
|
|
943
1988
|
return False
|
|
944
1989
|
|
|
1990
|
+
print("FNO Market is open today. Have a Nice Trade!")
|
|
1991
|
+
return True
|
|
1992
|
+
|
|
945
1993
|
def nse_expirydetails_by_symbol(symbol,meta ="Futures",i=0):
|
|
946
|
-
payload =
|
|
1994
|
+
payload = nse_quote_derivatives(symbol)
|
|
1995
|
+
expiry_dates = []
|
|
1996
|
+
|
|
1997
|
+
# Extract from new FNO payload structure
|
|
1998
|
+
if 'data' in payload:
|
|
1999
|
+
unique_dates = set()
|
|
2000
|
+
for entry in payload['data']:
|
|
2001
|
+
if 'expiryDate' in entry:
|
|
2002
|
+
# Filter by meta type if possible, though 'data' usually contains all
|
|
2003
|
+
# To be precise, we can check instrumentType
|
|
2004
|
+
it = entry.get('instrumentType', '')
|
|
2005
|
+
if (meta == "Futures" and "FUT" in it) or (meta == "Options" and "OPT" in it):
|
|
2006
|
+
unique_dates.add(entry['expiryDate'])
|
|
2007
|
+
expiry_dates = sorted(list(unique_dates), key=lambda x: datetime.datetime.strptime(x, "%d-%b-%Y"))
|
|
2008
|
+
|
|
2009
|
+
# Filter future dates
|
|
2010
|
+
future_expiry_dates = []
|
|
2011
|
+
if expiry_dates:
|
|
2012
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in expiry_dates]
|
|
2013
|
+
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"))
|
|
2014
|
+
|
|
2015
|
+
# Fallback to expiry_list if i is out of range
|
|
2016
|
+
if i >= len(future_expiry_dates):
|
|
2017
|
+
dates = expiry_list(symbol, type="list")
|
|
2018
|
+
if dates:
|
|
2019
|
+
temp_dates = [datetime.datetime.strptime(date, "%d-%b-%Y").date() for date in dates]
|
|
2020
|
+
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"))
|
|
2021
|
+
|
|
2022
|
+
if i >= len(future_expiry_dates):
|
|
2023
|
+
return None, None
|
|
2024
|
+
|
|
2025
|
+
currentExpiry = future_expiry_dates[i]
|
|
2026
|
+
currentExpiry_dt = datetime.datetime.strptime(currentExpiry, '%d-%b-%Y').date()
|
|
2027
|
+
date_today = run_time.date()
|
|
2028
|
+
dte = (currentExpiry_dt - date_today).days
|
|
2029
|
+
return currentExpiry_dt, dte
|
|
2030
|
+
|
|
2031
|
+
def security_wise_archive(from_date, to_date, symbol, series="ALL"):
|
|
2032
|
+
# The old /api/historical/securityArchives route is retired on the live
|
|
2033
|
+
# site (confirmed HTTP 503 straight from NSE's origin -- same dead-route
|
|
2034
|
+
# signature as nse_largedeals_historical() above, not a solvable Akamai
|
|
2035
|
+
# challenge: tiny generic Apache ErrorDocument body, consistent fast
|
|
2036
|
+
# origin timing on every attempt regardless of warm-up/referer).
|
|
2037
|
+
#
|
|
2038
|
+
# Found the real, current replacement by driving NSE's own "Security-wise
|
|
2039
|
+
# Archives (Equities)" report page
|
|
2040
|
+
# (https://www.nseindia.com/report-detail/eq_security) with Playwright
|
|
2041
|
+
# and capturing what it actually calls when you click Go:
|
|
2042
|
+
# `/api/historicalOR/generateSecurityWiseHistoricalData?from=..&to=..&
|
|
2043
|
+
# symbol=..&type=..&series=..` -- same host-prefix-swap family as
|
|
2044
|
+
# equity_history()/derivative_history() above, just a different path and
|
|
2045
|
+
# `type=` instead of `dataType=`. Confirmed live: response shape is the
|
|
2046
|
+
# same `{"data": [...]}` with the same CH_*/COP_DELIV_* column names the
|
|
2047
|
+
# old endpoint used (cross-checked against equity_history()'s numbers for
|
|
2048
|
+
# the same symbol/dates -- exact match).
|
|
2049
|
+
base_url = "https://www.nseindia.com/api/historicalOR/generateSecurityWiseHistoricalData"
|
|
2050
|
+
url = f"{base_url}?from={from_date}&to={to_date}&symbol={symbol.upper()}&type=priceVolumeDeliverable&series={series.upper()}"
|
|
2051
|
+
payload = nsefetch(url)
|
|
2052
|
+
return pd.DataFrame(payload['data'])
|
|
947
2053
|
|
|
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)
|
|
952
2054
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
2055
|
+
# ---------------------------------------------------------------------------
|
|
2056
|
+
# NSE's official, no-auth MCP (Model Context Protocol) servers
|
|
2057
|
+
#
|
|
2058
|
+
# NSE India publishes its own free, no-API-key-required MCP servers
|
|
2059
|
+
# (https://www.nseindia.com/nse-mcp) -- two streamable-HTTP endpoints:
|
|
2060
|
+
#
|
|
2061
|
+
# "bhavcopy" -- https://mcp.nseindia.in/bhavcopy/cm/mcp
|
|
2062
|
+
# ("nse-bhavcopy-redis-mcp", 21 tools): historical/derived data --
|
|
2063
|
+
# stock & index history, valuations, corporate actions, comparisons,
|
|
2064
|
+
# moving averages, 52-week range, market mood/breadth, symbol search.
|
|
2065
|
+
#
|
|
2066
|
+
# "cmmkt" -- https://mcp.nseindia.in/cmmkt/mcp
|
|
2067
|
+
# ("cm-market-mcp", 15 tools): live cash-market data -- live quotes,
|
|
2068
|
+
# gainers/losers, live index values, equity/SME/bond/call-auction
|
|
2069
|
+
# stock lists.
|
|
2070
|
+
#
|
|
2071
|
+
# This is a genuinely different, independent path into NSE data from the
|
|
2072
|
+
# rest of this module: it is NSE's own hosted service, not a scrape of
|
|
2073
|
+
# nseindia.com through curl_cffi's Akamai-impersonation transport, so it is
|
|
2074
|
+
# unaffected by Akamai Bot Manager entirely and is often the more reliable
|
|
2075
|
+
# choice when it covers the data you need. It still reuses this module's
|
|
2076
|
+
# shared curl_cffi session (_get_nse_session()) purely for cheap connection
|
|
2077
|
+
# pooling / one consistent TLS-fingerprint story -- the MCP calls themselves
|
|
2078
|
+
# need no cookies, no warm-up, and no auth of any kind.
|
|
2079
|
+
#
|
|
2080
|
+
# Protocol notes (streamable-HTTP MCP, JSON-RPC 2.0):
|
|
2081
|
+
# - POST an "initialize" request first; the response carries a
|
|
2082
|
+
# "Mcp-Session-Id" (or "mcp-session-id") header that must be echoed back
|
|
2083
|
+
# as a header on every subsequent request for that session.
|
|
2084
|
+
# - A "notifications/initialized" notification should follow (no response
|
|
2085
|
+
# body expected) before calling any tool.
|
|
2086
|
+
# - "tools/call" responses come back either as plain JSON or as an
|
|
2087
|
+
# SSE-framed body (Content-Type: text/event-stream) shaped like
|
|
2088
|
+
# "event:message\ndata:{...}\n\n" -- both are handled below.
|
|
2089
|
+
# - The actual tool result is nested at result.content[0].text, which is
|
|
2090
|
+
# itself a JSON string in practice for every tool checked so far.
|
|
2091
|
+
# - A small number of tools (confirmed: nse_get_gainers / nse_get_losers
|
|
2092
|
+
# on the cmmkt server) currently come back with isError=false but an
|
|
2093
|
+
# inner {"error": "..."} payload -- a live bug on NSE's own server side
|
|
2094
|
+
# ("Failed to parse cached data: ArrayList cannot be cast to Map").
|
|
2095
|
+
# That is treated the same as any other failure here: raised as
|
|
2096
|
+
# NSEEndpointError rather than silently handed back as "data".
|
|
2097
|
+
# ---------------------------------------------------------------------------
|
|
2098
|
+
|
|
2099
|
+
_NSE_MCP_SERVERS = {
|
|
2100
|
+
"bhavcopy": "https://mcp.nseindia.in/bhavcopy/cm/mcp",
|
|
2101
|
+
"cmmkt": "https://mcp.nseindia.in/cmmkt/mcp",
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
_NSE_MCP_CLIENT_VERSION = "2.98"
|
|
2105
|
+
|
|
2106
|
+
# Lightweight session-id cache, keyed by server URL, so repeated calls to the
|
|
2107
|
+
# same MCP server don't re-run the "initialize" handshake every time.
|
|
2108
|
+
_nse_mcp_session_cache = {}
|
|
2109
|
+
|
|
2110
|
+
|
|
2111
|
+
def _nse_mcp_parse_response(r):
|
|
2112
|
+
"""Parse one MCP HTTP response body, which comes back as either plain
|
|
2113
|
+
JSON or an SSE-framed body (Content-Type: text/event-stream) shaped like
|
|
2114
|
+
"event:message\\ndata:{...}\\n\\n". Returns the decoded JSON-RPC envelope
|
|
2115
|
+
dict either way.
|
|
2116
|
+
"""
|
|
2117
|
+
ctype = r.headers.get("content-type", "") or ""
|
|
2118
|
+
if "text/event-stream" in ctype:
|
|
2119
|
+
data_lines = [
|
|
2120
|
+
line[len("data:"):].strip()
|
|
2121
|
+
for line in r.text.splitlines()
|
|
2122
|
+
if line.startswith("data:")
|
|
2123
|
+
]
|
|
2124
|
+
if not data_lines:
|
|
2125
|
+
raise NSEEndpointError("nse_mcp: empty SSE response body")
|
|
2126
|
+
try:
|
|
2127
|
+
return json.loads("".join(data_lines))
|
|
2128
|
+
except ValueError:
|
|
2129
|
+
raise NSEEndpointError("nse_mcp: malformed SSE JSON payload")
|
|
2130
|
+
|
|
2131
|
+
try:
|
|
2132
|
+
return r.json()
|
|
2133
|
+
except ValueError:
|
|
2134
|
+
# A server occasionally mislabels which framing it used -- scan for
|
|
2135
|
+
# "data:" lines regardless of the declared content-type before
|
|
2136
|
+
# giving up.
|
|
2137
|
+
data_lines = [
|
|
2138
|
+
line[len("data:"):].strip()
|
|
2139
|
+
for line in r.text.splitlines()
|
|
2140
|
+
if line.startswith("data:")
|
|
2141
|
+
]
|
|
2142
|
+
if data_lines:
|
|
2143
|
+
try:
|
|
2144
|
+
return json.loads("".join(data_lines))
|
|
2145
|
+
except ValueError:
|
|
2146
|
+
pass
|
|
2147
|
+
raise NSEEndpointError(
|
|
2148
|
+
f"nse_mcp: non-JSON, non-SSE response body (content-type={ctype!r})"
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2151
|
+
|
|
2152
|
+
def _nse_mcp_initialize(server_url):
|
|
2153
|
+
"""Run the MCP "initialize" handshake (+ "notifications/initialized")
|
|
2154
|
+
against server_url and return the Mcp-Session-Id NSE's server hands
|
|
2155
|
+
back (or "" if the server doesn't issue one). Retries up to 3 times --
|
|
2156
|
+
the bhavcopy endpoint has been observed to 502 on a cold first request.
|
|
2157
|
+
"""
|
|
2158
|
+
session = _get_nse_session()
|
|
2159
|
+
mcp_headers = {
|
|
2160
|
+
"Content-Type": "application/json",
|
|
2161
|
+
"Accept": "application/json, text/event-stream",
|
|
2162
|
+
}
|
|
2163
|
+
init_body = {
|
|
2164
|
+
"jsonrpc": "2.0",
|
|
2165
|
+
"id": 1,
|
|
2166
|
+
"method": "initialize",
|
|
2167
|
+
"params": {
|
|
2168
|
+
"protocolVersion": "2024-11-05",
|
|
2169
|
+
"capabilities": {},
|
|
2170
|
+
"clientInfo": {"name": "nsepython", "version": _NSE_MCP_CLIENT_VERSION},
|
|
2171
|
+
},
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
last_exc = None
|
|
2175
|
+
for attempt in range(3):
|
|
2176
|
+
try:
|
|
2177
|
+
r = session.post(server_url, headers=mcp_headers, json=init_body, timeout=30)
|
|
2178
|
+
except Exception as e:
|
|
2179
|
+
last_exc = NSEEndpointError(f"nse_mcp initialize: request failed for {server_url}: {e}")
|
|
2180
|
+
continue
|
|
2181
|
+
|
|
2182
|
+
if r.status_code == 200:
|
|
2183
|
+
session_id = r.headers.get("mcp-session-id") or r.headers.get("Mcp-Session-Id") or ""
|
|
2184
|
+
notify_headers = dict(mcp_headers)
|
|
2185
|
+
if session_id:
|
|
2186
|
+
notify_headers["Mcp-Session-Id"] = session_id
|
|
2187
|
+
try:
|
|
2188
|
+
session.post(
|
|
2189
|
+
server_url, headers=notify_headers,
|
|
2190
|
+
json={"jsonrpc": "2.0", "method": "notifications/initialized"},
|
|
2191
|
+
timeout=15,
|
|
2192
|
+
)
|
|
2193
|
+
except Exception:
|
|
2194
|
+
pass # fire-and-forget notification; failure here is harmless
|
|
2195
|
+
return session_id
|
|
2196
|
+
|
|
2197
|
+
last_exc = NSEEndpointError(
|
|
2198
|
+
f"nse_mcp initialize: HTTP {r.status_code} for {server_url}"
|
|
2199
|
+
)
|
|
2200
|
+
|
|
2201
|
+
raise last_exc or NSEEndpointError(f"nse_mcp initialize: failed for {server_url}")
|
|
2202
|
+
|
|
2203
|
+
|
|
2204
|
+
def _nse_mcp_get_session_id(server_url, force_new=False):
|
|
2205
|
+
"""Return a cached Mcp-Session-Id for server_url, initializing (and
|
|
2206
|
+
caching) one if there isn't one yet or force_new is requested.
|
|
2207
|
+
"""
|
|
2208
|
+
if not force_new and server_url in _nse_mcp_session_cache:
|
|
2209
|
+
return _nse_mcp_session_cache[server_url]
|
|
2210
|
+
session_id = _nse_mcp_initialize(server_url)
|
|
2211
|
+
_nse_mcp_session_cache[server_url] = session_id
|
|
2212
|
+
return session_id
|
|
2213
|
+
|
|
2214
|
+
|
|
2215
|
+
def _nse_mcp_call(server_url, tool_name, arguments=None):
|
|
2216
|
+
"""Call one tool on an NSE-official MCP server and return its parsed
|
|
2217
|
+
result payload.
|
|
2218
|
+
|
|
2219
|
+
Handles the initialize/session-id handshake (with a small cache keyed
|
|
2220
|
+
by server_url so repeat calls don't re-initialize every time), both
|
|
2221
|
+
plain-JSON and SSE response framing, and the nested
|
|
2222
|
+
result.content[0].text tool-result convention (itself JSON-encoded for
|
|
2223
|
+
every tool checked so far). Raises NSEEndpointError -- never returns
|
|
2224
|
+
`{}` -- on any transport failure, JSON-RPC error, MCP tool-level error,
|
|
2225
|
+
or an application-level {"error": ...} payload the tool itself reports.
|
|
2226
|
+
"""
|
|
2227
|
+
arguments = arguments or {}
|
|
2228
|
+
session = _get_nse_session()
|
|
2229
|
+
mcp_headers = {
|
|
2230
|
+
"Content-Type": "application/json",
|
|
2231
|
+
"Accept": "application/json, text/event-stream",
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
data = None
|
|
2235
|
+
last_exc = None
|
|
2236
|
+
for attempt in (1, 2):
|
|
2237
|
+
try:
|
|
2238
|
+
session_id = _nse_mcp_get_session_id(server_url, force_new=(attempt == 2))
|
|
2239
|
+
except NSEEndpointError as e:
|
|
2240
|
+
last_exc = e
|
|
2241
|
+
continue
|
|
2242
|
+
|
|
2243
|
+
call_headers = dict(mcp_headers)
|
|
2244
|
+
if session_id:
|
|
2245
|
+
call_headers["Mcp-Session-Id"] = session_id
|
|
2246
|
+
|
|
2247
|
+
body = {
|
|
2248
|
+
"jsonrpc": "2.0",
|
|
2249
|
+
"id": 3,
|
|
2250
|
+
"method": "tools/call",
|
|
2251
|
+
"params": {"name": tool_name, "arguments": arguments},
|
|
2252
|
+
}
|
|
2253
|
+
try:
|
|
2254
|
+
r = session.post(server_url, headers=call_headers, json=body, timeout=30)
|
|
2255
|
+
except Exception as e:
|
|
2256
|
+
last_exc = NSEEndpointError(f"nse_mcp_call({tool_name}): request failed: {e}")
|
|
2257
|
+
continue
|
|
2258
|
+
|
|
2259
|
+
if r.status_code in (401, 403, 404, 409, 502, 503) and attempt == 1:
|
|
2260
|
+
# Could be a stale/expired session id, or the transient 502 seen
|
|
2261
|
+
# on the bhavcopy endpoint's first request -- drop the cached
|
|
2262
|
+
# session and retry once with a fresh initialize.
|
|
2263
|
+
last_exc = NSEEndpointError(
|
|
2264
|
+
f"nse_mcp_call({tool_name}): HTTP {r.status_code} from {server_url}"
|
|
2265
|
+
)
|
|
2266
|
+
_nse_mcp_session_cache.pop(server_url, None)
|
|
2267
|
+
continue
|
|
2268
|
+
|
|
2269
|
+
if r.status_code != 200:
|
|
2270
|
+
raise NSEEndpointError(
|
|
2271
|
+
f"nse_mcp_call({tool_name}): HTTP {r.status_code} from {server_url}"
|
|
2272
|
+
)
|
|
2273
|
+
|
|
2274
|
+
data = _nse_mcp_parse_response(r)
|
|
2275
|
+
last_exc = None
|
|
2276
|
+
break
|
|
2277
|
+
|
|
2278
|
+
if data is None:
|
|
2279
|
+
raise last_exc or NSEEndpointError(
|
|
2280
|
+
f"nse_mcp_call({tool_name}): failed against {server_url}"
|
|
2281
|
+
)
|
|
2282
|
+
|
|
2283
|
+
if data.get("error"):
|
|
2284
|
+
raise NSEEndpointError(
|
|
2285
|
+
f"nse_mcp_call({tool_name}): JSON-RPC error: {data['error']}"
|
|
2286
|
+
)
|
|
2287
|
+
|
|
2288
|
+
result = data.get("result") or {}
|
|
2289
|
+
content = result.get("content") or []
|
|
2290
|
+
if not content:
|
|
2291
|
+
raise NSEEndpointError(
|
|
2292
|
+
f"nse_mcp_call({tool_name}): empty/missing content in response: {result}"
|
|
2293
|
+
)
|
|
2294
|
+
|
|
2295
|
+
text = content[0].get("text", "")
|
|
2296
|
+
try:
|
|
2297
|
+
payload = json.loads(text)
|
|
2298
|
+
except (ValueError, TypeError):
|
|
2299
|
+
payload = text # plain text/markdown tool result -- hand it back as-is
|
|
2300
|
+
|
|
2301
|
+
if result.get("isError"):
|
|
2302
|
+
raise NSEEndpointError(
|
|
2303
|
+
f"nse_mcp_call({tool_name}): tool reported an error: {payload}"
|
|
2304
|
+
)
|
|
2305
|
+
|
|
2306
|
+
if isinstance(payload, dict) and "error" in payload:
|
|
2307
|
+
# Seen live on nse_get_gainers/nse_get_losers: isError=false but an
|
|
2308
|
+
# inner application-level error from NSE's own server. Don't hand
|
|
2309
|
+
# this back as if it were usable data.
|
|
2310
|
+
raise NSEEndpointError(
|
|
2311
|
+
f"nse_mcp_call({tool_name}): NSE's MCP server reported an application "
|
|
2312
|
+
f"error for this call: {payload['error']}"
|
|
2313
|
+
)
|
|
2314
|
+
|
|
2315
|
+
return payload
|
|
2316
|
+
|
|
2317
|
+
|
|
2318
|
+
def nse_mcp_call(server, tool_name, **kwargs):
|
|
2319
|
+
"""Call ANY tool on NSE's own official, no-auth MCP servers by name --
|
|
2320
|
+
a generic escape hatch for a tool this module doesn't have a named
|
|
2321
|
+
wrapper for (yet), or any new tool NSE adds to either server in future.
|
|
2322
|
+
|
|
2323
|
+
`server` is "bhavcopy" (historical/derived data) or "cmmkt" (live
|
|
2324
|
+
market data). `kwargs` become the tool's `arguments` object, passed
|
|
2325
|
+
straight through to NSE's MCP endpoint -- see nse_mcp_list_tools() for
|
|
2326
|
+
each tool's name, description and accepted arguments.
|
|
2327
|
+
|
|
2328
|
+
Backed by NSE's own official, no-auth MCP server, not the
|
|
2329
|
+
Akamai-affected nseindia.com scrape path the rest of this module uses --
|
|
2330
|
+
a notably more reliable route when it covers the data you need.
|
|
2331
|
+
"""
|
|
2332
|
+
server_url = _NSE_MCP_SERVERS.get(server)
|
|
2333
|
+
if server_url is None:
|
|
2334
|
+
raise NSEEndpointError(
|
|
2335
|
+
f"nse_mcp_call: unknown server {server!r}, expected 'bhavcopy' or 'cmmkt'"
|
|
2336
|
+
)
|
|
2337
|
+
return _nse_mcp_call(server_url, tool_name, kwargs)
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
def nse_mcp_list_tools(server=""):
|
|
2341
|
+
"""Return NSE's own live tools/list response -- name, description and
|
|
2342
|
+
full inputSchema -- for one MCP server ("bhavcopy" or "cmmkt"), or both
|
|
2343
|
+
(as a dict keyed by server name) when `server` is omitted/empty.
|
|
2344
|
+
|
|
2345
|
+
Always asks the server live rather than returning a hardcoded copy, so
|
|
2346
|
+
this stays accurate if/when NSE changes either server's toolset.
|
|
2347
|
+
"""
|
|
2348
|
+
if server:
|
|
2349
|
+
if server not in _NSE_MCP_SERVERS:
|
|
2350
|
+
raise NSEEndpointError(
|
|
2351
|
+
f"nse_mcp_list_tools: unknown server {server!r}, expected 'bhavcopy' or 'cmmkt'"
|
|
2352
|
+
)
|
|
2353
|
+
servers = {server: _NSE_MCP_SERVERS[server]}
|
|
2354
|
+
else:
|
|
2355
|
+
servers = _NSE_MCP_SERVERS
|
|
2356
|
+
|
|
2357
|
+
session = _get_nse_session()
|
|
2358
|
+
out = {}
|
|
2359
|
+
for name, url in servers.items():
|
|
2360
|
+
session_id = _nse_mcp_get_session_id(url)
|
|
2361
|
+
mcp_headers = {
|
|
2362
|
+
"Content-Type": "application/json",
|
|
2363
|
+
"Accept": "application/json, text/event-stream",
|
|
2364
|
+
}
|
|
2365
|
+
if session_id:
|
|
2366
|
+
mcp_headers["Mcp-Session-Id"] = session_id
|
|
2367
|
+
body = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
|
|
2368
|
+
r = session.post(url, headers=mcp_headers, json=body, timeout=30)
|
|
2369
|
+
if r.status_code != 200:
|
|
2370
|
+
raise NSEEndpointError(f"nse_mcp_list_tools({name}): HTTP {r.status_code}")
|
|
2371
|
+
data = _nse_mcp_parse_response(r)
|
|
2372
|
+
if data.get("error"):
|
|
2373
|
+
raise NSEEndpointError(f"nse_mcp_list_tools({name}): JSON-RPC error: {data['error']}")
|
|
2374
|
+
out[name] = (data.get("result") or {}).get("tools", [])
|
|
2375
|
+
|
|
2376
|
+
return out[server] if server else out
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
def _nse_mcp_records(payload, key):
|
|
2380
|
+
"""Return payload[key] (a list of record-dicts) as a DataFrame, or an
|
|
2381
|
+
empty DataFrame if the key is absent -- the same "list field on a dict
|
|
2382
|
+
payload becomes a DataFrame" convention used throughout this file.
|
|
2383
|
+
"""
|
|
2384
|
+
rows = payload.get(key) if isinstance(payload, dict) else None
|
|
2385
|
+
return pd.DataFrame(rows if rows else [])
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
# ---------------------------------------------------------------------------
|
|
2389
|
+
# Named wrappers -- "bhavcopy" server (nse-bhavcopy-redis-mcp, 21 tools)
|
|
2390
|
+
# ---------------------------------------------------------------------------
|
|
2391
|
+
|
|
2392
|
+
def nse_mcp_get_top_by_volume(date="today", n=10, sort_by="volume"):
|
|
2393
|
+
"""Get the top N most actively traded NSE stocks on a date, sorted by
|
|
2394
|
+
'volume' (traded quantity) or 'value' (turnover in Rs). Backed by NSE's
|
|
2395
|
+
own official no-auth MCP server (bhavcopy)."""
|
|
2396
|
+
payload = nse_mcp_call("bhavcopy", "get_top_by_volume", date=date, n=n, sortBy=sort_by)
|
|
2397
|
+
return _nse_mcp_records(payload, "stocks")
|
|
2398
|
+
|
|
2399
|
+
|
|
2400
|
+
def nse_mcp_get_top_movers(date="today", n=10, direction="gain"):
|
|
2401
|
+
"""Get the top N gaining ('gain') or losing ('loss') NSE stocks on a
|
|
2402
|
+
date, with OHLCV details. Backed by NSE's own official no-auth MCP
|
|
2403
|
+
server (bhavcopy)."""
|
|
2404
|
+
payload = nse_mcp_call("bhavcopy", "get_top_movers", date=date, n=n, direction=direction)
|
|
2405
|
+
return _nse_mcp_records(payload, "stocks")
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
def nse_mcp_nse_lookup_symbol(query):
|
|
2409
|
+
"""Look up NSE ticker symbols by partial name or keyword (ticker list
|
|
2410
|
+
only, no price data). Backed by NSE's own official no-auth MCP server
|
|
2411
|
+
(bhavcopy)."""
|
|
2412
|
+
payload = nse_mcp_call("bhavcopy", "nse_lookup_symbol", query=query)
|
|
2413
|
+
return payload.get("symbols", []) if isinstance(payload, dict) else payload
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
def nse_mcp_get_market_mood(date="today"):
|
|
2417
|
+
"""Get a factual read of NSE market mood for a day: India VIX level and
|
|
2418
|
+
trend, index/stock advance-decline breadth, and benchmark changes.
|
|
2419
|
+
Backed by NSE's own official no-auth MCP server (bhavcopy)."""
|
|
2420
|
+
return nse_mcp_call("bhavcopy", "get_market_mood", date=date)
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
def nse_mcp_get_index_valuation(index_name, months=24, date="today"):
|
|
2424
|
+
"""Get an NSE index's valuation ratios (P/E, P/B, dividend yield) and
|
|
2425
|
+
where today's value sits within its own recent range. Backed by NSE's
|
|
2426
|
+
own official no-auth MCP server (bhavcopy)."""
|
|
2427
|
+
return nse_mcp_call(
|
|
2428
|
+
"bhavcopy", "get_index_valuation", indexName=index_name, months=months, date=date
|
|
2429
|
+
)
|
|
2430
|
+
|
|
2431
|
+
|
|
2432
|
+
def nse_mcp_get_market_breadth(date="today"):
|
|
2433
|
+
"""Get overall NSE market breadth for a trading date: advances,
|
|
2434
|
+
declines, unchanged, A/D ratio, total volume. Backed by NSE's own
|
|
2435
|
+
official no-auth MCP server (bhavcopy)."""
|
|
2436
|
+
return nse_mcp_call("bhavcopy", "get_market_breadth", date=date)
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
def nse_mcp_get_corporate_actions(symbol, from_date="", to_date=""):
|
|
2440
|
+
"""Fetch actual NSE corporate action events (splits/bonus/dividends/
|
|
2441
|
+
other) for a stock, with exact ex-dates and adjustment factors. Backed
|
|
2442
|
+
by NSE's own official no-auth MCP server (bhavcopy)."""
|
|
2443
|
+
payload = nse_mcp_call(
|
|
2444
|
+
"bhavcopy", "get_corporate_actions", symbol=symbol, fromDate=from_date, toDate=to_date
|
|
2445
|
+
)
|
|
2446
|
+
return _nse_mcp_records(payload, "actions")
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
def nse_mcp_compare_indices(index_names, months=6, date="today"):
|
|
2450
|
+
"""Compare 2 to 10 NSE indices side by side: return, annualised
|
|
2451
|
+
volatility, max drawdown and current valuation. Backed by NSE's own
|
|
2452
|
+
official no-auth MCP server (bhavcopy)."""
|
|
2453
|
+
payload = nse_mcp_call(
|
|
2454
|
+
"bhavcopy", "compare_indices", indexNames=index_names, months=months, date=date
|
|
2455
|
+
)
|
|
2456
|
+
return _nse_mcp_records(payload, "indices")
|
|
2457
|
+
|
|
2458
|
+
|
|
2459
|
+
def nse_mcp_get_index_movers(date="today", period="1D", n=10, scope="equity"):
|
|
2460
|
+
"""Get the top gaining and top losing NSE indices for a day or period
|
|
2461
|
+
(1D/1W/1M/3M/6M/1Y) -- useful for sector/theme rotation. Backed by NSE's
|
|
2462
|
+
own official no-auth MCP server (bhavcopy). Returns the raw dict (both
|
|
2463
|
+
a 'gainers' and a 'losers' list) since the result isn't a single table."""
|
|
2464
|
+
return nse_mcp_call(
|
|
2465
|
+
"bhavcopy", "get_index_movers", date=date, period=period, n=n, scope=scope
|
|
2466
|
+
)
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
def nse_mcp_get_ltp_by_date(symbol, date="today"):
|
|
2470
|
+
"""Return the last traded (close) price for an NSE symbol on a date
|
|
2471
|
+
(previous trading day's price if the date is a non-trading day). Backed
|
|
2472
|
+
by NSE's own official no-auth MCP server (bhavcopy)."""
|
|
2473
|
+
return nse_mcp_call("bhavcopy", "get_ltp_by_date", symbol=symbol, date=date)
|
|
2474
|
+
|
|
2475
|
+
|
|
2476
|
+
def nse_mcp_get_bulk_quote(symbols):
|
|
2477
|
+
"""Get the latest price snapshot (OHLC, prev close, % change, volume)
|
|
2478
|
+
for up to 50 NSE stocks in one call. Backed by NSE's own official
|
|
2479
|
+
no-auth MCP server (bhavcopy)."""
|
|
2480
|
+
payload = nse_mcp_call("bhavcopy", "get_bulk_quote", symbols=symbols)
|
|
2481
|
+
return _nse_mcp_records(payload, "quotes")
|
|
2482
|
+
|
|
2483
|
+
|
|
2484
|
+
def nse_mcp_get_volume_analysis(symbol, days=30):
|
|
2485
|
+
"""Analyse trading volume trends for an NSE stock over N trading days:
|
|
2486
|
+
average/max/min volume, volume spike days, recent trend. Backed by
|
|
2487
|
+
NSE's own official no-auth MCP server (bhavcopy)."""
|
|
2488
|
+
return nse_mcp_call("bhavcopy", "get_volume_analysis", symbol=symbol, days=days)
|
|
2489
|
+
|
|
2490
|
+
|
|
2491
|
+
def nse_mcp_get_stock_history(symbol, months=3, end_date="today"):
|
|
2492
|
+
"""Get daily OHLCV price history for an NSE stock (up to 3 months per
|
|
2493
|
+
call; chain calls using the response's next_end_date for longer
|
|
2494
|
+
periods). Backed by NSE's own official no-auth MCP server (bhavcopy)."""
|
|
2495
|
+
payload = nse_mcp_call(
|
|
2496
|
+
"bhavcopy", "get_stock_history", symbol=symbol, months=months, endDate=end_date
|
|
2497
|
+
)
|
|
2498
|
+
return _nse_mcp_records(payload, "data")
|
|
2499
|
+
|
|
2500
|
+
|
|
2501
|
+
def nse_mcp_get_index_snapshot(date="today", filter=""):
|
|
2502
|
+
"""Get end-of-day values (OHLC, % change, turnover, P/E, P/B, dividend
|
|
2503
|
+
yield) for NSE indices on a date, optionally filtered by a name
|
|
2504
|
+
substring. Backed by NSE's own official no-auth MCP server (bhavcopy)."""
|
|
2505
|
+
payload = nse_mcp_call("bhavcopy", "get_index_snapshot", date=date, filter=filter)
|
|
2506
|
+
return _nse_mcp_records(payload, "indices")
|
|
2507
|
+
|
|
2508
|
+
|
|
2509
|
+
def nse_mcp_search_symbols(query):
|
|
2510
|
+
"""Search for NSE stock symbols by company name or partial symbol,
|
|
2511
|
+
returning matches with latest close price and % change. Backed by
|
|
2512
|
+
NSE's own official no-auth MCP server (bhavcopy)."""
|
|
2513
|
+
payload = nse_mcp_call("bhavcopy", "search_symbols", query=query)
|
|
2514
|
+
return _nse_mcp_records(payload, "results")
|
|
2515
|
+
|
|
2516
|
+
|
|
2517
|
+
def nse_mcp_get_stock_vs_index(symbol, index_name="Nifty 50", months=12, date="today"):
|
|
2518
|
+
"""Compare one NSE stock against a benchmark index over a period:
|
|
2519
|
+
return of each, outperformance, beta and correlation (stock return is
|
|
2520
|
+
already corporate-action adjusted). Backed by NSE's own official
|
|
2521
|
+
no-auth MCP server (bhavcopy)."""
|
|
2522
|
+
return nse_mcp_call(
|
|
2523
|
+
"bhavcopy", "get_stock_vs_index",
|
|
2524
|
+
symbol=symbol, indexName=index_name, months=months, date=date,
|
|
2525
|
+
)
|
|
2526
|
+
|
|
2527
|
+
|
|
2528
|
+
def nse_mcp_compare_stocks(symbols, months=6):
|
|
2529
|
+
"""Compare up to 10 NSE stocks side by side over a period: % return
|
|
2530
|
+
(ranked best to worst) and max drawdown per stock. Backed by NSE's own
|
|
2531
|
+
official no-auth MCP server (bhavcopy)."""
|
|
2532
|
+
payload = nse_mcp_call("bhavcopy", "compare_stocks", symbols=symbols, months=months)
|
|
2533
|
+
return _nse_mcp_records(payload, "stocks")
|
|
2534
|
+
|
|
2535
|
+
|
|
2536
|
+
def nse_mcp_get_index_history(index_name, months=3, end_date="today"):
|
|
2537
|
+
"""Get daily history (OHLC, % change, turnover, P/E, P/B, dividend
|
|
2538
|
+
yield) for an NSE index, up to 12 months per call; chain calls using
|
|
2539
|
+
next_end_date for longer periods. Backed by NSE's own official no-auth
|
|
2540
|
+
MCP server (bhavcopy)."""
|
|
2541
|
+
payload = nse_mcp_call(
|
|
2542
|
+
"bhavcopy", "get_index_history", indexName=index_name, months=months, endDate=end_date
|
|
2543
|
+
)
|
|
2544
|
+
return _nse_mcp_records(payload, "data")
|
|
2545
|
+
|
|
2546
|
+
|
|
2547
|
+
def nse_mcp_moving_average(symbol, days=20):
|
|
2548
|
+
"""Calculate the simple moving average (SMA) of close prices for an
|
|
2549
|
+
NSE stock over the last N trading days. Backed by NSE's own official
|
|
2550
|
+
no-auth MCP server (bhavcopy)."""
|
|
2551
|
+
return nse_mcp_call("bhavcopy", "moving_average", symbol=symbol, days=days)
|
|
2552
|
+
|
|
2553
|
+
|
|
2554
|
+
def nse_mcp_get_52_week_high_low(symbol):
|
|
2555
|
+
"""Get the 52-week high/low for an NSE stock, with dates and the
|
|
2556
|
+
current price's position within that range. Backed by NSE's own
|
|
2557
|
+
official no-auth MCP server (bhavcopy)."""
|
|
2558
|
+
return nse_mcp_call("bhavcopy", "get_52_week_high_low", symbol=symbol)
|
|
2559
|
+
|
|
2560
|
+
|
|
2561
|
+
def nse_mcp_get_index_performance(index_name, date="today"):
|
|
2562
|
+
"""Get an NSE index's price performance: 1-day change plus 1W/1M/3M/6M/
|
|
2563
|
+
1Y/2Y returns and 52-week high/low with distances. Backed by NSE's own
|
|
2564
|
+
official no-auth MCP server (bhavcopy)."""
|
|
2565
|
+
return nse_mcp_call("bhavcopy", "get_index_performance", indexName=index_name, date=date)
|
|
2566
|
+
|
|
2567
|
+
|
|
2568
|
+
# ---------------------------------------------------------------------------
|
|
2569
|
+
# Named wrappers -- "cmmkt" server (cm-market-mcp, 15 tools)
|
|
2570
|
+
# ---------------------------------------------------------------------------
|
|
2571
|
+
|
|
2572
|
+
def nse_mcp_cm_get_live_market_data(index="gainers"):
|
|
2573
|
+
"""Get live NSE market data for 'gainers' or 'loosers' (NSE's own
|
|
2574
|
+
spelling), refreshed every 5 minutes. Backed by NSE's own official
|
|
2575
|
+
no-auth MCP server (cmmkt)."""
|
|
2576
|
+
return nse_mcp_call("cmmkt", "cm_get_live_market_data", index=index)
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
def nse_mcp_cm_get_equity_stocks(limit=100, symbol_filter=""):
|
|
2580
|
+
"""Get latest live data for NSE Capital Market EQUITY-segment stocks
|
|
2581
|
+
(series EQ/BE/BL/BT/IL/IQ), refreshed every minute. Backed by NSE's own
|
|
2582
|
+
official no-auth MCP server (cmmkt)."""
|
|
2583
|
+
payload = nse_mcp_call(
|
|
2584
|
+
"cmmkt", "cm_get_equity_stocks", limit=limit, symbolFilter=symbol_filter
|
|
2585
|
+
)
|
|
2586
|
+
return _nse_mcp_records(payload, "stocks")
|
|
2587
|
+
|
|
2588
|
+
|
|
2589
|
+
def nse_mcp_nse_get_losers(limit=10):
|
|
2590
|
+
"""Get the top N NSE stocks by % loss, flattened across all indices and
|
|
2591
|
+
sorted ascending. Backed by NSE's own official no-auth MCP server
|
|
2592
|
+
(cmmkt). NOTE: as of this writing NSE's own server has a live bug on
|
|
2593
|
+
this specific tool (confirmed: an internal "ArrayList cannot be cast to
|
|
2594
|
+
Map" exception) -- this raises NSEEndpointError until NSE fixes it; use
|
|
2595
|
+
nse_mcp_nse_get_market_movers() for the same ranking in the meantime."""
|
|
2596
|
+
payload = nse_mcp_call("cmmkt", "nse_get_losers", limit=limit)
|
|
2597
|
+
return _nse_mcp_records(payload, "losers") if isinstance(payload, dict) else payload
|
|
2598
|
+
|
|
2599
|
+
|
|
2600
|
+
def nse_mcp_cm_get_call_auction_stocks(limit=100, symbol_filter=""):
|
|
2601
|
+
"""Get latest live data for NSE Call Auction session stocks (series
|
|
2602
|
+
CA/CB), refreshed every minute. Backed by NSE's own official no-auth
|
|
2603
|
+
MCP server (cmmkt)."""
|
|
2604
|
+
payload = nse_mcp_call(
|
|
2605
|
+
"cmmkt", "cm_get_call_auction_stocks", limit=limit, symbolFilter=symbol_filter
|
|
2606
|
+
)
|
|
2607
|
+
return _nse_mcp_records(payload, "stocks")
|
|
2608
|
+
|
|
2609
|
+
|
|
2610
|
+
def nse_mcp_cm_get_bond_stocks(limit=100, symbol_filter=""):
|
|
2611
|
+
"""Get latest live data for NSE BONDS/debt instrument series, refreshed
|
|
2612
|
+
every minute. Backed by NSE's own official no-auth MCP server (cmmkt)."""
|
|
2613
|
+
payload = nse_mcp_call(
|
|
2614
|
+
"cmmkt", "cm_get_bond_stocks", limit=limit, symbolFilter=symbol_filter
|
|
2615
|
+
)
|
|
2616
|
+
return _nse_mcp_records(payload, "stocks")
|
|
2617
|
+
|
|
2618
|
+
|
|
2619
|
+
def nse_mcp_cm_get_live_gainers():
|
|
2620
|
+
"""Return raw NSE gainers data grouped by index segment (NIFTY,
|
|
2621
|
+
BANKNIFTY, NIFTYNEXT50, allSec, etc.) -- not sorted by % change; use
|
|
2622
|
+
nse_mcp_nse_get_market_movers() for a sorted ranking instead. Backed by
|
|
2623
|
+
NSE's own official no-auth MCP server (cmmkt)."""
|
|
2624
|
+
return nse_mcp_call("cmmkt", "cm_get_live_gainers")
|
|
2625
|
+
|
|
2626
|
+
|
|
2627
|
+
def nse_mcp_nse_get_gainers(limit=10):
|
|
2628
|
+
"""Get the top N NSE stocks by % gain, flattened across all indices and
|
|
2629
|
+
sorted descending. Backed by NSE's own official no-auth MCP server
|
|
2630
|
+
(cmmkt). NOTE: as of this writing NSE's own server has a live bug on
|
|
2631
|
+
this specific tool (confirmed: an internal "ArrayList cannot be cast to
|
|
2632
|
+
Map" exception) -- this raises NSEEndpointError until NSE fixes it; use
|
|
2633
|
+
nse_mcp_nse_get_market_movers() for the same ranking in the meantime."""
|
|
2634
|
+
payload = nse_mcp_call("cmmkt", "nse_get_gainers", limit=limit)
|
|
2635
|
+
return _nse_mcp_records(payload, "gainers") if isinstance(payload, dict) else payload
|
|
2636
|
+
|
|
2637
|
+
|
|
2638
|
+
def nse_mcp_cm_get_data_status():
|
|
2639
|
+
"""Check freshness of NSE live gainers/losers market data (last crawl
|
|
2640
|
+
time, crawl interval, Redis TTL). Backed by NSE's own official no-auth
|
|
2641
|
+
MCP server (cmmkt)."""
|
|
2642
|
+
return nse_mcp_call("cmmkt", "cm_get_data_status")
|
|
2643
|
+
|
|
2644
|
+
|
|
2645
|
+
def nse_mcp_cm_get_stock_quote(symbol):
|
|
2646
|
+
"""Get the latest live quote for one NSE CM stock by exact symbol
|
|
2647
|
+
(works for equity, SME, bond or call-auction segments). Backed by
|
|
2648
|
+
NSE's own official no-auth MCP server (cmmkt)."""
|
|
2649
|
+
return nse_mcp_call("cmmkt", "cm_get_stock_quote", symbol=symbol)
|
|
2650
|
+
|
|
2651
|
+
|
|
2652
|
+
def nse_mcp_cm_get_index_quote(index_name):
|
|
2653
|
+
"""Get the full live quote for one NSE index by exact name: last
|
|
2654
|
+
value, change, day's OHLC, 52-week range, and 1W/1M/1Y comparisons.
|
|
2655
|
+
Backed by NSE's own official no-auth MCP server (cmmkt)."""
|
|
2656
|
+
return nse_mcp_call("cmmkt", "cm_get_index_quote", indexName=index_name)
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
def nse_mcp_cm_get_sme_stocks(limit=100, symbol_filter=""):
|
|
2660
|
+
"""Get latest live data for NSE SME (Small & Medium Enterprises) stocks
|
|
2661
|
+
(series SM/ST), refreshed every minute. Backed by NSE's own official
|
|
2662
|
+
no-auth MCP server (cmmkt)."""
|
|
2663
|
+
payload = nse_mcp_call(
|
|
2664
|
+
"cmmkt", "cm_get_sme_stocks", limit=limit, symbolFilter=symbol_filter
|
|
2665
|
+
)
|
|
2666
|
+
return _nse_mcp_records(payload, "stocks")
|
|
2667
|
+
|
|
2668
|
+
|
|
2669
|
+
def nse_mcp_cm_get_live_losers():
|
|
2670
|
+
"""Return raw NSE losers data grouped by index segment (NIFTY,
|
|
2671
|
+
BANKNIFTY, NIFTYNEXT50, allSec, etc.) -- not sorted by % change; use
|
|
2672
|
+
nse_mcp_nse_get_market_movers() for a sorted ranking instead. Backed by
|
|
2673
|
+
NSE's own official no-auth MCP server (cmmkt)."""
|
|
2674
|
+
return nse_mcp_call("cmmkt", "cm_get_live_losers")
|
|
2675
|
+
|
|
2676
|
+
|
|
2677
|
+
def nse_mcp_cm_get_live_indices(group="", name_filter=""):
|
|
2678
|
+
"""Get the latest live values of NSE indices (last, previous close,
|
|
2679
|
+
change, day's OHLC) across six groups (derivatives/broad/sectoral/
|
|
2680
|
+
strategy/thematic/fixed_income), optionally filtered by group and/or a
|
|
2681
|
+
name substring. Backed by NSE's own official no-auth MCP server
|
|
2682
|
+
(cmmkt)."""
|
|
2683
|
+
return nse_mcp_call("cmmkt", "cm_get_live_indices", group=group, nameFilter=name_filter)
|
|
2684
|
+
|
|
2685
|
+
|
|
2686
|
+
def nse_mcp_nse_get_market_movers(index_name=None, limit=10):
|
|
2687
|
+
"""Get the top N gainers and top N losers (sorted) from all NSE
|
|
2688
|
+
securities, or filtered to one of NIFTY/BANKNIFTY/NIFTYNEXT50. The
|
|
2689
|
+
PRIMARY tool for "top gainers/losers today" style questions. Backed by
|
|
2690
|
+
NSE's own official no-auth MCP server (cmmkt). Returns the raw dict
|
|
2691
|
+
(both a 'gainers' and a 'losers' list) since the result isn't a single
|
|
2692
|
+
table."""
|
|
2693
|
+
return nse_mcp_call(
|
|
2694
|
+
"cmmkt", "nse_get_market_movers", indexName=index_name or "", limit=limit
|
|
2695
|
+
)
|
|
2696
|
+
|
|
2697
|
+
|
|
2698
|
+
def nse_mcp_cm_get_allstocks_status():
|
|
2699
|
+
"""Check freshness of NSE's all-stocks live data cache: last crawl
|
|
2700
|
+
time, availability, and segment-wise stock counts. Backed by NSE's own
|
|
2701
|
+
official no-auth MCP server (cmmkt)."""
|
|
2702
|
+
return nse_mcp_call("cmmkt", "cm_get_allstocks_status")
|