nsepython 2.98__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 +991 -47
- {nsepython-2.98.dist-info → nsepython-2.99.dist-info}/METADATA +1 -1
- nsepython-2.99.dist-info/RECORD +7 -0
- nsepython-2.98.dist-info/RECORD +0 -7
- {nsepython-2.98.dist-info → nsepython-2.99.dist-info}/WHEEL +0 -0
- {nsepython-2.98.dist-info → nsepython-2.99.dist-info}/licenses/LICENSE +0 -0
- {nsepython-2.98.dist-info → nsepython-2.99.dist-info}/top_level.txt +0 -0
nsepython/rahu.py
CHANGED
|
@@ -288,7 +288,18 @@ curl_headers = ''' -H "authority: beta.nseindia.com" -H "cache-control: max-age=
|
|
|
288
288
|
run_time=datetime.datetime.now()
|
|
289
289
|
|
|
290
290
|
#Constants
|
|
291
|
-
|
|
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']
|
|
292
303
|
|
|
293
304
|
def running_status():
|
|
294
305
|
start_now=datetime.datetime.now().replace(hour=9, minute=15, second=0, microsecond=0)
|
|
@@ -440,21 +451,47 @@ def oi_chain_builder(symbol,expiry="latest",oi_mode="full"):
|
|
|
440
451
|
|
|
441
452
|
def nse_quote_derivatives(symbol):
|
|
442
453
|
symbol = nsesymbolpurify(symbol)
|
|
443
|
-
|
|
444
|
-
|
|
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)
|
|
445
465
|
return payload
|
|
446
466
|
else:
|
|
447
467
|
return {"error": f"{symbol} is not in derivatives list."}
|
|
448
468
|
|
|
449
469
|
def nse_quote(symbol,section=""):
|
|
450
|
-
#https://forum.unofficed.com/t/nsetools-get-quote-is-not-fetching-delivery-data-and-delivery-can-you-include-this-as-part-of-feature-request/1115/4
|
|
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.
|
|
451
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()
|
|
452
489
|
|
|
453
490
|
if(section==""):
|
|
454
|
-
if any(x in
|
|
455
|
-
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolDerivativesData&symbol='+
|
|
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)
|
|
456
493
|
else:
|
|
457
|
-
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+
|
|
494
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol_u)
|
|
458
495
|
return payload
|
|
459
496
|
|
|
460
497
|
if(section=="trade_info"):
|
|
@@ -480,7 +517,14 @@ def nse_quote(symbol,section=""):
|
|
|
480
517
|
# - securityWiseDP.seriesRemarks: no equivalent field exists in
|
|
481
518
|
# the new response -> always None, same as it is for most
|
|
482
519
|
# symbols on the old route anyway.
|
|
483
|
-
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+
|
|
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
|
+
)
|
|
484
528
|
eq = payload['equityResponse'][0]
|
|
485
529
|
ob = eq.get('orderBook', {})
|
|
486
530
|
md = eq.get('metaData', {})
|
|
@@ -529,12 +573,49 @@ def nse_quote(symbol,section=""):
|
|
|
529
573
|
},
|
|
530
574
|
}
|
|
531
575
|
|
|
532
|
-
if(section
|
|
533
|
-
#
|
|
534
|
-
#
|
|
535
|
-
#
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
)
|
|
538
619
|
def nse_expirydetails(payload, i=0, symbol=None):
|
|
539
620
|
expiry_dates = []
|
|
540
621
|
if 'records' in payload:
|
|
@@ -581,17 +662,45 @@ def nse_expirydetails(payload, i=0, symbol=None):
|
|
|
581
662
|
date_today = run_time.date()
|
|
582
663
|
dte = (currentExpiry_dt - date_today).days
|
|
583
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
|
+
|
|
584
692
|
def pcr(payload, inp=0):
|
|
585
693
|
ce_oi = 0
|
|
586
694
|
pe_oi = 0
|
|
587
|
-
|
|
695
|
+
|
|
588
696
|
# Identify the data and expiry dates based on structure
|
|
589
697
|
if 'records' in payload:
|
|
590
698
|
# Legacy structure
|
|
591
699
|
data_list = payload['records']['data']
|
|
592
700
|
expiry_dates = payload['records']['expiryDates']
|
|
593
701
|
elif 'data' in payload:
|
|
594
|
-
# New structure
|
|
702
|
+
# New structure (covers BOTH the nested per-strike shape and the
|
|
703
|
+
# flat per-contract-leg shape -- see _pcr_entry_oi() above)
|
|
595
704
|
data_list = payload['data']
|
|
596
705
|
# Extract unique sorted expiry dates from data
|
|
597
706
|
unique_dates = set()
|
|
@@ -601,9 +710,16 @@ def pcr(payload, inp=0):
|
|
|
601
710
|
unique_dates.add(ed)
|
|
602
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"))
|
|
603
712
|
else:
|
|
604
|
-
#
|
|
605
|
-
#
|
|
606
|
-
|
|
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
|
+
)
|
|
607
723
|
|
|
608
724
|
if not expiry_dates or inp >= len(expiry_dates):
|
|
609
725
|
# Requested index is outside the current payload's scope.
|
|
@@ -611,8 +727,10 @@ def pcr(payload, inp=0):
|
|
|
611
727
|
symbol = payload.get('symbol') or payload.get('records', {}).get('symbol')
|
|
612
728
|
if not symbol and 'data' in payload and len(payload['data']) > 0:
|
|
613
729
|
first = payload['data'][0]
|
|
614
|
-
symbol = first.get('symbol') or
|
|
615
|
-
|
|
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
|
+
|
|
616
734
|
if symbol and inp > 0:
|
|
617
735
|
# Fetch all expiries to find the target one
|
|
618
736
|
all_expiries = expiry_list(symbol, type="list")
|
|
@@ -629,17 +747,16 @@ def pcr(payload, inp=0):
|
|
|
629
747
|
return 0.0
|
|
630
748
|
|
|
631
749
|
target_expiry = expiry_dates[inp]
|
|
632
|
-
|
|
750
|
+
|
|
633
751
|
found_data = False
|
|
634
752
|
for i in data_list:
|
|
635
753
|
curr_exp = i.get('expiryDate') or i.get('expiryDates')
|
|
636
754
|
if curr_exp == target_expiry:
|
|
637
755
|
found_data = True
|
|
638
756
|
try:
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
pe_oi += i['PE'].get('openInterest', 0)
|
|
757
|
+
c, p = _pcr_entry_oi(i)
|
|
758
|
+
ce_oi += c
|
|
759
|
+
pe_oi += p
|
|
643
760
|
except (KeyError, TypeError):
|
|
644
761
|
pass
|
|
645
762
|
|
|
@@ -663,13 +780,17 @@ def pcr(payload, inp=0):
|
|
|
663
780
|
#forum.unofficed.com/t/unable-to-find-nse-quote-meta-api/702/4
|
|
664
781
|
#Refer https://forum.unofficed.com/t/changed-the-nse-quote-ltp-function/1276
|
|
665
782
|
def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
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.
|
|
666
787
|
if(optionType!="-"):
|
|
667
788
|
payload = nse_quote_derivatives(symbol)
|
|
668
789
|
else:
|
|
669
|
-
if any(x in symbol for x in indices):
|
|
790
|
+
if any(x in symbol.upper() for x in indices):
|
|
670
791
|
payload = nse_quote_derivatives(symbol)
|
|
671
792
|
else:
|
|
672
|
-
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol)
|
|
793
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol.upper())
|
|
673
794
|
|
|
674
795
|
lastPrice = 0
|
|
675
796
|
|
|
@@ -763,19 +884,36 @@ def nse_quote_ltp(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
|
763
884
|
# print(nse_quote_ltp("RELIANCE","next","PE",2300))
|
|
764
885
|
|
|
765
886
|
def nse_quote_meta(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
887
|
+
# Round 3 bug fix: case-sensitive index routing (see nse_quote_ltp()).
|
|
766
888
|
if(optionType!="-"):
|
|
767
889
|
payload = nse_quote_derivatives(symbol)
|
|
768
890
|
else:
|
|
769
|
-
if any(x in symbol for x in indices):
|
|
891
|
+
if any(x in symbol.upper() for x in indices):
|
|
770
892
|
payload = nse_quote_derivatives(symbol)
|
|
771
893
|
else:
|
|
772
|
-
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol)
|
|
894
|
+
payload = nsefetch('https://www.nseindia.com/api/NextApi/apiClient/GetQuoteApi?functionName=getSymbolData&marketType=N&series=EQ&symbol='+symbol.upper())
|
|
773
895
|
|
|
774
896
|
metadata = {}
|
|
775
897
|
|
|
776
898
|
if(optionType=="-"):
|
|
777
899
|
if 'equityResponse' in payload and len(payload['equityResponse']) > 0:
|
|
778
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
|
+
}
|
|
779
917
|
return metadata
|
|
780
918
|
|
|
781
919
|
meta = "Options"
|
|
@@ -845,15 +983,113 @@ def nse_quote_meta(symbol,expiryDate="latest",optionType="-",strikePrice=0):
|
|
|
845
983
|
return metadata
|
|
846
984
|
|
|
847
985
|
def nse_optionchain_ltp(payload,strikePrice,optionType,inp=0,intent=""):
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
+
)
|
|
857
1093
|
|
|
858
1094
|
def nse_eq(symbol):
|
|
859
1095
|
symbol = nsesymbolpurify(symbol)
|
|
@@ -916,10 +1152,22 @@ def option_chain(symbol):
|
|
|
916
1152
|
return nse_optionchain_scrapper(symbol)
|
|
917
1153
|
|
|
918
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.
|
|
919
1162
|
if(type=="clearing"):
|
|
920
1163
|
payload = nsefetch('https://www.nseindia.com/api/holiday-master?type=clearing')
|
|
921
|
-
|
|
1164
|
+
elif(type=="trading"):
|
|
922
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
|
+
)
|
|
923
1171
|
return payload
|
|
924
1172
|
|
|
925
1173
|
def holiday_master(type="trading"):
|
|
@@ -1128,6 +1376,22 @@ def black_scholes_dexter(S0,X,t,σ="",r=10,q=0.0,td=365):
|
|
|
1128
1376
|
S0,X,σ,r,q,t = float(S0),float(X),float(σ/100),float(r/100),float(q/100),float(t/td)
|
|
1129
1377
|
#https://unofficed.com/black-scholes-model-options-calculator-google-sheet/
|
|
1130
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
|
+
|
|
1131
1395
|
d1 = (math.log(S0/X)+(r-q+0.5*σ**2)*t)/(σ*math.sqrt(t))
|
|
1132
1396
|
#stackoverflow.com/questions/34258537/python-typeerror-unsupported-operand-types-for-float-and-int
|
|
1133
1397
|
|
|
@@ -1304,7 +1568,6 @@ def derivative_history(symbol,start_date,end_date,instrumentType,expiry_date,str
|
|
|
1304
1568
|
|
|
1305
1569
|
|
|
1306
1570
|
def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
1307
|
-
if(end_date==""):end_date=end_date
|
|
1308
1571
|
# Same retirement as derivative_history_virgin()/equity_history_virgin()
|
|
1309
1572
|
# above -- /api/historical/* is gone, /api/historicalOR/* is the working
|
|
1310
1573
|
# replacement with an identical response shape.
|
|
@@ -1313,6 +1576,7 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
1313
1576
|
|
|
1314
1577
|
#print(payload)
|
|
1315
1578
|
|
|
1579
|
+
payload_data = None
|
|
1316
1580
|
for key, value in payload['expiryDatesByInstrument'].items():
|
|
1317
1581
|
if type.lower() == "options" and "OPT" in key:
|
|
1318
1582
|
payload_data = payload['expiryDatesByInstrument'][key]
|
|
@@ -1320,7 +1584,20 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
1320
1584
|
elif type.lower() == "futures" and "FUT" in key:
|
|
1321
1585
|
payload_data = payload['expiryDatesByInstrument'][key]
|
|
1322
1586
|
break
|
|
1323
|
-
|
|
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
|
+
|
|
1324
1601
|
# Convert start_date and end_date to datetime objects
|
|
1325
1602
|
start_date = datetime.datetime.strptime(start_date, "%d-%m-%Y")
|
|
1326
1603
|
end_date = datetime.datetime.strptime(end_date, "%d-%m-%Y")
|
|
@@ -1329,7 +1606,7 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
1329
1606
|
filtered_date_payload = []
|
|
1330
1607
|
|
|
1331
1608
|
# Initialize a flag to check if the first date after end_date has been added
|
|
1332
|
-
added_after_end_date = False
|
|
1609
|
+
added_after_end_date = False
|
|
1333
1610
|
|
|
1334
1611
|
# Iterate through date_payload and filter dates within the range
|
|
1335
1612
|
for date_str in payload_data:
|
|
@@ -1339,7 +1616,7 @@ def expiry_history(symbol,start_date="",end_date="",type="options"):
|
|
|
1339
1616
|
elif date_obj > end_date and not added_after_end_date:
|
|
1340
1617
|
filtered_date_payload.append(date_str)
|
|
1341
1618
|
added_after_end_date = True
|
|
1342
|
-
|
|
1619
|
+
|
|
1343
1620
|
return filtered_date_payload
|
|
1344
1621
|
|
|
1345
1622
|
# # Nifty Indicies Site
|
|
@@ -1592,8 +1869,13 @@ def nse_preopen(key="NIFTY",type="pandas"):
|
|
|
1592
1869
|
|
|
1593
1870
|
#By Avinash https://forum.unofficed.com/t/nsepython-documentation/376/102?u=dexter
|
|
1594
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.
|
|
1595
1877
|
preOpen_gainer=nse_preopen(key)
|
|
1596
|
-
return preOpen_gainer[preOpen_gainer['pChange'] >
|
|
1878
|
+
return preOpen_gainer[preOpen_gainer['pChange'] >filter],preOpen_gainer[preOpen_gainer['pChange'] <-filter]
|
|
1597
1879
|
|
|
1598
1880
|
# type = "securities"
|
|
1599
1881
|
# type = "etf"
|
|
@@ -1673,8 +1955,20 @@ def nse_largedeals_historical(from_date, to_date, mode="bulk_deals"):
|
|
|
1673
1955
|
#print(get_fao_participant_oi("04-06-2021"))
|
|
1674
1956
|
def get_fao_participant_oi(date):
|
|
1675
1957
|
date = date.replace("-","")
|
|
1676
|
-
|
|
1677
|
-
|
|
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]
|
|
1678
1972
|
return payload
|
|
1679
1973
|
|
|
1680
1974
|
#https://forum.unofficed.com/t/how-to-check-if-the-market-is-open-today-or-not/1268/1
|
|
@@ -1756,3 +2050,653 @@ def security_wise_archive(from_date, to_date, symbol, series="ALL"):
|
|
|
1756
2050
|
url = f"{base_url}?from={from_date}&to={to_date}&symbol={symbol.upper()}&type=priceVolumeDeliverable&series={series.upper()}"
|
|
1757
2051
|
payload = nsefetch(url)
|
|
1758
2052
|
return pd.DataFrame(payload['data'])
|
|
2053
|
+
|
|
2054
|
+
|
|
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")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
nsepython/__init__.py,sha256=_1YN_O25fj7GRUrOREinuxKiXiF0l4cs-Grzjec8mZg,41
|
|
2
|
+
nsepython/rahu.py,sha256=FTz7FfZOb2MilRtt1ENAxoHvFZtUC6J0A84mKQMuzis,124167
|
|
3
|
+
nsepython-2.99.dist-info/licenses/LICENSE,sha256=VXapH-nuSz68DG5ScURElxHkl1lKk4MOK9cgZCSRDXs,34502
|
|
4
|
+
nsepython-2.99.dist-info/METADATA,sha256=Ib_NPqQNPLNxP79vidhr1brGaj3SZbcXRL5mYUm3YZM,7533
|
|
5
|
+
nsepython-2.99.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
nsepython-2.99.dist-info/top_level.txt,sha256=-hTS08c-pS4rPpiuzLkamRsgHTq7t6BG0M5wnU-iPy4,10
|
|
7
|
+
nsepython-2.99.dist-info/RECORD,,
|
nsepython-2.98.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
nsepython/__init__.py,sha256=_1YN_O25fj7GRUrOREinuxKiXiF0l4cs-Grzjec8mZg,41
|
|
2
|
-
nsepython/rahu.py,sha256=-97HGlPHoWECZqwkYRisJdGrWR6XqZVOPLl5L-QOZY0,79833
|
|
3
|
-
nsepython-2.98.dist-info/licenses/LICENSE,sha256=VXapH-nuSz68DG5ScURElxHkl1lKk4MOK9cgZCSRDXs,34502
|
|
4
|
-
nsepython-2.98.dist-info/METADATA,sha256=_5Sg0radO7R0aziYEq0Usm6uy-_28hMepbMmtPxX8mg,7533
|
|
5
|
-
nsepython-2.98.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
-
nsepython-2.98.dist-info/top_level.txt,sha256=-hTS08c-pS4rPpiuzLkamRsgHTq7t6BG0M5wnU-iPy4,10
|
|
7
|
-
nsepython-2.98.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|