nseindiapy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nseindiapy-0.1.0/PKG-INFO +85 -0
- nseindiapy-0.1.0/README.md +58 -0
- nseindiapy-0.1.0/pyproject.toml +43 -0
- nseindiapy-0.1.0/src/nseindiapy/__init__.py +14 -0
- nseindiapy-0.1.0/src/nseindiapy/_http.py +41 -0
- nseindiapy-0.1.0/src/nseindiapy/_nse_session.py +60 -0
- nseindiapy-0.1.0/src/nseindiapy/_utils.py +29 -0
- nseindiapy-0.1.0/src/nseindiapy/client.py +75 -0
- nseindiapy-0.1.0/src/nseindiapy/historical.py +299 -0
- nseindiapy-0.1.0/src/nseindiapy/holidays.py +104 -0
- nseindiapy-0.1.0/src/nseindiapy/live.py +51 -0
- nseindiapy-0.1.0/src/nseindiapy/py.typed +0 -0
- nseindiapy-0.1.0/src/nseindiapy/reports.py +197 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nseindiapy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for NSE Indices (niftyindices.com) — historical data, live market data, daily snapshots, reports, and holiday calendar.
|
|
5
|
+
Keywords: nse,nifty,india,stock market,indices,finance
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
16
|
+
Requires-Dist: requests>=2.32
|
|
17
|
+
Requires-Dist: polars>=1.0
|
|
18
|
+
Requires-Dist: python-dateutil>=2.9
|
|
19
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
20
|
+
Requires-Dist: playwright>=1.40 ; extra == 'playwright'
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Project-URL: Homepage, https://skamalj.github.io/nseindiapy/
|
|
23
|
+
Project-URL: Repository, https://github.com/skamalj/nseindiapy
|
|
24
|
+
Project-URL: Documentation, https://skamalj.github.io/nseindiapy/
|
|
25
|
+
Provides-Extra: playwright
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# nseindiapy
|
|
29
|
+
|
|
30
|
+
Python client for **NSE Indices** ([niftyindices.com](https://www.niftyindices.com)) — historical OHLC, TRI, P/E-P/B, India VIX, live index snapshot, daily snapshots, monthly reports, and the trading holiday calendar. All tabular data returned as [Polars](https://pola.rs/) DataFrames.
|
|
31
|
+
|
|
32
|
+
📖 **[Full documentation](https://skamalj.github.io/nseindiapy/)**
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install nseindiapy
|
|
38
|
+
# or
|
|
39
|
+
uv add nseindiapy
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from datetime import date
|
|
46
|
+
from nseindiapy import NiftyIndicesClient
|
|
47
|
+
|
|
48
|
+
client = NiftyIndicesClient()
|
|
49
|
+
|
|
50
|
+
# Historical OHLC — auto-paginates for ranges > 365 days
|
|
51
|
+
df = client.historical.price_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
52
|
+
|
|
53
|
+
# Total Return Index
|
|
54
|
+
tri = client.historical.tri_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
55
|
+
|
|
56
|
+
# P/E, P/B, Dividend Yield
|
|
57
|
+
pepb = client.historical.pepb_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
58
|
+
|
|
59
|
+
# India VIX (from daily snapshot CSV)
|
|
60
|
+
vix = client.historical.vix_history(date(2024, 1, 1), date(2024, 12, 31))
|
|
61
|
+
|
|
62
|
+
# Live indices (131 indices, CDN, no auth)
|
|
63
|
+
live = client.live.live_indices()
|
|
64
|
+
|
|
65
|
+
# Daily closing snapshot (all indices)
|
|
66
|
+
snap = client.reports.daily_snapshot(date.today())
|
|
67
|
+
|
|
68
|
+
# Holiday calendar
|
|
69
|
+
holidays = client.holidays.get_holidays(2026)
|
|
70
|
+
trading = client.holidays.trading_days(date(2026, 1, 1), date(2026, 3, 31))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
- **Single entry point** — `NiftyIndicesClient` with four sub-modules
|
|
76
|
+
- **Auto-pagination** — 365-day API limit handled transparently
|
|
77
|
+
- **Session seeding** — ASP.NET cookie seeded automatically; no manual setup needed
|
|
78
|
+
- **Polars DataFrames** — typed columns, `pl.Date` dates, `pl.Float64` numerics
|
|
79
|
+
- **No API key** — all endpoints are public
|
|
80
|
+
|
|
81
|
+
## Links
|
|
82
|
+
|
|
83
|
+
- [Documentation](https://skamalj.github.io/nseindiapy/)
|
|
84
|
+
- [PyPI](https://pypi.org/project/nseindiapy/)
|
|
85
|
+
- [GitHub](https://github.com/skamalj/nseindiapy)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# nseindiapy
|
|
2
|
+
|
|
3
|
+
Python client for **NSE Indices** ([niftyindices.com](https://www.niftyindices.com)) — historical OHLC, TRI, P/E-P/B, India VIX, live index snapshot, daily snapshots, monthly reports, and the trading holiday calendar. All tabular data returned as [Polars](https://pola.rs/) DataFrames.
|
|
4
|
+
|
|
5
|
+
📖 **[Full documentation](https://skamalj.github.io/nseindiapy/)**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install nseindiapy
|
|
11
|
+
# or
|
|
12
|
+
uv add nseindiapy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from datetime import date
|
|
19
|
+
from nseindiapy import NiftyIndicesClient
|
|
20
|
+
|
|
21
|
+
client = NiftyIndicesClient()
|
|
22
|
+
|
|
23
|
+
# Historical OHLC — auto-paginates for ranges > 365 days
|
|
24
|
+
df = client.historical.price_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
25
|
+
|
|
26
|
+
# Total Return Index
|
|
27
|
+
tri = client.historical.tri_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
28
|
+
|
|
29
|
+
# P/E, P/B, Dividend Yield
|
|
30
|
+
pepb = client.historical.pepb_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
31
|
+
|
|
32
|
+
# India VIX (from daily snapshot CSV)
|
|
33
|
+
vix = client.historical.vix_history(date(2024, 1, 1), date(2024, 12, 31))
|
|
34
|
+
|
|
35
|
+
# Live indices (131 indices, CDN, no auth)
|
|
36
|
+
live = client.live.live_indices()
|
|
37
|
+
|
|
38
|
+
# Daily closing snapshot (all indices)
|
|
39
|
+
snap = client.reports.daily_snapshot(date.today())
|
|
40
|
+
|
|
41
|
+
# Holiday calendar
|
|
42
|
+
holidays = client.holidays.get_holidays(2026)
|
|
43
|
+
trading = client.holidays.trading_days(date(2026, 1, 1), date(2026, 3, 31))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Single entry point** — `NiftyIndicesClient` with four sub-modules
|
|
49
|
+
- **Auto-pagination** — 365-day API limit handled transparently
|
|
50
|
+
- **Session seeding** — ASP.NET cookie seeded automatically; no manual setup needed
|
|
51
|
+
- **Polars DataFrames** — typed columns, `pl.Date` dates, `pl.Float64` numerics
|
|
52
|
+
- **No API key** — all endpoints are public
|
|
53
|
+
|
|
54
|
+
## Links
|
|
55
|
+
|
|
56
|
+
- [Documentation](https://skamalj.github.io/nseindiapy/)
|
|
57
|
+
- [PyPI](https://pypi.org/project/nseindiapy/)
|
|
58
|
+
- [GitHub](https://github.com/skamalj/nseindiapy)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nseindiapy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for NSE Indices (niftyindices.com) — historical data, live market data, daily snapshots, reports, and holiday calendar."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
keywords = ["nse", "nifty", "india", "stock market", "indices", "finance"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Office/Business :: Financial",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests>=2.32",
|
|
22
|
+
"polars>=1.0",
|
|
23
|
+
"python-dateutil>=2.9",
|
|
24
|
+
"beautifulsoup4>=4.12",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
playwright = ["playwright>=1.40"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://skamalj.github.io/nseindiapy/"
|
|
32
|
+
Repository = "https://github.com/skamalj/nseindiapy"
|
|
33
|
+
Documentation = "https://skamalj.github.io/nseindiapy/"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.11.15,<0.12.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"mkdocs-material>=9.5",
|
|
42
|
+
"mkdocstrings[python]>=0.25",
|
|
43
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""nseindia — Python client for NSE Indices (niftyindices.com)."""
|
|
2
|
+
from .client import NiftyIndicesClient
|
|
3
|
+
from .historical import HistoricalData
|
|
4
|
+
from .holidays import Holidays
|
|
5
|
+
from .live import LiveMarket
|
|
6
|
+
from .reports import Reports
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"NiftyIndicesClient",
|
|
10
|
+
"HistoricalData",
|
|
11
|
+
"LiveMarket",
|
|
12
|
+
"Reports",
|
|
13
|
+
"Holidays",
|
|
14
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
BASE_URL = "https://www.niftyindices.com"
|
|
6
|
+
BLOB_URL = "https://iislliveblob.niftyindices.com"
|
|
7
|
+
|
|
8
|
+
# X-Requested-With is the jQuery AJAX marker required by ASP.NET WebForms.
|
|
9
|
+
# Without it the POST endpoints may be blocked at WAF/proxy level.
|
|
10
|
+
_HEADERS = {
|
|
11
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
12
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
13
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
14
|
+
"Referer": "https://www.niftyindices.com/reports/historical-data",
|
|
15
|
+
"Origin": "https://www.niftyindices.com",
|
|
16
|
+
"User-Agent": (
|
|
17
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
18
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
19
|
+
"Chrome/125.0.0.0 Safari/537.36"
|
|
20
|
+
),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_SEED_URL = BASE_URL + "/reports/historical-data"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_session(seed: bool = True) -> requests.Session:
|
|
27
|
+
"""Create a requests.Session pre-configured for niftyindices.com.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
seed: If ``True`` (default), performs a GET to the historical-data page
|
|
31
|
+
to establish an ASP.NET session cookie before any POST calls.
|
|
32
|
+
This bypasses proxy-level blocks that check for a valid session.
|
|
33
|
+
"""
|
|
34
|
+
session = requests.Session()
|
|
35
|
+
session.headers.update(_HEADERS)
|
|
36
|
+
if seed:
|
|
37
|
+
try:
|
|
38
|
+
session.get(_SEED_URL, timeout=20)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass # network errors handled at call site
|
|
41
|
+
return session
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""NSE India session helper — establishes the cookie session required by nseindia.com APIs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
_NSE_HOME = "https://www.nseindia.com"
|
|
8
|
+
_NSE_VIX_PAGE = "https://www.nseindia.com/market-data/india-vix"
|
|
9
|
+
|
|
10
|
+
_HEADERS = {
|
|
11
|
+
"User-Agent": (
|
|
12
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
13
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
14
|
+
"Chrome/124.0.0.0 Safari/537.36"
|
|
15
|
+
),
|
|
16
|
+
"Accept": (
|
|
17
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,"
|
|
18
|
+
"image/avif,image/webp,image/apng,*/*;q=0.8"
|
|
19
|
+
),
|
|
20
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
21
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
22
|
+
"Connection": "keep-alive",
|
|
23
|
+
"Upgrade-Insecure-Requests": "1",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_API_HEADERS = {
|
|
27
|
+
"Accept": "application/json, text/plain, */*",
|
|
28
|
+
"Referer": _NSE_VIX_PAGE,
|
|
29
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def make_nse_session() -> requests.Session:
|
|
34
|
+
"""Return a requests.Session primed with NSE India cookies.
|
|
35
|
+
|
|
36
|
+
NSE India gates its JSON APIs behind a session cookie. This function:
|
|
37
|
+
1. Visits the homepage to seed the initial cookie
|
|
38
|
+
2. Visits the India VIX page to establish the correct Referer context
|
|
39
|
+
3. Returns the session with API-specific headers set
|
|
40
|
+
|
|
41
|
+
Note: NSE India uses JavaScript-based bot detection. If the session cannot
|
|
42
|
+
be established (e.g. 503 from a restricted network), the VIX API will raise
|
|
43
|
+
an ``HTTPError``. In that case the data is unavailable from this environment.
|
|
44
|
+
"""
|
|
45
|
+
session = requests.Session()
|
|
46
|
+
session.headers.update(_HEADERS)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# Step 1: seed cookies from homepage
|
|
50
|
+
session.get(_NSE_HOME, timeout=20)
|
|
51
|
+
time.sleep(0.5)
|
|
52
|
+
# Step 2: visit VIX page for Referer context
|
|
53
|
+
session.get(_NSE_VIX_PAGE, timeout=20)
|
|
54
|
+
time.sleep(0.3)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass # network issues handled at call site
|
|
57
|
+
|
|
58
|
+
# Switch to JSON-API headers for subsequent API calls
|
|
59
|
+
session.headers.update(_API_HEADERS)
|
|
60
|
+
return session
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date, timedelta
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from dateutil.relativedelta import relativedelta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fmt_date(d: date) -> str:
|
|
11
|
+
return d.strftime("%d %b %Y")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_response(resp: requests.Response) -> list[dict]:
|
|
15
|
+
outer = resp.json()
|
|
16
|
+
inner = outer.get("d", "[]")
|
|
17
|
+
if not isinstance(inner, str):
|
|
18
|
+
return inner
|
|
19
|
+
if inner.lower() in ("false", "[]", ""):
|
|
20
|
+
return []
|
|
21
|
+
return json.loads(inner)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def date_chunks(start: date, end: date, months: int = 12):
|
|
25
|
+
current = start
|
|
26
|
+
while current < end:
|
|
27
|
+
chunk_end = min(current + relativedelta(months=months) - timedelta(days=1), end)
|
|
28
|
+
yield current, chunk_end
|
|
29
|
+
current = chunk_end + timedelta(days=1)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Top-level client for niftyindices.com."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ._http import make_session
|
|
5
|
+
from .historical import HistoricalData
|
|
6
|
+
from .holidays import Holidays
|
|
7
|
+
from .live import LiveMarket
|
|
8
|
+
from .reports import Reports
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NiftyIndicesClient:
|
|
12
|
+
"""Unified client for niftyindices.com data.
|
|
13
|
+
|
|
14
|
+
All network calls share a single ``requests.Session`` with the required
|
|
15
|
+
headers pre-configured.
|
|
16
|
+
|
|
17
|
+
Sub-modules:
|
|
18
|
+
- ``client.historical`` — price OHLC, TRI, P/E-P/B, India VIX (via NSE India),
|
|
19
|
+
index type/category/name discovery
|
|
20
|
+
- ``client.live`` — live index snapshot (CDN, no auth required)
|
|
21
|
+
- ``client.reports`` — daily snapshots CSV, monthly PDFs/ZIPs, static resources
|
|
22
|
+
- ``client.holidays`` — holiday calendar (HTML scrape), trading day utilities
|
|
23
|
+
|
|
24
|
+
Example::
|
|
25
|
+
|
|
26
|
+
from datetime import date
|
|
27
|
+
from nseindia import NiftyIndicesClient
|
|
28
|
+
|
|
29
|
+
client = NiftyIndicesClient()
|
|
30
|
+
|
|
31
|
+
# Historical OHLC (auto-paginates > 365 days)
|
|
32
|
+
df = client.historical.price_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
33
|
+
|
|
34
|
+
# TRI and P/E-P/B
|
|
35
|
+
tri = client.historical.tri_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
36
|
+
pepb = client.historical.pepb_history("NIFTY 50", date(2024, 1, 1), date(2024, 12, 31))
|
|
37
|
+
|
|
38
|
+
# India VIX (fetched from NSE India, session cookie handled automatically)
|
|
39
|
+
vix = client.historical.vix_history(date(2024, 1, 1), date(2024, 12, 31))
|
|
40
|
+
|
|
41
|
+
# Index discovery
|
|
42
|
+
types = client.historical.list_index_types()
|
|
43
|
+
cats = client.historical.list_sub_categories("Equity")
|
|
44
|
+
names = client.historical.list_indices("Broad Market Indices")
|
|
45
|
+
|
|
46
|
+
# Live index snapshot (CDN, ~15-30s refresh during market hours)
|
|
47
|
+
live = client.live.live_indices()
|
|
48
|
+
|
|
49
|
+
# Daily closing CSV
|
|
50
|
+
snap = client.reports.daily_snapshot(date.today())
|
|
51
|
+
|
|
52
|
+
# Monthly reports
|
|
53
|
+
client.reports.download_monthly(2026, 5, dest_dir="./data")
|
|
54
|
+
|
|
55
|
+
# Holiday calendar
|
|
56
|
+
holidays = client.holidays.get_holidays(2026)
|
|
57
|
+
trading = client.holidays.trading_days(date(2026, 1, 1), date(2026, 3, 31))
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self._session = make_session()
|
|
62
|
+
self.historical = HistoricalData(self._session)
|
|
63
|
+
self.live = LiveMarket(self._session)
|
|
64
|
+
self.reports = Reports(self._session)
|
|
65
|
+
self.holidays = Holidays(self._session)
|
|
66
|
+
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
"""Close the underlying HTTP session."""
|
|
69
|
+
self._session.close()
|
|
70
|
+
|
|
71
|
+
def __enter__(self) -> "NiftyIndicesClient":
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __exit__(self, *_: object) -> None:
|
|
75
|
+
self.close()
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Historical index data — price OHLC, TRI, P/E-P/B, India VIX."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import time
|
|
6
|
+
from datetime import date, timedelta
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from ._http import BASE_URL, BLOB_URL
|
|
12
|
+
from ._utils import date_chunks, fmt_date, parse_response
|
|
13
|
+
|
|
14
|
+
_ENDPOINT = BASE_URL + "/Backpage.aspx/{method}"
|
|
15
|
+
_SNAPSHOT_URL = BASE_URL + "/Daily_Snapshot/ind_close_all_{date}.csv"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HistoricalData:
|
|
19
|
+
def __init__(self, session: requests.Session) -> None:
|
|
20
|
+
self._s = session
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------------
|
|
23
|
+
# Metadata
|
|
24
|
+
# ------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
def get_index_mapping(self) -> list[dict]:
|
|
27
|
+
"""Fetch the index name mapping table from the CDN.
|
|
28
|
+
|
|
29
|
+
The file is served with a UTF-8 BOM — decoded automatically.
|
|
30
|
+
"""
|
|
31
|
+
resp = self._s.get(
|
|
32
|
+
f"{BLOB_URL}/assets/json/IndexMapping.json", timeout=30
|
|
33
|
+
)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
import json
|
|
36
|
+
# CDN serves the file with a UTF-8 BOM (EF BB BF prefix)
|
|
37
|
+
text = resp.content.decode("utf-8-sig")
|
|
38
|
+
return json.loads(text)
|
|
39
|
+
|
|
40
|
+
def list_index_types(self) -> list[str]:
|
|
41
|
+
"""Return top-level index types: Equity, Fixed Income, Multi Asset."""
|
|
42
|
+
resp = self._s.post(
|
|
43
|
+
_ENDPOINT.format(method="gethistoricaltypedata1"),
|
|
44
|
+
json={"cinfo": {}},
|
|
45
|
+
timeout=30,
|
|
46
|
+
)
|
|
47
|
+
resp.raise_for_status()
|
|
48
|
+
return [r["indextype"] for r in parse_response(resp)]
|
|
49
|
+
|
|
50
|
+
def list_sub_categories(
|
|
51
|
+
self,
|
|
52
|
+
index_type: str = "Equity",
|
|
53
|
+
section: str = "Historical Index Data",
|
|
54
|
+
) -> list[str]:
|
|
55
|
+
"""List sub-categories for a given index type and section.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
index_type: One of ``"Equity"``, ``"Fixed Income"``, ``"Multi Asset"``.
|
|
59
|
+
section: ``"Historical Index Data"`` or ``"Total Returns Index Values"``.
|
|
60
|
+
"""
|
|
61
|
+
resp = self._s.post(
|
|
62
|
+
_ENDPOINT.format(method="gethistoricaltypeSubindexdata"),
|
|
63
|
+
json={"cinfo": {"indextype": index_type, "indexgroup": section}},
|
|
64
|
+
timeout=30,
|
|
65
|
+
)
|
|
66
|
+
resp.raise_for_status()
|
|
67
|
+
return [r["indextype"] for r in parse_response(resp)]
|
|
68
|
+
|
|
69
|
+
def list_indices(
|
|
70
|
+
self,
|
|
71
|
+
sub_category: str,
|
|
72
|
+
section: str = "Historical Index Data",
|
|
73
|
+
) -> list[str]:
|
|
74
|
+
"""List index names within a sub-category."""
|
|
75
|
+
resp = self._s.post(
|
|
76
|
+
_ENDPOINT.format(method="gethistoricaltypeindexdata"),
|
|
77
|
+
json={"cinfo": {"indextype": sub_category, "indexgroup": section}},
|
|
78
|
+
timeout=30,
|
|
79
|
+
)
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
return [r["indextype"] for r in parse_response(resp)]
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Internal helpers
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _post_data(
|
|
88
|
+
self,
|
|
89
|
+
method: str,
|
|
90
|
+
index_name: str,
|
|
91
|
+
start: date,
|
|
92
|
+
end: date,
|
|
93
|
+
) -> list[dict]:
|
|
94
|
+
payload = {
|
|
95
|
+
"cinfo": (
|
|
96
|
+
f"{{'name':'{index_name}',"
|
|
97
|
+
f"'startDate':'{fmt_date(start)}',"
|
|
98
|
+
f"'endDate':'{fmt_date(end)}',"
|
|
99
|
+
f"'indexName':'{index_name}'}}"
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
resp = self._s.post(
|
|
103
|
+
_ENDPOINT.format(method=method), json=payload, timeout=30
|
|
104
|
+
)
|
|
105
|
+
resp.raise_for_status()
|
|
106
|
+
return parse_response(resp)
|
|
107
|
+
|
|
108
|
+
def _paginated(
|
|
109
|
+
self,
|
|
110
|
+
method: str,
|
|
111
|
+
index_name: str,
|
|
112
|
+
start: date,
|
|
113
|
+
end: date,
|
|
114
|
+
delay: float = 0.5,
|
|
115
|
+
) -> list[dict]:
|
|
116
|
+
rows: list[dict] = []
|
|
117
|
+
for chunk_start, chunk_end in date_chunks(start, end, months=12):
|
|
118
|
+
rows.extend(self._post_data(method, index_name, chunk_start, chunk_end))
|
|
119
|
+
if chunk_end < end:
|
|
120
|
+
time.sleep(delay)
|
|
121
|
+
return rows
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
# Public data methods
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def price_history(
|
|
128
|
+
self, index_name: str, start: date, end: date
|
|
129
|
+
) -> pl.DataFrame:
|
|
130
|
+
"""Daily OHLC (Price Return Index) for any NSE index.
|
|
131
|
+
|
|
132
|
+
Paginates automatically for date ranges > 365 days.
|
|
133
|
+
Available from 1990. Date format is handled internally.
|
|
134
|
+
"""
|
|
135
|
+
rows = self._paginated(
|
|
136
|
+
"getHistoricaldatatabletoString", index_name.upper(), start, end
|
|
137
|
+
)
|
|
138
|
+
if not rows:
|
|
139
|
+
return pl.DataFrame(schema={
|
|
140
|
+
"index_name": pl.Utf8, "date": pl.Date,
|
|
141
|
+
"open": pl.Float64, "high": pl.Float64,
|
|
142
|
+
"low": pl.Float64, "close": pl.Float64,
|
|
143
|
+
})
|
|
144
|
+
df = pl.DataFrame(rows)
|
|
145
|
+
# Normalise whichever index-name field the API returns
|
|
146
|
+
for src in ("INDEX_NAME", "Index Name"):
|
|
147
|
+
if src in df.columns and "index_name" not in df.columns:
|
|
148
|
+
df = df.rename({src: "index_name"})
|
|
149
|
+
return (
|
|
150
|
+
df.rename({
|
|
151
|
+
"HistoricalDate": "date",
|
|
152
|
+
"OPEN": "open",
|
|
153
|
+
"HIGH": "high",
|
|
154
|
+
"LOW": "low",
|
|
155
|
+
"CLOSE": "close",
|
|
156
|
+
})
|
|
157
|
+
.with_columns([
|
|
158
|
+
pl.col("date").str.to_date("%d %b %Y"),
|
|
159
|
+
pl.col("open").cast(pl.Float64),
|
|
160
|
+
pl.col("high").cast(pl.Float64),
|
|
161
|
+
pl.col("low").cast(pl.Float64),
|
|
162
|
+
pl.col("close").cast(pl.Float64),
|
|
163
|
+
])
|
|
164
|
+
.select(["index_name", "date", "open", "high", "low", "close"])
|
|
165
|
+
.sort("date")
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def tri_history(
|
|
169
|
+
self, index_name: str, start: date, end: date
|
|
170
|
+
) -> pl.DataFrame:
|
|
171
|
+
"""Daily Total Return Index (TRI) values.
|
|
172
|
+
|
|
173
|
+
Gross TRI includes dividends reinvested. Net TRI (ntr) applies
|
|
174
|
+
withholding tax and is only available for NIFTY 50, NIFTY MIDCAP 50,
|
|
175
|
+
and NIFTY 500. Available from 1995.
|
|
176
|
+
"""
|
|
177
|
+
rows = self._paginated(
|
|
178
|
+
"getTotalReturnIndexString", index_name.upper(), start, end
|
|
179
|
+
)
|
|
180
|
+
if not rows:
|
|
181
|
+
return pl.DataFrame(schema={
|
|
182
|
+
"index_name": pl.Utf8, "date": pl.Date,
|
|
183
|
+
"tri": pl.Float64, "ntr": pl.Float64,
|
|
184
|
+
})
|
|
185
|
+
df = pl.DataFrame(rows)
|
|
186
|
+
rename_map: dict[str, str] = {}
|
|
187
|
+
for src, dst in [
|
|
188
|
+
("Index Name", "index_name"),
|
|
189
|
+
("Date", "date"),
|
|
190
|
+
("TotalReturnsIndex", "tri"),
|
|
191
|
+
("NTR_Value", "ntr"),
|
|
192
|
+
]:
|
|
193
|
+
if src in df.columns:
|
|
194
|
+
rename_map[src] = dst
|
|
195
|
+
df = df.rename(rename_map)
|
|
196
|
+
casts = [pl.col("date").str.to_date("%d %b %Y"), pl.col("tri").cast(pl.Float64)]
|
|
197
|
+
if "ntr" in df.columns:
|
|
198
|
+
casts.append(
|
|
199
|
+
pl.when(pl.col("ntr").str.strip_chars() == "")
|
|
200
|
+
.then(pl.lit(None))
|
|
201
|
+
.otherwise(pl.col("ntr"))
|
|
202
|
+
.cast(pl.Float64)
|
|
203
|
+
.alias("ntr")
|
|
204
|
+
)
|
|
205
|
+
keep = [c for c in ["index_name", "date", "tri", "ntr"] if c in df.columns]
|
|
206
|
+
return df.with_columns(casts).select(keep).sort("date")
|
|
207
|
+
|
|
208
|
+
def pepb_history(
|
|
209
|
+
self, index_name: str, start: date, end: date
|
|
210
|
+
) -> pl.DataFrame:
|
|
211
|
+
"""Daily P/E, P/B and Dividend Yield ratios. Available from 1996."""
|
|
212
|
+
rows = self._paginated(
|
|
213
|
+
"getpepbHistoricaldataDBtoString", index_name.upper(), start, end
|
|
214
|
+
)
|
|
215
|
+
if not rows:
|
|
216
|
+
return pl.DataFrame(schema={
|
|
217
|
+
"index_name": pl.Utf8, "date": pl.Date,
|
|
218
|
+
"pe": pl.Float64, "pb": pl.Float64, "div_yield": pl.Float64,
|
|
219
|
+
})
|
|
220
|
+
df = pl.DataFrame(rows)
|
|
221
|
+
return (
|
|
222
|
+
df.rename({k: v for k, v in {
|
|
223
|
+
"Index Name": "index_name",
|
|
224
|
+
"DATE": "date",
|
|
225
|
+
"divYield": "div_yield",
|
|
226
|
+
}.items() if k in df.columns})
|
|
227
|
+
.with_columns([
|
|
228
|
+
pl.col("date").str.to_date("%d %b %Y"),
|
|
229
|
+
pl.col("pe").cast(pl.Float64),
|
|
230
|
+
pl.col("pb").cast(pl.Float64),
|
|
231
|
+
pl.col("div_yield").cast(pl.Float64),
|
|
232
|
+
])
|
|
233
|
+
.select(["index_name", "date", "pe", "pb", "div_yield"])
|
|
234
|
+
.sort("date")
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def vix_history(self, start: date, end: date) -> pl.DataFrame:
|
|
238
|
+
"""Daily OHLC for India VIX, extracted from niftyindices.com daily snapshot CSVs.
|
|
239
|
+
|
|
240
|
+
The original ``Backpage.aspx/BindHistoricalIndiaVixData`` endpoint was
|
|
241
|
+
decommissioned. The Daily Snapshot CSV already contains India VIX as a
|
|
242
|
+
named row — no NSE India session or Playwright needed.
|
|
243
|
+
|
|
244
|
+
Verified live from ``ind_close_all_05062026.csv``::
|
|
245
|
+
|
|
246
|
+
India VIX,05-06-2026,15.885,16.37,13.46,15.79,-0.1,-.61,-,-,-,-,-
|
|
247
|
+
|
|
248
|
+
Columns returned: ``date``, ``open``, ``high``, ``low``, ``close``,
|
|
249
|
+
``points_change``, ``change_pct``.
|
|
250
|
+
|
|
251
|
+
Non-trading days (weekends / holidays) are skipped automatically —
|
|
252
|
+
those dates have no CSV file (HTTP 404).
|
|
253
|
+
"""
|
|
254
|
+
rows: list[dict] = []
|
|
255
|
+
dt = start
|
|
256
|
+
while dt <= end:
|
|
257
|
+
url = _SNAPSHOT_URL.format(date=dt.strftime("%d%m%Y"))
|
|
258
|
+
try:
|
|
259
|
+
resp = self._s.get(url, timeout=15)
|
|
260
|
+
if resp.status_code == 404:
|
|
261
|
+
dt += timedelta(days=1)
|
|
262
|
+
continue
|
|
263
|
+
resp.raise_for_status()
|
|
264
|
+
# Parse CSV; find "India VIX" row
|
|
265
|
+
df_snap = pl.read_csv(
|
|
266
|
+
io.BytesIO(resp.content),
|
|
267
|
+
encoding="utf8-lossy",
|
|
268
|
+
infer_schema_length=0,
|
|
269
|
+
)
|
|
270
|
+
df_snap = df_snap.rename({c: c.strip() for c in df_snap.columns})
|
|
271
|
+
vix_row = df_snap.filter(
|
|
272
|
+
pl.col("Index Name").str.strip_chars() == "India VIX"
|
|
273
|
+
)
|
|
274
|
+
if not vix_row.is_empty():
|
|
275
|
+
r = vix_row.row(0, named=True)
|
|
276
|
+
rows.append({
|
|
277
|
+
"date": dt,
|
|
278
|
+
"open": float(r.get("Open Index Value", "nan").replace(",", "") or "nan"),
|
|
279
|
+
"high": float(r.get("High Index Value", "nan").replace(",", "") or "nan"),
|
|
280
|
+
"low": float(r.get("Low Index Value", "nan").replace(",", "") or "nan"),
|
|
281
|
+
"close": float(r.get("Closing Index Value", "nan").replace(",", "") or "nan"),
|
|
282
|
+
"points_change": float((r.get("Points Change") or "nan").replace(",", "") or "nan"),
|
|
283
|
+
"change_pct": float((r.get("Change(%)") or "nan").replace(",", "") or "nan"),
|
|
284
|
+
})
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
dt += timedelta(days=1)
|
|
288
|
+
|
|
289
|
+
if not rows:
|
|
290
|
+
return pl.DataFrame(schema={
|
|
291
|
+
"date": pl.Date,
|
|
292
|
+
"open": pl.Float64, "high": pl.Float64,
|
|
293
|
+
"low": pl.Float64, "close": pl.Float64,
|
|
294
|
+
"points_change": pl.Float64, "change_pct": pl.Float64,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
return pl.DataFrame(rows).with_columns(
|
|
298
|
+
pl.col("date").cast(pl.Date)
|
|
299
|
+
).sort("date")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""NSE trading holiday calendar.
|
|
2
|
+
|
|
3
|
+
Note: ``POST /Backpage.aspx/getAllHolidays`` does not exist (HTTP 500,
|
|
4
|
+
"Unknown web method"). Holiday data is scraped from the HTML page at
|
|
5
|
+
``https://www.niftyindices.com/resources/holiday-calendar``.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import date, timedelta
|
|
10
|
+
|
|
11
|
+
import polars as pl
|
|
12
|
+
import requests
|
|
13
|
+
from bs4 import BeautifulSoup
|
|
14
|
+
|
|
15
|
+
from ._http import BASE_URL
|
|
16
|
+
|
|
17
|
+
_CALENDAR_URL = BASE_URL + "/resources/holiday-calendar"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _scrape_holidays(html: str) -> list[dict]:
|
|
21
|
+
"""Parse holiday rows from the holiday-calendar HTML page.
|
|
22
|
+
|
|
23
|
+
Actual page columns (verified June 2026):
|
|
24
|
+
SR. NO. | Date (DD/MM/YYYY) | Day | Occasion
|
|
25
|
+
"""
|
|
26
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
27
|
+
rows = []
|
|
28
|
+
for tr in soup.find_all("tr"):
|
|
29
|
+
tds = tr.find_all("td")
|
|
30
|
+
if len(tds) == 4:
|
|
31
|
+
sr_no = tds[0].get_text(strip=True)
|
|
32
|
+
trading_date = tds[1].get_text(strip=True)
|
|
33
|
+
week_day = tds[2].get_text(strip=True)
|
|
34
|
+
description = tds[3].get_text(strip=True)
|
|
35
|
+
# Skip header rows — sr_no must be numeric
|
|
36
|
+
if sr_no.isdigit() and trading_date:
|
|
37
|
+
rows.append({
|
|
38
|
+
"sr_no": int(sr_no),
|
|
39
|
+
"date_str": trading_date,
|
|
40
|
+
"week_day": week_day,
|
|
41
|
+
"description": description,
|
|
42
|
+
})
|
|
43
|
+
return rows
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Holidays:
|
|
47
|
+
def __init__(self, session: requests.Session) -> None:
|
|
48
|
+
self._s = session
|
|
49
|
+
|
|
50
|
+
def get_holidays(self, year: int) -> pl.DataFrame:
|
|
51
|
+
"""Fetch the NSE trading holiday list for *year* by scraping the HTML page.
|
|
52
|
+
|
|
53
|
+
Returns a DataFrame with columns:
|
|
54
|
+
``sr_no`` (int), ``date`` (Date), ``week_day`` (str), ``description`` (str).
|
|
55
|
+
"""
|
|
56
|
+
resp = self._s.get(_CALENDAR_URL, params={"year": str(year)}, timeout=20)
|
|
57
|
+
resp.raise_for_status()
|
|
58
|
+
raw_rows = _scrape_holidays(resp.text)
|
|
59
|
+
|
|
60
|
+
if not raw_rows:
|
|
61
|
+
return pl.DataFrame(schema={
|
|
62
|
+
"sr_no": pl.Int64,
|
|
63
|
+
"date": pl.Date,
|
|
64
|
+
"week_day": pl.Utf8,
|
|
65
|
+
"description": pl.Utf8,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
df = pl.DataFrame(raw_rows)
|
|
69
|
+
# Date strings on the page are "DD/MM/YYYY" format e.g. "26/01/2026"
|
|
70
|
+
df = df.with_columns(
|
|
71
|
+
pl.col("date_str").str.to_date("%d/%m/%Y", strict=False).alias("date")
|
|
72
|
+
).drop("date_str")
|
|
73
|
+
|
|
74
|
+
return df.drop_nulls(subset=["date"]).sort("date")
|
|
75
|
+
|
|
76
|
+
def holiday_dates(self, year: int) -> set[date]:
|
|
77
|
+
"""Return a set of holiday ``date`` objects for *year*."""
|
|
78
|
+
df = self.get_holidays(year)
|
|
79
|
+
if df.is_empty() or "date" not in df.columns:
|
|
80
|
+
return set()
|
|
81
|
+
return {d for d in df["date"].to_list() if d is not None}
|
|
82
|
+
|
|
83
|
+
def trading_days(self, start: date, end: date) -> list[date]:
|
|
84
|
+
"""Return all NSE trading days between *start* and *end* inclusive.
|
|
85
|
+
|
|
86
|
+
Fetches the holiday calendar for every calendar year spanned by the range.
|
|
87
|
+
"""
|
|
88
|
+
holidays: set[date] = set()
|
|
89
|
+
for y in range(start.year, end.year + 1):
|
|
90
|
+
holidays |= self.holiday_dates(y)
|
|
91
|
+
|
|
92
|
+
days: list[date] = []
|
|
93
|
+
dt = start
|
|
94
|
+
while dt <= end:
|
|
95
|
+
if dt.weekday() < 5 and dt not in holidays:
|
|
96
|
+
days.append(dt)
|
|
97
|
+
dt += timedelta(days=1)
|
|
98
|
+
return days
|
|
99
|
+
|
|
100
|
+
def is_trading_day(self, dt: date) -> bool:
|
|
101
|
+
"""Return ``True`` if *dt* is an NSE trading day."""
|
|
102
|
+
if dt.weekday() >= 5:
|
|
103
|
+
return False
|
|
104
|
+
return dt not in self.holiday_dates(dt.year)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Live market data from niftyindices.com CDN.
|
|
2
|
+
|
|
3
|
+
Note on removed endpoints
|
|
4
|
+
--------------------------
|
|
5
|
+
The following Backpage.aspx methods were found to be non-existent (HTTP 500
|
|
6
|
+
"Unknown web method") during live testing in June 2026 — they are not
|
|
7
|
+
implemented:
|
|
8
|
+
|
|
9
|
+
* ``getEquityStockWatch`` — page is a redirect button to nseindia.com
|
|
10
|
+
* ``getIndexMovers`` — redirect to nseindia.com
|
|
11
|
+
* ``getETFList`` — redirect to nseindia.com
|
|
12
|
+
* ``getReturnProfileData`` — page is server-rendered; no backing API
|
|
13
|
+
|
|
14
|
+
Use ``client.historical.price_history()`` or ``client.historical.tri_history()``
|
|
15
|
+
to compute return profiles from raw data.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import requests
|
|
20
|
+
import polars as pl
|
|
21
|
+
|
|
22
|
+
from ._http import BLOB_URL
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LiveMarket:
|
|
26
|
+
def __init__(self, session: requests.Session) -> None:
|
|
27
|
+
self._s = session
|
|
28
|
+
|
|
29
|
+
def live_indices(self) -> pl.DataFrame:
|
|
30
|
+
"""Snapshot of all live NSE index values from the CDN.
|
|
31
|
+
|
|
32
|
+
Endpoint: ``GET https://iislliveblob.niftyindices.com/jsonfiles/LiveIndicesWatch_new.json``
|
|
33
|
+
|
|
34
|
+
Refreshed every 15–30 s during market hours (09:15–15:30 IST, Mon–Fri).
|
|
35
|
+
Outside trading hours the previous session's close data is returned.
|
|
36
|
+
|
|
37
|
+
Actual response columns (verified June 2026):
|
|
38
|
+
``indexName``, ``last``, ``percChange``, ``icChange``, ``open``,
|
|
39
|
+
``high``, ``low``, ``previousClose``, ``timeVal``, ``yearHigh``,
|
|
40
|
+
``yearLow``, ``indicativeClose``, ``isConstituents``, ``constituents``
|
|
41
|
+
"""
|
|
42
|
+
resp = self._s.get(
|
|
43
|
+
f"{BLOB_URL}/jsonfiles/LiveIndicesWatch_new.json", timeout=10
|
|
44
|
+
)
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
payload = resp.json()
|
|
47
|
+
# CDN returns {"data": [...]} wrapper
|
|
48
|
+
data: list[dict] = payload.get("data", payload) if isinstance(payload, dict) else payload
|
|
49
|
+
if not data:
|
|
50
|
+
return pl.DataFrame()
|
|
51
|
+
return pl.DataFrame(data)
|
|
File without changes
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Daily and monthly report downloads from niftyindices.com."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
from datetime import date, timedelta
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
_BASE = "https://www.niftyindices.com"
|
|
12
|
+
|
|
13
|
+
_MONTH_UPPER = {
|
|
14
|
+
1: "JAN", 2: "FEB", 3: "MAR", 4: "APR",
|
|
15
|
+
5: "MAY", 6: "JUN", 7: "JUL", 8: "AUG",
|
|
16
|
+
9: "SEP", 10: "OCT", 11: "NOV", 12: "DEC",
|
|
17
|
+
}
|
|
18
|
+
_MONTH_MIXED = {
|
|
19
|
+
1: "January", 2: "February", 3: "March", 4: "April",
|
|
20
|
+
5: "May", 6: "June", 7: "July", 8: "August",
|
|
21
|
+
9: "September", 10: "October", 11: "November", 12: "December",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Reports:
|
|
26
|
+
def __init__(self, session: requests.Session) -> None:
|
|
27
|
+
self._s = session
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
# Daily reports
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def daily_snapshot(self, dt: date) -> pl.DataFrame | None:
|
|
34
|
+
"""Download and parse the daily index snapshot CSV for *dt*.
|
|
35
|
+
|
|
36
|
+
Returns a Polars DataFrame or ``None`` if *dt* was a non-trading day
|
|
37
|
+
(HTTP 404).
|
|
38
|
+
"""
|
|
39
|
+
url = (
|
|
40
|
+
f"{_BASE}/Daily_Snapshot/"
|
|
41
|
+
f"ind_close_all_{dt.strftime('%d%m%Y')}.csv"
|
|
42
|
+
)
|
|
43
|
+
resp = self._s.get(url, timeout=30)
|
|
44
|
+
if resp.status_code == 404:
|
|
45
|
+
return None
|
|
46
|
+
resp.raise_for_status()
|
|
47
|
+
return self._parse_snapshot(resp.content)
|
|
48
|
+
|
|
49
|
+
def daily_snapshot_bytes(self, dt: date) -> bytes | None:
|
|
50
|
+
"""Return raw CSV bytes for the daily snapshot, or ``None`` for non-trading days."""
|
|
51
|
+
url = (
|
|
52
|
+
f"{_BASE}/Daily_Snapshot/"
|
|
53
|
+
f"ind_close_all_{dt.strftime('%d%m%Y')}.csv"
|
|
54
|
+
)
|
|
55
|
+
resp = self._s.get(url, timeout=30)
|
|
56
|
+
if resp.status_code == 404:
|
|
57
|
+
return None
|
|
58
|
+
resp.raise_for_status()
|
|
59
|
+
return resp.content
|
|
60
|
+
|
|
61
|
+
def daily_snapshot_range(
|
|
62
|
+
self, start: date, end: date
|
|
63
|
+
) -> pl.DataFrame:
|
|
64
|
+
"""Download and combine daily snapshots for all trading days in [start, end]."""
|
|
65
|
+
frames: list[pl.DataFrame] = []
|
|
66
|
+
dt = start
|
|
67
|
+
while dt <= end:
|
|
68
|
+
df = self.daily_snapshot(dt)
|
|
69
|
+
if df is not None:
|
|
70
|
+
frames.append(df)
|
|
71
|
+
dt += timedelta(days=1)
|
|
72
|
+
return pl.concat(frames) if frames else pl.DataFrame()
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _parse_snapshot(content: bytes) -> pl.DataFrame:
|
|
76
|
+
df = pl.read_csv(
|
|
77
|
+
io.BytesIO(content),
|
|
78
|
+
encoding="utf8-lossy",
|
|
79
|
+
infer_schema_length=0,
|
|
80
|
+
)
|
|
81
|
+
df = df.rename({c: c.strip() for c in df.columns})
|
|
82
|
+
numeric_cols = [
|
|
83
|
+
"Open Index Value", "High Index Value", "Low Index Value",
|
|
84
|
+
"Closing Index Value", "Points Change", "Change(%)",
|
|
85
|
+
"Volume", "Turnover (Rs. Cr.)", "P/E", "P/B", "Div Yield",
|
|
86
|
+
]
|
|
87
|
+
casts = []
|
|
88
|
+
for col in numeric_cols:
|
|
89
|
+
if col in df.columns:
|
|
90
|
+
casts.append(
|
|
91
|
+
pl.col(col)
|
|
92
|
+
.str.replace_all(",", "")
|
|
93
|
+
.str.strip_chars()
|
|
94
|
+
.cast(pl.Float64, strict=False)
|
|
95
|
+
.alias(col)
|
|
96
|
+
)
|
|
97
|
+
if "Index Date" in df.columns:
|
|
98
|
+
casts.append(
|
|
99
|
+
pl.col("Index Date")
|
|
100
|
+
.str.to_date("%d-%b-%Y", strict=False)
|
|
101
|
+
.alias("Index Date")
|
|
102
|
+
)
|
|
103
|
+
return df.with_columns(casts) if casts else df
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Monthly report URLs
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def monthly_urls(self, year: int, month: int) -> dict[str, str]:
|
|
110
|
+
"""Return all known monthly report URLs for *year/month*."""
|
|
111
|
+
mmm = _MONTH_UPPER[month]
|
|
112
|
+
mon = _MONTH_MIXED[month]
|
|
113
|
+
y = str(year)
|
|
114
|
+
return {
|
|
115
|
+
"index_dashboard_equity": (
|
|
116
|
+
f"{_BASE}/Index_Dashboard/Index_Dashboard_{mmm}{y}.pdf"
|
|
117
|
+
),
|
|
118
|
+
"index_dashboard_fixed_income": (
|
|
119
|
+
f"{_BASE}/Index_Dashboard_Fixed_Income/"
|
|
120
|
+
f"Index_Dashboard_FixedIncome_{mmm}{y}.pdf"
|
|
121
|
+
),
|
|
122
|
+
"market_cap_weightage": (
|
|
123
|
+
f"{_BASE}/Market_Cap_Weightage/"
|
|
124
|
+
f"Market_Cap_Weightage_{mon}{y}.zip"
|
|
125
|
+
),
|
|
126
|
+
"impact_cost": (
|
|
127
|
+
f"{_BASE}/Impact_Cost/Impact_Cost_{mmm}{y}.pdf"
|
|
128
|
+
),
|
|
129
|
+
"passive_insights": (
|
|
130
|
+
f"{_BASE}/Nifty_Passive_Insights/"
|
|
131
|
+
f"NiftyPassiveFundReport-{mon}{y}.pdf"
|
|
132
|
+
),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def download_monthly(
|
|
136
|
+
self,
|
|
137
|
+
year: int,
|
|
138
|
+
month: int,
|
|
139
|
+
dest_dir: str = ".",
|
|
140
|
+
) -> dict[str, str]:
|
|
141
|
+
"""Download all monthly reports into *dest_dir*.
|
|
142
|
+
|
|
143
|
+
Returns a mapping of report name → saved file path (or an error
|
|
144
|
+
message if the file was not found).
|
|
145
|
+
"""
|
|
146
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
147
|
+
urls = self.monthly_urls(year, month)
|
|
148
|
+
results: dict[str, str] = {}
|
|
149
|
+
for name, url in urls.items():
|
|
150
|
+
resp = self._s.get(url, timeout=60)
|
|
151
|
+
if resp.status_code == 404:
|
|
152
|
+
results[name] = f"NOT FOUND: {url}"
|
|
153
|
+
continue
|
|
154
|
+
resp.raise_for_status()
|
|
155
|
+
ext = url.rsplit(".", 1)[-1]
|
|
156
|
+
path = os.path.join(dest_dir, f"{name}_{year}_{month:02d}.{ext}")
|
|
157
|
+
with open(path, "wb") as f:
|
|
158
|
+
f.write(resp.content)
|
|
159
|
+
results[name] = path
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
# Static resource downloads
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
_RESOURCES: dict[str, str] = {
|
|
167
|
+
"methodology_equity": (
|
|
168
|
+
f"{_BASE}/Methodology/Method_NIFTY_Equity_Indices.pdf"
|
|
169
|
+
),
|
|
170
|
+
"methodology_fixed_income": (
|
|
171
|
+
f"{_BASE}/Methodology/Method_NIFTY_Fixed_Income_Indices.pdf"
|
|
172
|
+
),
|
|
173
|
+
"tracking_error": (
|
|
174
|
+
f"{_BASE}/docs/default-source/tracking-error/trackingerror.pdf"
|
|
175
|
+
),
|
|
176
|
+
"benchmark_codes": (
|
|
177
|
+
f"{_BASE}/BenchmarkCodes/Nifty_Indices_Benchmark_Codes.pdf"
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def resource_urls(self) -> dict[str, str]:
|
|
182
|
+
"""Return URLs for static methodology and reference documents."""
|
|
183
|
+
return dict(self._RESOURCES)
|
|
184
|
+
|
|
185
|
+
def download_resource(self, name: str, dest_path: str) -> str:
|
|
186
|
+
"""Download a named static resource PDF to *dest_path*."""
|
|
187
|
+
if name not in self._RESOURCES:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"Unknown resource '{name}'. "
|
|
190
|
+
f"Available: {list(self._RESOURCES)}"
|
|
191
|
+
)
|
|
192
|
+
url = self._RESOURCES[name]
|
|
193
|
+
resp = self._s.get(url, timeout=60)
|
|
194
|
+
resp.raise_for_status()
|
|
195
|
+
with open(dest_path, "wb") as f:
|
|
196
|
+
f.write(resp.content)
|
|
197
|
+
return dest_path
|