tejhq 0.2.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.
- tejhq-0.2.0/.gitignore +16 -0
- tejhq-0.2.0/LICENSE +21 -0
- tejhq-0.2.0/PKG-INFO +257 -0
- tejhq-0.2.0/README.md +219 -0
- tejhq-0.2.0/pyproject.toml +72 -0
- tejhq-0.2.0/src/tej/__init__.py +73 -0
- tejhq-0.2.0/src/tej/_http.py +183 -0
- tejhq-0.2.0/src/tej/_version.py +1 -0
- tejhq-0.2.0/src/tej/async_client.py +293 -0
- tejhq-0.2.0/src/tej/client.py +308 -0
- tejhq-0.2.0/src/tej/exceptions.py +77 -0
- tejhq-0.2.0/src/tej/models.py +134 -0
- tejhq-0.2.0/src/tej/py.typed +0 -0
tejhq-0.2.0/.gitignore
ADDED
tejhq-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TejHQ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tejhq-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tejhq
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for tej-api: open EOD market data for NSE and BSE.
|
|
5
|
+
Project-URL: Homepage, https://tejhq.dev
|
|
6
|
+
Project-URL: Documentation, https://tejhq.dev/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/tejhq/tej-sdk-py
|
|
8
|
+
Project-URL: Issues, https://github.com/tejhq/tej-sdk-py/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/tejhq/tej-sdk-py/blob/master/CHANGELOG.md
|
|
10
|
+
Author-email: TejHQ <support@tejhq.dev>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: bhavcopy,bse,india,market-data,nse,ohlcv,stocks
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Provides-Extra: pandas
|
|
34
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
35
|
+
Provides-Extra: polars
|
|
36
|
+
Requires-Dist: polars>=1.0; extra == 'polars'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# tej
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/tejhq/)
|
|
42
|
+
[](https://pypi.org/project/tejhq/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
|
|
45
|
+
Python SDK for [tej-api](https://api.tejhq.dev): open, free, end-of-day market data for NSE and BSE. No signup, no keys, no catch.
|
|
46
|
+
|
|
47
|
+
**Zero runtime dependencies.** Sync and async clients with identical surface. Returns plain `list[dict]` rows so you can feed them straight to polars, pandas, or your own code without an extra conversion step.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install tejhq
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That is the entire install. No `httpx`, no `aiohttp`, no `pydantic`. Just stdlib. Distribution is `tejhq`; import as `tej`.
|
|
56
|
+
|
|
57
|
+
## Quick start (sync)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from tej import Client
|
|
61
|
+
|
|
62
|
+
c = Client()
|
|
63
|
+
|
|
64
|
+
# Last 90 days of RELIANCE (default range)
|
|
65
|
+
rows = c.ohlcv("RELIANCE", "nse")
|
|
66
|
+
print(rows[-1])
|
|
67
|
+
# {'date': '2026-06-05', 'open': 1442.0, 'high': 1455.7, ...}
|
|
68
|
+
|
|
69
|
+
# Full history since 2010
|
|
70
|
+
rows = c.ohlcv("RELIANCE", "nse", "2010-01-04", "2026-06-05")
|
|
71
|
+
print(len(rows)) # ~4040
|
|
72
|
+
|
|
73
|
+
# Full-market snapshot for one trading date
|
|
74
|
+
snap = c.snapshot("nse", "2025-05-28")
|
|
75
|
+
print(len(snap)) # ~2700 symbols
|
|
76
|
+
|
|
77
|
+
# Corporate actions (dividends, splits, bonuses)
|
|
78
|
+
acts = c.actions("RELIANCE")
|
|
79
|
+
print(acts[0])
|
|
80
|
+
# {'exchange': 'NSE', 'type': 'dividend', 'ex_date': '2024-10-28', 'cash_amount': 10.0, ...}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quick start (async)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import asyncio
|
|
87
|
+
from tej import AsyncClient
|
|
88
|
+
|
|
89
|
+
async def main():
|
|
90
|
+
async with AsyncClient() as c:
|
|
91
|
+
# Fan out 10 symbols concurrently
|
|
92
|
+
symbols = ["RELIANCE", "TCS", "INFY", "HDFCBANK", "ICICIBANK",
|
|
93
|
+
"SBIN", "HINDUNILVR", "ITC", "LT", "AXISBANK"]
|
|
94
|
+
results = await asyncio.gather(*[c.ohlcv(s, "nse") for s in symbols])
|
|
95
|
+
for sym, rows in zip(symbols, results):
|
|
96
|
+
print(sym, len(rows))
|
|
97
|
+
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Use with polars / pandas
|
|
102
|
+
|
|
103
|
+
`tej` returns `list[dict]` so you bring your own dataframe library.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import polars as pl
|
|
107
|
+
from tej import Client
|
|
108
|
+
|
|
109
|
+
rows = Client().ohlcv("RELIANCE", "nse", "2010-01-04", "2026-06-05")
|
|
110
|
+
df = pl.DataFrame(rows)
|
|
111
|
+
df = df.with_columns(pl.col("date").str.to_date())
|
|
112
|
+
df.head()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import pandas as pd
|
|
117
|
+
from tej import Client
|
|
118
|
+
|
|
119
|
+
rows = Client().ohlcv("RELIANCE", "nse", "2025-01-01", "2025-06-01")
|
|
120
|
+
df = pd.DataFrame(rows)
|
|
121
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
122
|
+
df.set_index("date", inplace=True)
|
|
123
|
+
df["close"].plot()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Optional extras install the dataframe library alongside the SDK:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install "tejhq[polars]"
|
|
130
|
+
pip install "tejhq[pandas]"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Free key endpoints
|
|
134
|
+
|
|
135
|
+
Get a key at [tejhq.dev/keys](https://tejhq.dev/keys), no card, no password.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from tej import Client
|
|
139
|
+
|
|
140
|
+
c = Client(api_key="tej_live_...")
|
|
141
|
+
|
|
142
|
+
# Back-adjusted prices, continuous through splits, bonuses and dividends
|
|
143
|
+
adj = c.adjusted("RELIANCE", "nse", "2024-10-01", "2024-11-30")
|
|
144
|
+
print(adj[-1]["adj_close"], adj[-1]["adj_factor_cumulative"])
|
|
145
|
+
|
|
146
|
+
# Symbol history by ISIN or symbol
|
|
147
|
+
c.symbols("nse", isin="INE040A01034")
|
|
148
|
+
c.symbols("nse", symbol="HDFCBANK")
|
|
149
|
+
|
|
150
|
+
# Account, keys, today's usage
|
|
151
|
+
c.me()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Pro endpoints
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
c = Client(api_key="tej_live_...") # a key on the Pro tier
|
|
158
|
+
|
|
159
|
+
# Derived metrics per day: returns, 52w high/low, average volume and turnover
|
|
160
|
+
m = c.metrics("RELIANCE", "nse", "2026-01-01")
|
|
161
|
+
|
|
162
|
+
# Point-in-time liquidity universe, survivorship-bias-free
|
|
163
|
+
members = c.universe("liquid500", "nse", as_of="2019-03-15")
|
|
164
|
+
symbols = [x["symbol"] for x in members]
|
|
165
|
+
|
|
166
|
+
# Up to 50 symbols in one request, dict keyed by symbol
|
|
167
|
+
bars = c.batch(["RELIANCE", "TCS", "INFY"], "nse", "2026-01-01")
|
|
168
|
+
|
|
169
|
+
# Free text to symbol
|
|
170
|
+
c.resolve("tata motors", exchange="nse", limit=3)
|
|
171
|
+
c.resolve("zomato") # former ticker resolves to the current one
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## API reference
|
|
175
|
+
|
|
176
|
+
| Method | Endpoint | Tier | Returns |
|
|
177
|
+
| --- | --- | --- | --- |
|
|
178
|
+
| `c.ohlcv(symbol, exchange, from_=None, to=None)` | `GET /v1/ohlcv/{exchange}/{symbol}` | keyless | `list[OHLCV]` |
|
|
179
|
+
| `c.snapshot(exchange, date)` | `GET /v1/snapshot/{exchange}?date=` | keyless | `list[SnapshotRow]` |
|
|
180
|
+
| `c.actions(symbol)` | `GET /v1/actions/{symbol}` | keyless | `list[Action]` |
|
|
181
|
+
| `c.adjusted(symbol, exchange, from_=None, to=None)` | `GET /v1/adjusted/{exchange}/{symbol}` | free key | `list[AdjustedRow]` |
|
|
182
|
+
| `c.symbols(exchange, symbol=None, isin=None)` | `GET /v1/symbols/{exchange}` | free key | `list[SymbolInterval]` |
|
|
183
|
+
| `c.me()` | `GET /v1/me` | free key | `dict` |
|
|
184
|
+
| `c.metrics(symbol, exchange, from_=None, to=None)` | `GET /v1/metrics/{exchange}/{symbol}` | pro | `list[MetricsRow]` |
|
|
185
|
+
| `c.universe(name, exchange="nse", as_of=None)` | `GET /v1/universe/{name}` | pro | `list[UniverseMember]` |
|
|
186
|
+
| `c.batch(symbols, exchange="nse", from_=None, to=None)` | `GET /v1/batch` | pro | `dict[str, list[OHLCV]]` |
|
|
187
|
+
| `c.resolve(q, exchange="both", limit=5)` | `GET /v1/resolve` | pro | `list[ResolveHit]` |
|
|
188
|
+
| `c.health()`, `c.ready()` | `GET /health`, `GET /ready` | keyless | `dict` |
|
|
189
|
+
|
|
190
|
+
Need the response envelope (with `meta`)? Use `c.ohlcv_envelope(...)`, which returns the raw `{"data": [...], "meta": {...}}` dict.
|
|
191
|
+
|
|
192
|
+
`AsyncClient` exposes the same surface with `await`.
|
|
193
|
+
|
|
194
|
+
## Configuration
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from tej import Client
|
|
198
|
+
|
|
199
|
+
c = Client(
|
|
200
|
+
base_url="https://api.tejhq.dev", # override for self-hosted or staging
|
|
201
|
+
api_key=None, # tej_live_... from https://tejhq.dev/keys; keyless works for ohlcv, snapshot, actions
|
|
202
|
+
timeout=30.0, # seconds
|
|
203
|
+
max_retries=3, # exponential backoff on 5xx/429/network
|
|
204
|
+
user_agent_suffix="my-app/1.0", # for attribution
|
|
205
|
+
default_headers={"X-My-Header": "hi"},
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Errors
|
|
210
|
+
|
|
211
|
+
All errors inherit from `tej.TejError`. The specific subclass tells you what happened:
|
|
212
|
+
|
|
213
|
+
| Exception | When |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `BadRequestError` | HTTP 400, bad path or query parameter (also raised locally on invalid args before the request goes out, as a plain `ValueError`) |
|
|
216
|
+
| `AuthError` | HTTP 401, `key_required` when no key was sent to a gated endpoint, `invalid_key` when the key is malformed, unknown, or revoked |
|
|
217
|
+
| `NotFoundError` | HTTP 404 |
|
|
218
|
+
| `ProRequiredError` | HTTP 402, the key's tier is too low for this endpoint (`/v1/universe`, `/v1/metrics` need Pro) |
|
|
219
|
+
| `TejError` with `error_code == "key_required"` | HTTP 401, the endpoint needs a key and none was sent (`/v1/adjusted`, `/v1/symbols`) |
|
|
220
|
+
| `TejError` with `error_code == "invalid_key"` | HTTP 401, the key is malformed, unknown, or revoked |
|
|
221
|
+
| `RateLimitError` | HTTP 429 |
|
|
222
|
+
| `ServerError` | HTTP 5xx |
|
|
223
|
+
| `NetworkError` | DNS, connection, TLS, or timeout failure |
|
|
224
|
+
| `TejError` | Anything else |
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from tej import Client, ProRequiredError
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
Client().ohlcv("RELIANCE", "nse")
|
|
231
|
+
except ProRequiredError as e:
|
|
232
|
+
print(e.error_code) # 'pro_required'
|
|
233
|
+
print(e.request_id) # tej-api request id from response headers
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Why a separate package
|
|
237
|
+
|
|
238
|
+
[tej-api](https://github.com/tejhq/tej-api) is a Go service. [tej-bazaar](https://github.com/tejhq/tej-bazaar) is the Python ingestion pipeline. This repo is the thin client that anyone can `pip install` without dragging in either, and that releases on its own cadence as the API evolves.
|
|
239
|
+
|
|
240
|
+
## Coverage
|
|
241
|
+
|
|
242
|
+
- **NSE bhavcopy + corp actions**: 2010-01-04 to today, ~4,047 trading days, ~7M rows
|
|
243
|
+
- **BSE bhavcopy + corp actions**: 2024-07-08 to today (the SEBI CMTS cutover), ~470 days, ~1M rows
|
|
244
|
+
- **Cron refresh**: weekdays 20:00 IST
|
|
245
|
+
|
|
246
|
+
Keyless endpoints are served at the Cloudflare edge from pre-rendered JSON, so most requests return in well under a second and repeat requests are cache hits. Keyless access is rate limited to 100 requests per 10 seconds per IP at the edge and 120 per minute at origin; the SDK retries 429s with backoff. Keyed requests bypass the edge cache and carry `X-RateLimit-Limit-Day` and `X-RateLimit-Remaining-Day` headers: 1,000 a day and 300 a minute on a free key.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT. Use it, fork it, ship products on top of it.
|
|
251
|
+
|
|
252
|
+
## Links
|
|
253
|
+
|
|
254
|
+
- API docs: [tejhq.dev/docs](https://tejhq.dev/docs)
|
|
255
|
+
- OpenAPI spec: [api.tejhq.dev/openapi.yaml](https://api.tejhq.dev/openapi.yaml)
|
|
256
|
+
- Bulk parquet downloads: [data.tejhq.dev](https://data.tejhq.dev) and [huggingface.co/datasets/tejhq/indian-markets](https://huggingface.co/datasets/tejhq/indian-markets)
|
|
257
|
+
- Issues: [github.com/tejhq/tej-sdk-py/issues](https://github.com/tejhq/tej-sdk-py/issues)
|
tejhq-0.2.0/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# tej
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/tejhq/)
|
|
4
|
+
[](https://pypi.org/project/tejhq/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Python SDK for [tej-api](https://api.tejhq.dev): open, free, end-of-day market data for NSE and BSE. No signup, no keys, no catch.
|
|
8
|
+
|
|
9
|
+
**Zero runtime dependencies.** Sync and async clients with identical surface. Returns plain `list[dict]` rows so you can feed them straight to polars, pandas, or your own code without an extra conversion step.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install tejhq
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That is the entire install. No `httpx`, no `aiohttp`, no `pydantic`. Just stdlib. Distribution is `tejhq`; import as `tej`.
|
|
18
|
+
|
|
19
|
+
## Quick start (sync)
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from tej import Client
|
|
23
|
+
|
|
24
|
+
c = Client()
|
|
25
|
+
|
|
26
|
+
# Last 90 days of RELIANCE (default range)
|
|
27
|
+
rows = c.ohlcv("RELIANCE", "nse")
|
|
28
|
+
print(rows[-1])
|
|
29
|
+
# {'date': '2026-06-05', 'open': 1442.0, 'high': 1455.7, ...}
|
|
30
|
+
|
|
31
|
+
# Full history since 2010
|
|
32
|
+
rows = c.ohlcv("RELIANCE", "nse", "2010-01-04", "2026-06-05")
|
|
33
|
+
print(len(rows)) # ~4040
|
|
34
|
+
|
|
35
|
+
# Full-market snapshot for one trading date
|
|
36
|
+
snap = c.snapshot("nse", "2025-05-28")
|
|
37
|
+
print(len(snap)) # ~2700 symbols
|
|
38
|
+
|
|
39
|
+
# Corporate actions (dividends, splits, bonuses)
|
|
40
|
+
acts = c.actions("RELIANCE")
|
|
41
|
+
print(acts[0])
|
|
42
|
+
# {'exchange': 'NSE', 'type': 'dividend', 'ex_date': '2024-10-28', 'cash_amount': 10.0, ...}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start (async)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from tej import AsyncClient
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
async with AsyncClient() as c:
|
|
53
|
+
# Fan out 10 symbols concurrently
|
|
54
|
+
symbols = ["RELIANCE", "TCS", "INFY", "HDFCBANK", "ICICIBANK",
|
|
55
|
+
"SBIN", "HINDUNILVR", "ITC", "LT", "AXISBANK"]
|
|
56
|
+
results = await asyncio.gather(*[c.ohlcv(s, "nse") for s in symbols])
|
|
57
|
+
for sym, rows in zip(symbols, results):
|
|
58
|
+
print(sym, len(rows))
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Use with polars / pandas
|
|
64
|
+
|
|
65
|
+
`tej` returns `list[dict]` so you bring your own dataframe library.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import polars as pl
|
|
69
|
+
from tej import Client
|
|
70
|
+
|
|
71
|
+
rows = Client().ohlcv("RELIANCE", "nse", "2010-01-04", "2026-06-05")
|
|
72
|
+
df = pl.DataFrame(rows)
|
|
73
|
+
df = df.with_columns(pl.col("date").str.to_date())
|
|
74
|
+
df.head()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import pandas as pd
|
|
79
|
+
from tej import Client
|
|
80
|
+
|
|
81
|
+
rows = Client().ohlcv("RELIANCE", "nse", "2025-01-01", "2025-06-01")
|
|
82
|
+
df = pd.DataFrame(rows)
|
|
83
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
84
|
+
df.set_index("date", inplace=True)
|
|
85
|
+
df["close"].plot()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Optional extras install the dataframe library alongside the SDK:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install "tejhq[polars]"
|
|
92
|
+
pip install "tejhq[pandas]"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Free key endpoints
|
|
96
|
+
|
|
97
|
+
Get a key at [tejhq.dev/keys](https://tejhq.dev/keys), no card, no password.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from tej import Client
|
|
101
|
+
|
|
102
|
+
c = Client(api_key="tej_live_...")
|
|
103
|
+
|
|
104
|
+
# Back-adjusted prices, continuous through splits, bonuses and dividends
|
|
105
|
+
adj = c.adjusted("RELIANCE", "nse", "2024-10-01", "2024-11-30")
|
|
106
|
+
print(adj[-1]["adj_close"], adj[-1]["adj_factor_cumulative"])
|
|
107
|
+
|
|
108
|
+
# Symbol history by ISIN or symbol
|
|
109
|
+
c.symbols("nse", isin="INE040A01034")
|
|
110
|
+
c.symbols("nse", symbol="HDFCBANK")
|
|
111
|
+
|
|
112
|
+
# Account, keys, today's usage
|
|
113
|
+
c.me()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Pro endpoints
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
c = Client(api_key="tej_live_...") # a key on the Pro tier
|
|
120
|
+
|
|
121
|
+
# Derived metrics per day: returns, 52w high/low, average volume and turnover
|
|
122
|
+
m = c.metrics("RELIANCE", "nse", "2026-01-01")
|
|
123
|
+
|
|
124
|
+
# Point-in-time liquidity universe, survivorship-bias-free
|
|
125
|
+
members = c.universe("liquid500", "nse", as_of="2019-03-15")
|
|
126
|
+
symbols = [x["symbol"] for x in members]
|
|
127
|
+
|
|
128
|
+
# Up to 50 symbols in one request, dict keyed by symbol
|
|
129
|
+
bars = c.batch(["RELIANCE", "TCS", "INFY"], "nse", "2026-01-01")
|
|
130
|
+
|
|
131
|
+
# Free text to symbol
|
|
132
|
+
c.resolve("tata motors", exchange="nse", limit=3)
|
|
133
|
+
c.resolve("zomato") # former ticker resolves to the current one
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## API reference
|
|
137
|
+
|
|
138
|
+
| Method | Endpoint | Tier | Returns |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| `c.ohlcv(symbol, exchange, from_=None, to=None)` | `GET /v1/ohlcv/{exchange}/{symbol}` | keyless | `list[OHLCV]` |
|
|
141
|
+
| `c.snapshot(exchange, date)` | `GET /v1/snapshot/{exchange}?date=` | keyless | `list[SnapshotRow]` |
|
|
142
|
+
| `c.actions(symbol)` | `GET /v1/actions/{symbol}` | keyless | `list[Action]` |
|
|
143
|
+
| `c.adjusted(symbol, exchange, from_=None, to=None)` | `GET /v1/adjusted/{exchange}/{symbol}` | free key | `list[AdjustedRow]` |
|
|
144
|
+
| `c.symbols(exchange, symbol=None, isin=None)` | `GET /v1/symbols/{exchange}` | free key | `list[SymbolInterval]` |
|
|
145
|
+
| `c.me()` | `GET /v1/me` | free key | `dict` |
|
|
146
|
+
| `c.metrics(symbol, exchange, from_=None, to=None)` | `GET /v1/metrics/{exchange}/{symbol}` | pro | `list[MetricsRow]` |
|
|
147
|
+
| `c.universe(name, exchange="nse", as_of=None)` | `GET /v1/universe/{name}` | pro | `list[UniverseMember]` |
|
|
148
|
+
| `c.batch(symbols, exchange="nse", from_=None, to=None)` | `GET /v1/batch` | pro | `dict[str, list[OHLCV]]` |
|
|
149
|
+
| `c.resolve(q, exchange="both", limit=5)` | `GET /v1/resolve` | pro | `list[ResolveHit]` |
|
|
150
|
+
| `c.health()`, `c.ready()` | `GET /health`, `GET /ready` | keyless | `dict` |
|
|
151
|
+
|
|
152
|
+
Need the response envelope (with `meta`)? Use `c.ohlcv_envelope(...)`, which returns the raw `{"data": [...], "meta": {...}}` dict.
|
|
153
|
+
|
|
154
|
+
`AsyncClient` exposes the same surface with `await`.
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from tej import Client
|
|
160
|
+
|
|
161
|
+
c = Client(
|
|
162
|
+
base_url="https://api.tejhq.dev", # override for self-hosted or staging
|
|
163
|
+
api_key=None, # tej_live_... from https://tejhq.dev/keys; keyless works for ohlcv, snapshot, actions
|
|
164
|
+
timeout=30.0, # seconds
|
|
165
|
+
max_retries=3, # exponential backoff on 5xx/429/network
|
|
166
|
+
user_agent_suffix="my-app/1.0", # for attribution
|
|
167
|
+
default_headers={"X-My-Header": "hi"},
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Errors
|
|
172
|
+
|
|
173
|
+
All errors inherit from `tej.TejError`. The specific subclass tells you what happened:
|
|
174
|
+
|
|
175
|
+
| Exception | When |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `BadRequestError` | HTTP 400, bad path or query parameter (also raised locally on invalid args before the request goes out, as a plain `ValueError`) |
|
|
178
|
+
| `AuthError` | HTTP 401, `key_required` when no key was sent to a gated endpoint, `invalid_key` when the key is malformed, unknown, or revoked |
|
|
179
|
+
| `NotFoundError` | HTTP 404 |
|
|
180
|
+
| `ProRequiredError` | HTTP 402, the key's tier is too low for this endpoint (`/v1/universe`, `/v1/metrics` need Pro) |
|
|
181
|
+
| `TejError` with `error_code == "key_required"` | HTTP 401, the endpoint needs a key and none was sent (`/v1/adjusted`, `/v1/symbols`) |
|
|
182
|
+
| `TejError` with `error_code == "invalid_key"` | HTTP 401, the key is malformed, unknown, or revoked |
|
|
183
|
+
| `RateLimitError` | HTTP 429 |
|
|
184
|
+
| `ServerError` | HTTP 5xx |
|
|
185
|
+
| `NetworkError` | DNS, connection, TLS, or timeout failure |
|
|
186
|
+
| `TejError` | Anything else |
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from tej import Client, ProRequiredError
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
Client().ohlcv("RELIANCE", "nse")
|
|
193
|
+
except ProRequiredError as e:
|
|
194
|
+
print(e.error_code) # 'pro_required'
|
|
195
|
+
print(e.request_id) # tej-api request id from response headers
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Why a separate package
|
|
199
|
+
|
|
200
|
+
[tej-api](https://github.com/tejhq/tej-api) is a Go service. [tej-bazaar](https://github.com/tejhq/tej-bazaar) is the Python ingestion pipeline. This repo is the thin client that anyone can `pip install` without dragging in either, and that releases on its own cadence as the API evolves.
|
|
201
|
+
|
|
202
|
+
## Coverage
|
|
203
|
+
|
|
204
|
+
- **NSE bhavcopy + corp actions**: 2010-01-04 to today, ~4,047 trading days, ~7M rows
|
|
205
|
+
- **BSE bhavcopy + corp actions**: 2024-07-08 to today (the SEBI CMTS cutover), ~470 days, ~1M rows
|
|
206
|
+
- **Cron refresh**: weekdays 20:00 IST
|
|
207
|
+
|
|
208
|
+
Keyless endpoints are served at the Cloudflare edge from pre-rendered JSON, so most requests return in well under a second and repeat requests are cache hits. Keyless access is rate limited to 100 requests per 10 seconds per IP at the edge and 120 per minute at origin; the SDK retries 429s with backoff. Keyed requests bypass the edge cache and carry `X-RateLimit-Limit-Day` and `X-RateLimit-Remaining-Day` headers: 1,000 a day and 300 a minute on a free key.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT. Use it, fork it, ship products on top of it.
|
|
213
|
+
|
|
214
|
+
## Links
|
|
215
|
+
|
|
216
|
+
- API docs: [tejhq.dev/docs](https://tejhq.dev/docs)
|
|
217
|
+
- OpenAPI spec: [api.tejhq.dev/openapi.yaml](https://api.tejhq.dev/openapi.yaml)
|
|
218
|
+
- Bulk parquet downloads: [data.tejhq.dev](https://data.tejhq.dev) and [huggingface.co/datasets/tejhq/indian-markets](https://huggingface.co/datasets/tejhq/indian-markets)
|
|
219
|
+
- Issues: [github.com/tejhq/tej-sdk-py/issues](https://github.com/tejhq/tej-sdk-py/issues)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tejhq"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python SDK for tej-api: open EOD market data for NSE and BSE."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "TejHQ", email = "support@tejhq.dev" }]
|
|
13
|
+
keywords = ["nse", "bse", "bhavcopy", "ohlcv", "market-data", "india", "stocks"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = []
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
polars = ["polars>=1.0"]
|
|
33
|
+
pandas = ["pandas>=2.0"]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0",
|
|
36
|
+
"pytest-asyncio>=0.23",
|
|
37
|
+
"ruff>=0.6",
|
|
38
|
+
"mypy>=1.10",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://tejhq.dev"
|
|
43
|
+
Documentation = "https://tejhq.dev/docs"
|
|
44
|
+
Repository = "https://github.com/tejhq/tej-sdk-py"
|
|
45
|
+
Issues = "https://github.com/tejhq/tej-sdk-py/issues"
|
|
46
|
+
Changelog = "https://github.com/tejhq/tej-sdk-py/blob/master/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.version]
|
|
49
|
+
path = "src/tej/_version.py"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/tej"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = ["src/tej", "README.md", "LICENSE", "pyproject.toml"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
asyncio_mode = "auto"
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
target-version = "py39"
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
# mypy dropped 3.9 target support; runtime 3.9 compat is covered by the CI matrix
|
|
70
|
+
python_version = "3.10"
|
|
71
|
+
strict = true
|
|
72
|
+
files = ["src/tej"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Python SDK for `tej-api <https://api.tejhq.dev>`_.
|
|
2
|
+
|
|
3
|
+
Zero runtime dependencies. Sync + async clients with identical surface.
|
|
4
|
+
|
|
5
|
+
Quick start::
|
|
6
|
+
|
|
7
|
+
from tej import Client
|
|
8
|
+
|
|
9
|
+
c = Client()
|
|
10
|
+
rows = c.ohlcv("RELIANCE", "nse", "2025-01-01", "2025-01-31")
|
|
11
|
+
print(len(rows))
|
|
12
|
+
|
|
13
|
+
Async::
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from tej import AsyncClient
|
|
17
|
+
|
|
18
|
+
async def main():
|
|
19
|
+
async with AsyncClient() as c:
|
|
20
|
+
return await c.ohlcv("RELIANCE", "nse", "2025-01-01", "2025-01-31")
|
|
21
|
+
|
|
22
|
+
asyncio.run(main())
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from ._version import __version__
|
|
26
|
+
from .async_client import AsyncClient
|
|
27
|
+
from .client import Client
|
|
28
|
+
from .exceptions import (
|
|
29
|
+
AuthError,
|
|
30
|
+
BadRequestError,
|
|
31
|
+
NetworkError,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
ProRequiredError,
|
|
34
|
+
RateLimitError,
|
|
35
|
+
ServerError,
|
|
36
|
+
TejError,
|
|
37
|
+
)
|
|
38
|
+
from .models import (
|
|
39
|
+
OHLCV,
|
|
40
|
+
Action,
|
|
41
|
+
AdjustedRow,
|
|
42
|
+
Envelope,
|
|
43
|
+
Exchange,
|
|
44
|
+
MetricsRow,
|
|
45
|
+
ResolveHit,
|
|
46
|
+
SnapshotRow,
|
|
47
|
+
SymbolInterval,
|
|
48
|
+
UniverseMember,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"__version__",
|
|
53
|
+
"Action",
|
|
54
|
+
"AdjustedRow",
|
|
55
|
+
"AsyncClient",
|
|
56
|
+
"AuthError",
|
|
57
|
+
"BadRequestError",
|
|
58
|
+
"Client",
|
|
59
|
+
"Envelope",
|
|
60
|
+
"Exchange",
|
|
61
|
+
"MetricsRow",
|
|
62
|
+
"NetworkError",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
"OHLCV",
|
|
65
|
+
"ProRequiredError",
|
|
66
|
+
"RateLimitError",
|
|
67
|
+
"ResolveHit",
|
|
68
|
+
"ServerError",
|
|
69
|
+
"SnapshotRow",
|
|
70
|
+
"SymbolInterval",
|
|
71
|
+
"TejError",
|
|
72
|
+
"UniverseMember",
|
|
73
|
+
]
|